From 9801f69d0b4f272219dcd6668934b81f9d8bf9cf Mon Sep 17 00:00:00 2001 From: Joppe Blondel Date: Sun, 3 Aug 2025 12:55:31 +0200 Subject: [PATCH] Added high-res pass --- negstation/image_pipeline.py | 16 ++- negstation/negstation.py | 3 + negstation/widgets/export_widget.py | 33 +++++ negstation/widgets/open_image_widget.py | 32 ++++- negstation/widgets/open_raw_widget.py | 28 +++- negstation/widgets/pipeline_stage_widget.py | 38 ++++-- negstation/widgets/stage_viewer_widget.py | 12 +- negstation_layout.ini | 136 +++++++++++++------- negstation_widgets.json | 4 + 9 files changed, 233 insertions(+), 69 deletions(-) create mode 100644 negstation/widgets/export_widget.py diff --git a/negstation/image_pipeline.py b/negstation/image_pipeline.py index 944377a..95e0b11 100644 --- a/negstation/image_pipeline.py +++ b/negstation/image_pipeline.py @@ -9,10 +9,12 @@ class ImagePipeline: self.id_counter = 0 self.stages = {} self.stagedata = {} + self.stagedata_full = {} def register_stage(self, name: str): self.stages[self.id_counter] = name self.stagedata[self.id_counter] = None + self.stagedata_full[self.id_counter] = None self.bus.publish_deferred("pipeline_stages", self.stages) self.id_counter += 1 return self.id_counter-1 @@ -22,9 +24,17 @@ class ImagePipeline: self.stages[id] = name self.bus.publish_deferred("pipeline_stages", self.stages) - def publish(self, id: int, img: np.ndarray): - self.stagedata[id] = img.astype(np.float32) - self.bus.publish_deferred("pipeline_stage", (id, self.stagedata[id])) + def publish(self, id: int, img: np.ndarray, full_res=False): + if img is None: + return + if full_res: + self.stagedata_full[id] = img.astype(np.float32) + self.bus.publish_deferred( + "pipeline_stage_full", (id, self.stagedata_full[id])) + else: + self.stagedata[id] = img.astype(np.float32) + self.bus.publish_deferred( + "pipeline_stage", (id, self.stagedata[id])) def get_stage_data(self, id: int): if id >= 0 and id < len(self.stages): diff --git a/negstation/negstation.py b/negstation/negstation.py index ab3c92b..f3be9bc 100644 --- a/negstation/negstation.py +++ b/negstation/negstation.py @@ -95,6 +95,9 @@ class EditorManager: dpg.add_menu_item( label="Save Layout", callback=self.layout_manager.save_layout ) + dpg.add_menu_item( + label="Run full-res pipeline", callback=lambda: self.bus.publish_deferred("process_full_res", None) + ) dpg.add_menu_item( label="Quit", callback=lambda: dpg.stop_dearpygui() ) diff --git a/negstation/widgets/export_widget.py b/negstation/widgets/export_widget.py new file mode 100644 index 0000000..409c89b --- /dev/null +++ b/negstation/widgets/export_widget.py @@ -0,0 +1,33 @@ +import dearpygui.dearpygui as dpg +import numpy as np + +from .pipeline_stage_widget import PipelineStageWidget + + +class ExportStage(PipelineStageWidget): + name = "Export Image" + register = True + has_pipeline_in = True + has_pipeline_out = False + + def __init__(self, manager, logger): + super().__init__(manager, logger, default_stage_out="opened_image") + self.manager.bus.subscribe( + "process_full_res", self._on_process_full_res, True) + + def create_pipeline_stage_content(self): + dpg.add_text("Some export fields") + + def _on_process_full_res(self, data): + self.logger.info("Starting full res pipeline export") + + def on_pipeline_data(self, img): + if img is None: + return + self.logger.info("low res image received, ignore") + + def on_full_res_pipeline_data(self, img): + if img is None: + return + h, w, _ = img.shape + self.logger.info(f"Full res image received: {w}x{h}") diff --git a/negstation/widgets/open_image_widget.py b/negstation/widgets/open_image_widget.py index bef294d..6ca0444 100644 --- a/negstation/widgets/open_image_widget.py +++ b/negstation/widgets/open_image_widget.py @@ -15,6 +15,11 @@ class OpenImageWidget(PipelineStageWidget): super().__init__(manager, logger, default_stage_out="opened_image") self.dialog_tag = dpg.generate_uuid() self.output_tag = dpg.generate_uuid() + self.img = None + self.img_full = None + + self.manager.bus.subscribe( + "process_full_res", self._on_process_full_res, True) def create_pipeline_stage_content(self): with dpg.file_dialog( @@ -46,9 +51,30 @@ class OpenImageWidget(PipelineStageWidget): self.logger.info(f"Selected file '{selection}'") try: img = Image.open(selection).convert("RGBA") - arr = np.asarray(img).astype(np.float32) / \ + rgba = np.asarray(img).astype(np.float32) / \ 255.0 # normalize to [0,1] - # Publish into pipeline - self.manager.pipeline.publish(self.pipeline_stage_out_id, arr) + h, w, _ = rgba.shape + + # scale for small version + max_dim = 500 + scale = min(1.0, max_dim / w, max_dim / h) + if scale < 1.0: + # convert to 0–255 uint8, resize with PIL, back to float32 [0–1] + pil = Image.fromarray( + (rgba * 255).astype(np.uint8), mode="RGBA") + new_w, new_h = int(w * scale), int(h * scale) + pil = pil.resize((new_w, new_h), Image.LANCZOS) + rgba_small = np.asarray(pil).astype(np.float32) / 255.0 + w_small, h_small = new_w, new_h + + self.img_full = rgba + self.img = rgba_small + + self.manager.pipeline.publish( + self.pipeline_stage_out_id, rgba_small) except Exception as e: self.logger.error(f"Failed to load image {selection}: {e}") + + def _on_process_full_res(self, data): + self.manager.pipeline.publish( + self.pipeline_stage_out_id, self.img_full, True) diff --git a/negstation/widgets/open_raw_widget.py b/negstation/widgets/open_raw_widget.py index bc4c5bd..4e9d14a 100644 --- a/negstation/widgets/open_raw_widget.py +++ b/negstation/widgets/open_raw_widget.py @@ -1,6 +1,7 @@ import dearpygui.dearpygui as dpg import rawpy import numpy as np +from PIL import Image from .pipeline_stage_widget import PipelineStageWidget @@ -18,6 +19,8 @@ class OpenRawWidget(PipelineStageWidget): self.config_group = dpg.generate_uuid() self.busy_group = dpg.generate_uuid() self.raw_path = None + self.img = None + self.img_full = None self.config = { # Demosaic algorithm "demosaic_algorithm": rawpy.DemosaicAlgorithm.AHD, @@ -39,6 +42,9 @@ class OpenRawWidget(PipelineStageWidget): "four_color_rgb": False, } + self.manager.bus.subscribe( + "process_full_res", self._on_process_full_res, True) + def get_config(self): return {} @@ -203,6 +209,26 @@ class OpenRawWidget(PipelineStageWidget): rgba = np.concatenate([rgb_float, alpha], axis=2) - self.manager.pipeline.publish(self.pipeline_stage_out_id, rgba) + # scale for small version + max_dim = 500 + scale = min(1.0, max_dim / w, max_dim / h) + if scale < 1.0: + # convert to 0–255 uint8, resize with PIL, back to float32 [0–1] + pil = Image.fromarray((rgba * 255).astype(np.uint8), mode="RGBA") + new_w, new_h = int(w * scale), int(h * scale) + pil = pil.resize((new_w, new_h), Image.LANCZOS) + rgba_small = np.asarray(pil).astype(np.float32) / 255.0 + w_small, h_small = new_w, new_h + + self.img_full = rgba + self.img = rgba_small + + self.manager.pipeline.publish(self.pipeline_stage_out_id, rgba_small) dpg.configure_item(self.config_group, show=True) dpg.configure_item(self.busy_group, show=False) + + def _on_process_full_res(self, data): + if self.img_full is None: + return + self.manager.pipeline.publish( + self.pipeline_stage_out_id, self.img_full, True) diff --git a/negstation/widgets/pipeline_stage_widget.py b/negstation/widgets/pipeline_stage_widget.py index 08cdde1..9724235 100644 --- a/negstation/widgets/pipeline_stage_widget.py +++ b/negstation/widgets/pipeline_stage_widget.py @@ -23,16 +23,21 @@ class PipelineStageWidget(BaseWidget): self.pipeline_stage_out_id = None self.pipeline_config_group_tag = dpg.generate_uuid() self.stage_in_combo = dpg.generate_uuid() + self._last_full = False if self.has_pipeline_out: self.pipeline_stage_out_id = self.manager.pipeline.register_stage( default_stage_out ) - self.manager.bus.subscribe("pipeline_stages", self._on_stage_list, True) + self.manager.bus.subscribe( + "pipeline_stages", self._on_stage_list, True) if self.has_pipeline_in: self.pipeline_stage_in_id = 0 - self.manager.bus.subscribe("pipeline_stage", self._on_stage_data, True) + self.manager.bus.subscribe( + "pipeline_stage", self._on_stage_data, True) + self.manager.bus.subscribe( + "pipeline_stage_full", self._on_stage_data_full, True) # force getting all available pipeline stages self.manager.pipeline.republish_stages() @@ -43,7 +48,8 @@ class PipelineStageWidget(BaseWidget): label="Stage In", items=[], callback=self._on_stage_in_select, - default_value=f"{self.manager.pipeline.get_stage_name(0)} : 0", + default_value=f"{ + self.manager.pipeline.get_stage_name(0)} : 0", tag=self.stage_in_combo ) if self.has_pipeline_out: @@ -59,11 +65,6 @@ class PipelineStageWidget(BaseWidget): dpg.add_separator() self.create_pipeline_stage_content() - def publish_stage(self, img: np.ndarray): - """Publishes an image to output stage""" - if self.has_pipeline_out: - self.manager.pipeline.publish(self.pipeline_stage_out_id, img) - def create_pipeline_stage_content(self): """Must be implemented by the widget, creates the content of the window""" raise NotImplementedError @@ -72,6 +73,12 @@ class PipelineStageWidget(BaseWidget): """Must be implemented by the widget, is called when there is a new image published on the in stage""" pass + def publish_stage(self, img): + """Publishes an image to output stage""" + if self.has_pipeline_out: + self.manager.pipeline.publish( + self.pipeline_stage_out_id, img, full_res=self._last_full) + # Callbacks def _on_window_close(self): @@ -97,12 +104,25 @@ class PipelineStageWidget(BaseWidget): pipeline_id = data[0] img = data[1] if self.has_pipeline_in and pipeline_id == self.pipeline_stage_in_id: + self._last_full = False self.on_pipeline_data(img) + def _on_stage_data_full(self, data): + pipeline_id = data[0] + img = data[1] + if self.has_pipeline_in and pipeline_id == self.pipeline_stage_in_id: + self._last_full = True + if hasattr(self, "on_full_res_pipeline_data"): + self.on_full_res_pipeline_data(img) + else: + self.on_pipeline_data(img) + # Override the window resize callback + def _on_window_resize(self, data): win_w, win_h = dpg.get_item_rect_size(self.window_tag) - group_w, group_h = dpg.get_item_rect_size(self.pipeline_config_group_tag) + group_w, group_h = dpg.get_item_rect_size( + self.pipeline_config_group_tag) group_x, group_y = dpg.get_item_pos(self.pipeline_config_group_tag) self.window_height = win_h - group_h - group_y - 12 self.window_width = win_w - 7 diff --git a/negstation/widgets/stage_viewer_widget.py b/negstation/widgets/stage_viewer_widget.py index ad1138b..fce6661 100644 --- a/negstation/widgets/stage_viewer_widget.py +++ b/negstation/widgets/stage_viewer_widget.py @@ -31,19 +31,13 @@ class PipelineStageViewer(PipelineStageWidget): if img is None: return h, w, _ = img.shape - max_dim = 500 - scale = min(1.0, max_dim / w, max_dim / h) - if scale < 1.0: - # convert to 0–255 uint8, resize with PIL, back to float32 [0–1] - pil = Image.fromarray((img * 255).astype(np.uint8), mode="RGBA") - new_w, new_h = int(w * scale), int(h * scale) - pil = pil.resize((new_w, new_h), Image.LANCZOS) - img = np.asarray(pil).astype(np.float32) / 255.0 - w, h = new_w, new_h self.img = img self.needs_update = True + def on_full_res_pipeline_data(self, img): + pass + def update_texture(self, img: np.ndarray): """Only call from update function""" # TODO show a smaller version of the image to speed things up diff --git a/negstation_layout.ini b/negstation_layout.ini index e496bf2..de2a8bf 100644 --- a/negstation_layout.ini +++ b/negstation_layout.ini @@ -60,7 +60,7 @@ DockId=0x00000012,0 Pos=198,19 Size=602,425 Collapsed=0 -DockId=0x00000011,0 +DockId=0x0000000F,0 [Window][###49] Pos=0,495 @@ -72,7 +72,7 @@ DockId=0x0000000C,0 Pos=246,19 Size=554,379 Collapsed=0 -DockId=0x00000011,0 +DockId=0x0000000F,0 [Window][###60] Pos=0,19 @@ -81,16 +81,16 @@ Collapsed=0 DockId=0x00000012,1 [Window][###107] -Pos=246,400 -Size=554,200 +Pos=939,19 +Size=261,534 Collapsed=0 -DockId=0x00000016,0 +DockId=0x00000011,0 [Window][###35] Pos=272,19 Size=710,625 Collapsed=0 -DockId=0x00000011,0 +DockId=0x0000000F,0 [Window][###43] Pos=0,490 @@ -117,16 +117,16 @@ Collapsed=0 DockId=0x00000015,0 [Window][###111] -Pos=900,19 -Size=300,781 +Pos=984,412 +Size=216,388 Collapsed=0 DockId=0x0000001A,0 [Window][###96] Pos=984,19 -Size=216,781 +Size=216,391 Collapsed=0 -DockId=0x0000001D,0 +DockId=0x00000019,0 [Window][###127] Pos=984,365 @@ -134,38 +134,86 @@ Size=216,435 Collapsed=0 DockId=0x0000001E,0 -[Docking][Data] -DockSpace ID=0x7C6B3D9B Window=0xA87D555D Pos=0,19 Size=1200,781 Split=X - DockNode ID=0x0000001B Parent=0x7C6B3D9B SizeRef=982,781 Split=X - DockNode ID=0x00000019 Parent=0x0000001B SizeRef=898,781 Split=X - DockNode ID=0x00000017 Parent=0x00000019 SizeRef=898,781 Split=X - DockNode ID=0x00000009 Parent=0x00000017 SizeRef=270,581 Split=Y Selected=0x3BEDC6B0 - DockNode ID=0x0000000B Parent=0x00000009 SizeRef=196,474 Split=Y Selected=0x3BEDC6B0 - DockNode ID=0x0000000D Parent=0x0000000B SizeRef=196,99 Split=Y Selected=0x99D84869 - DockNode ID=0x00000012 Parent=0x0000000D SizeRef=196,423 Selected=0x0F59680E - DockNode ID=0x00000013 Parent=0x0000000D SizeRef=196,156 Split=Y Selected=0xB4AD3310 - DockNode ID=0x00000014 Parent=0x00000013 SizeRef=244,625 Split=Y Selected=0xB4AD3310 - DockNode ID=0x0000001F Parent=0x00000014 SizeRef=270,469 Selected=0xB4AD3310 - DockNode ID=0x00000020 Parent=0x00000014 SizeRef=270,154 Selected=0x0531B3D5 - DockNode ID=0x00000015 Parent=0x00000013 SizeRef=244,154 Selected=0x8773D56E - DockNode ID=0x0000000E Parent=0x0000000B SizeRef=196,373 Selected=0x3BEDC6B0 - DockNode ID=0x0000000C Parent=0x00000009 SizeRef=196,105 Selected=0x4F81AB74 - DockNode ID=0x0000000A Parent=0x00000017 SizeRef=710,581 Split=X - DockNode ID=0x00000003 Parent=0x0000000A SizeRef=299,581 Split=Y Selected=0x52849BCC - DockNode ID=0x00000005 Parent=0x00000003 SizeRef=299,473 Split=Y Selected=0x52849BCC - DockNode ID=0x00000007 Parent=0x00000005 SizeRef=299,86 Selected=0x52849BCC - DockNode ID=0x00000008 Parent=0x00000005 SizeRef=299,385 Selected=0xBD79B41E - DockNode ID=0x00000006 Parent=0x00000003 SizeRef=299,106 Selected=0x84DD78D1 - DockNode ID=0x00000004 Parent=0x0000000A SizeRef=499,581 Split=Y - DockNode ID=0x00000001 Parent=0x00000004 SizeRef=800,379 Split=Y Selected=0x7FF1E0B5 - DockNode ID=0x0000000F Parent=0x00000001 SizeRef=602,425 Split=Y Selected=0x38519A65 - DockNode ID=0x00000011 Parent=0x0000000F SizeRef=554,379 CentralNode=1 Selected=0x977476CD - DockNode ID=0x00000016 Parent=0x0000000F SizeRef=554,200 Selected=0x3A881EEF - DockNode ID=0x00000010 Parent=0x00000001 SizeRef=602,154 Selected=0x083320CE - DockNode ID=0x00000002 Parent=0x00000004 SizeRef=800,200 Selected=0x1834836D - DockNode ID=0x00000018 Parent=0x00000019 SizeRef=300,781 Selected=0x7E9438EA - DockNode ID=0x0000001A Parent=0x0000001B SizeRef=300,781 Selected=0x7E9438EA - DockNode ID=0x0000001C Parent=0x7C6B3D9B SizeRef=216,781 Split=Y Selected=0x714F2F7B - DockNode ID=0x0000001D Parent=0x0000001C SizeRef=216,344 Selected=0x714F2F7B - DockNode ID=0x0000001E Parent=0x0000001C SizeRef=216,435 Selected=0x7740BFE4 +[Window][###125] +Pos=984,600 +Size=216,200 +Collapsed=0 +DockId=0x00000022,0 + +[Window][###103] +Pos=984,19 +Size=216,579 +Collapsed=0 +DockId=0x00000025,0 + +[Window][###139] +Pos=984,600 +Size=216,200 +Collapsed=0 +DockId=0x00000026,0 + +[Window][###101] +Pos=984,19 +Size=216,609 +Collapsed=0 +DockId=0x00000029,0 + +[Window][###121] +Pos=984,630 +Size=216,170 +Collapsed=0 +DockId=0x0000002A,0 + +[Window][###129] +Pos=939,555 +Size=261,245 +Collapsed=0 +DockId=0x00000016,0 + +[Docking][Data] +DockSpace ID=0x7C6B3D9B Window=0xA87D555D Pos=0,19 Size=1200,781 Split=X + DockNode ID=0x0000002B Parent=0x7C6B3D9B SizeRef=937,781 Split=X + DockNode ID=0x00000027 Parent=0x0000002B SizeRef=982,781 Split=X + DockNode ID=0x00000023 Parent=0x00000027 SizeRef=982,781 Split=X + DockNode ID=0x0000001B Parent=0x00000023 SizeRef=982,781 Split=X + DockNode ID=0x00000017 Parent=0x0000001B SizeRef=898,781 Split=X + DockNode ID=0x00000009 Parent=0x00000017 SizeRef=270,581 Split=Y Selected=0x3BEDC6B0 + DockNode ID=0x0000000B Parent=0x00000009 SizeRef=196,474 Split=Y Selected=0x3BEDC6B0 + DockNode ID=0x0000000D Parent=0x0000000B SizeRef=196,99 Split=Y Selected=0x99D84869 + DockNode ID=0x00000012 Parent=0x0000000D SizeRef=196,423 Selected=0x0F59680E + DockNode ID=0x00000013 Parent=0x0000000D SizeRef=196,156 Split=Y Selected=0xB4AD3310 + DockNode ID=0x00000014 Parent=0x00000013 SizeRef=244,625 Split=Y Selected=0xB4AD3310 + DockNode ID=0x0000001F Parent=0x00000014 SizeRef=270,469 Selected=0xB4AD3310 + DockNode ID=0x00000020 Parent=0x00000014 SizeRef=270,154 Selected=0x0531B3D5 + DockNode ID=0x00000015 Parent=0x00000013 SizeRef=244,154 Selected=0x8773D56E + DockNode ID=0x0000000E Parent=0x0000000B SizeRef=196,373 Selected=0x3BEDC6B0 + DockNode ID=0x0000000C Parent=0x00000009 SizeRef=196,105 Selected=0x4F81AB74 + DockNode ID=0x0000000A Parent=0x00000017 SizeRef=665,581 Split=X + DockNode ID=0x00000003 Parent=0x0000000A SizeRef=299,581 Split=Y Selected=0x52849BCC + DockNode ID=0x00000005 Parent=0x00000003 SizeRef=299,473 Split=Y Selected=0x52849BCC + DockNode ID=0x00000007 Parent=0x00000005 SizeRef=299,86 Selected=0x52849BCC + DockNode ID=0x00000008 Parent=0x00000005 SizeRef=299,385 Selected=0xBD79B41E + DockNode ID=0x00000006 Parent=0x00000003 SizeRef=299,106 Selected=0x84DD78D1 + DockNode ID=0x00000004 Parent=0x0000000A SizeRef=499,581 Split=Y + DockNode ID=0x00000001 Parent=0x00000004 SizeRef=800,379 Split=Y Selected=0x7FF1E0B5 + DockNode ID=0x0000000F Parent=0x00000001 SizeRef=602,425 CentralNode=1 Selected=0x977476CD + DockNode ID=0x00000010 Parent=0x00000001 SizeRef=602,154 Selected=0x083320CE + DockNode ID=0x00000002 Parent=0x00000004 SizeRef=800,200 Selected=0x1834836D + DockNode ID=0x00000018 Parent=0x0000001B SizeRef=300,781 Selected=0x7E9438EA + DockNode ID=0x0000001C Parent=0x00000023 SizeRef=216,781 Split=Y Selected=0x714F2F7B + DockNode ID=0x0000001D Parent=0x0000001C SizeRef=216,344 Split=Y Selected=0x714F2F7B + DockNode ID=0x00000021 Parent=0x0000001D SizeRef=216,579 Split=Y Selected=0x714F2F7B + DockNode ID=0x00000019 Parent=0x00000021 SizeRef=216,391 Selected=0x714F2F7B + DockNode ID=0x0000001A Parent=0x00000021 SizeRef=216,388 Selected=0x7E9438EA + DockNode ID=0x00000022 Parent=0x0000001D SizeRef=216,200 Selected=0x0D80EC84 + DockNode ID=0x0000001E Parent=0x0000001C SizeRef=216,435 Selected=0x7740BFE4 + DockNode ID=0x00000024 Parent=0x00000027 SizeRef=216,781 Split=Y Selected=0xCF08B82F + DockNode ID=0x00000025 Parent=0x00000024 SizeRef=216,579 Selected=0xCF08B82F + DockNode ID=0x00000026 Parent=0x00000024 SizeRef=216,200 Selected=0x032CD220 + DockNode ID=0x00000028 Parent=0x0000002B SizeRef=216,781 Split=Y Selected=0xB5C8EB4F + DockNode ID=0x00000029 Parent=0x00000028 SizeRef=216,609 Selected=0xB5C8EB4F + DockNode ID=0x0000002A Parent=0x00000028 SizeRef=216,170 Selected=0xF8004A44 + DockNode ID=0x0000002C Parent=0x7C6B3D9B SizeRef=261,781 Split=Y Selected=0xC8700185 + DockNode ID=0x00000011 Parent=0x0000002C SizeRef=142,534 Selected=0x3A881EEF + DockNode ID=0x00000016 Parent=0x0000002C SizeRef=142,245 Selected=0xC8700185 diff --git a/negstation_widgets.json b/negstation_widgets.json index 2290fa7..4403a32 100644 --- a/negstation_widgets.json +++ b/negstation_widgets.json @@ -26,5 +26,9 @@ { "widget_type": "HistogramWidget", "config": {} + }, + { + "widget_type": "ExportStage", + "config": {} } ] \ No newline at end of file