From 38593fc2c51de4a77796f82e264321da8c7e0513 Mon Sep 17 00:00:00 2001 From: Joppe Blondel Date: Sat, 2 Aug 2025 15:59:16 +0200 Subject: [PATCH] RAW loading and processing widget added --- negstation/widgets/open_image_widget.py | 9 +- negstation/widgets/open_raw_widget.py | 205 ++++++++++++++++++++++ negstation/widgets/stage_viewer_widget.py | 4 +- negstation_layout.ini | 90 +++++++--- negstation_widgets.json | 4 + 5 files changed, 281 insertions(+), 31 deletions(-) create mode 100644 negstation/widgets/open_raw_widget.py diff --git a/negstation/widgets/open_image_widget.py b/negstation/widgets/open_image_widget.py index accbcf7..bef294d 100644 --- a/negstation/widgets/open_image_widget.py +++ b/negstation/widgets/open_image_widget.py @@ -25,6 +25,9 @@ class OpenImageWidget(PipelineStageWidget): height=300, width=400, ): + dpg.add_file_extension( + "Image files {.png,.jpg,.jpeg,.bmp .gif,.tif,.tiff}", + ) dpg.add_file_extension(".*") dpg.add_button(label="Open File...", callback=self._on_open_file) @@ -33,7 +36,8 @@ class OpenImageWidget(PipelineStageWidget): def _on_file_selected(self, sender, app_data): selection = ( - f"{app_data['current_path']}/{list(app_data['selections'].keys())[0]}" + f"{app_data['current_path'] + }/{list(app_data['selections'].keys())[0]}" if isinstance(app_data, dict) else None ) @@ -42,7 +46,8 @@ class OpenImageWidget(PipelineStageWidget): self.logger.info(f"Selected file '{selection}'") try: img = Image.open(selection).convert("RGBA") - arr = np.asarray(img).astype(np.float32) / 255.0 # normalize to [0,1] + arr = 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) except Exception as e: diff --git a/negstation/widgets/open_raw_widget.py b/negstation/widgets/open_raw_widget.py new file mode 100644 index 0000000..173290e --- /dev/null +++ b/negstation/widgets/open_raw_widget.py @@ -0,0 +1,205 @@ +import dearpygui.dearpygui as dpg +import rawpy +import numpy as np + +from .pipeline_stage_widget import PipelineStageWidget + + +class OpenRawWidget(PipelineStageWidget): + name = "Open RAW File" + register = True + has_pipeline_in = False + has_pipeline_out = True + + def __init__(self, manager, logger): + super().__init__(manager, logger, default_stage_out="opened_raw") + self.dialog_tag = dpg.generate_uuid() + self.output_tag = dpg.generate_uuid() + self.config_group = dpg.generate_uuid() + self.busy_group = dpg.generate_uuid() + self.raw_path = None + self.config = { + # Demosaic algorithm + "demosaic_algorithm": rawpy.DemosaicAlgorithm.AHD, + # Output color space + "output_color": rawpy.ColorSpace.sRGB, + # Bits per sample + "output_bps": 16, + # White balance + "use_camera_wb": True, + "use_auto_wb": False, + "user_wb": (1.0, 1.0, 1.0, 1.0), + # Brightness/exposure + "bright": 1.0, + "no_auto_bright": False, + # Gamma correction (you’ll pass (1.0, config["gamma"]) down) + "gamma": 1.0, + # Size & quality toggles + "half_size": False, + "four_color_rgb": False, + } + + def create_pipeline_stage_content(self): + with dpg.file_dialog( + directory_selector=False, + show=False, + callback=self._on_file_selected, + tag=self.dialog_tag, + height=300, + width=400, + ): + dpg.add_file_extension( + "RAW files {.nef,.cr2}", + ) + dpg.add_file_extension(".*") + + with dpg.group(tag=self.config_group): + with dpg.group(horizontal=True): + dpg.add_button(label="Open File...", + callback=self._on_open_file) + dpg.add_button(label="Reprocess", + callback=self._process_and_publish) + + dpg.add_combo( + label="Demosaic", + items=[alg.name for alg in rawpy.DemosaicAlgorithm], + default_value=rawpy.DemosaicAlgorithm.AHD.name, + callback=lambda s, a, u: self.config.__setitem__( + "demosaic_algorithm", rawpy.DemosaicAlgorithm[a]) + ) + dpg.add_combo( + label="Color Space", + items=[cs.name for cs in rawpy.ColorSpace], + default_value=rawpy.ColorSpace.sRGB.name, + callback=lambda s, a, u: self.config.__setitem__( + "output_color", rawpy.ColorSpace[a]) + ) + dpg.add_combo( + label="Output Bits", + items=["8", "16"], + default_value="16", + callback=lambda s, a, u: self.config.__setitem__( + "output_bps", int(a)) + ) + dpg.add_checkbox( + label="Use Camera WB", + default_value=True, + callback=lambda s, a, u: self.config.__setitem__( + "use_camera_wb", a) + ) + dpg.add_checkbox( + label="Auto WB", + default_value=False, + callback=lambda s, a, u: self.config.__setitem__( + "use_auto_wb", a) + ) + dpg.add_slider_float( + label="Manual WB R Gain", + default_value=1.0, min_value=0.1, max_value=4.0, + callback=lambda s, a, u: self.config.__setitem__( + "user_wb", (a, self.config["user_wb"][1], self.config["user_wb"][2], self.config["user_wb"][3])) + ) + dpg.add_slider_float( + label="Manual WB G Gain", + default_value=1.0, min_value=0.1, max_value=4.0, + callback=lambda s, a, u: self.config.__setitem__( + "user_wb", (self.config["user_wb"][0], a, a, self.config["user_wb"][3])) + ) + dpg.add_slider_float( + label="Manual WB B Gain", + default_value=1.0, min_value=0.1, max_value=4.0, + callback=lambda s, a, u: self.config.__setitem__( + "user_wb", (self.config["user_wb"][0], self.config["user_wb"][1], self.config["user_wb"][2], a)) + ) + dpg.add_slider_float( + label="Bright", + default_value=1.0, min_value=0.1, max_value=4.0, + callback=lambda s, a, u: self.config.__setitem__("bright", a) + ) + dpg.add_checkbox( + label="No Auto Bright", + default_value=False, + callback=lambda s, a, u: self.config.__setitem__( + "no_auto_bright", a) + ) + dpg.add_slider_float( + label="Gamma", + default_value=1.0, min_value=0.1, max_value=3.0, + callback=lambda s, a, u: self.config.__setitem__("gamma", a) + ) + dpg.add_checkbox( + label="Half-size", + default_value=False, + callback=lambda s, a, u: self.config.__setitem__( + "half_size", a) + ) + dpg.add_checkbox( + label="4-color RGB", + default_value=False, + callback=lambda s, a, u: self.config.__setitem__( + "four_color_rgb", a) + ) + + with dpg.group(tag=self.busy_group): + dpg.add_text("Processing...") + + def _on_open_file(self): + dpg.configure_item(self.dialog_tag, show=True) + + def _on_file_selected(self, sender, app_data): + selection = ( + f"{app_data['current_path'] + }/{list(app_data['selections'].keys())[0]}" + if isinstance(app_data, dict) + else None + ) + if not selection: + return + self.raw_path = selection + self.logger.info(f"Selected file '{selection}'") + self._process_and_publish() + + def _process_and_publish(self): + if self.raw_path is None: + return + self.logger.info("Processing RAW image") + + dpg.configure_item(self.config_group, show=False) + dpg.configure_item(self.busy_group, show=True) + + with rawpy.imread(self.raw_path) as raw: + # Prepare postprocess kwargs from config + postprocess_args = { + 'demosaic_algorithm': self.config["demosaic_algorithm"], + 'output_color': self.config["output_color"], + 'output_bps': self.config["output_bps"], + 'bright': self.config["bright"], + 'no_auto_bright': self.config["no_auto_bright"], + 'gamma': (1.0, self.config["gamma"]), + 'half_size': self.config["half_size"], + 'four_color_rgb': self.config["four_color_rgb"], + } + + if self.config["use_camera_wb"]: + postprocess_args['use_camera_wb'] = True + elif self.config["use_auto_wb"]: + postprocess_args['use_auto_wb'] = True + else: + postprocess_args['user_wb'] = self.config["user_wb"] + + # Postprocess into RGB + rgb = raw.postprocess(**postprocess_args) + + # Normalize to float32 in 0.0-1.0 range depending on output_bps + max_val = (2 ** self.config["output_bps"]) - 1 + rgb_float = rgb.astype(np.float32) / max_val + + # Add alpha channel (fully opaque) + h, w, _ = rgb_float.shape + alpha = np.ones((h, w, 1), dtype=np.float32) + + rgba = np.concatenate([rgb_float, alpha], axis=2) + + self.manager.pipeline.publish(self.pipeline_stage_out_id, rgba) + dpg.configure_item(self.config_group, show=True) + dpg.configure_item(self.busy_group, show=False) diff --git a/negstation/widgets/stage_viewer_widget.py b/negstation/widgets/stage_viewer_widget.py index 16f3d2c..ad1138b 100644 --- a/negstation/widgets/stage_viewer_widget.py +++ b/negstation/widgets/stage_viewer_widget.py @@ -28,6 +28,8 @@ class PipelineStageViewer(PipelineStageWidget): def on_pipeline_data(self, img): # Resize if needed + if img is None: + return h, w, _ = img.shape max_dim = 500 scale = min(1.0, max_dim / w, max_dim / h) @@ -66,7 +68,7 @@ class PipelineStageViewer(PipelineStageWidget): avail_w = win_w avail_h = win_h - scale = min(avail_w / w, avail_h / h) #, 1.0) + scale = min(avail_w / w, avail_h / h) # , 1.0) disp_w = int(w * scale) disp_h = int(h * scale) diff --git a/negstation_layout.ini b/negstation_layout.ini index da9a5ae..bde30e1 100644 --- a/negstation_layout.ini +++ b/negstation_layout.ini @@ -51,9 +51,9 @@ DockId=0x00000008,0 [Window][###23] Pos=0,19 -Size=250,459 +Size=305,85 Collapsed=0 -DockId=0x00000017,0 +DockId=0x00000021,0 [Window][###32] Pos=0,376 @@ -108,30 +108,64 @@ Pos=237,120 Size=300,200 Collapsed=0 -[Docking][Data] -DockSpace ID=0x7C6B3D9B Window=0xA87D555D Pos=0,19 Size=800,581 Split=X - DockNode ID=0x00000009 Parent=0x7C6B3D9B SizeRef=250,581 Split=Y Selected=0xD36850C8 - DockNode ID=0x0000000B Parent=0x00000009 SizeRef=147,355 Split=Y Selected=0xD36850C8 - DockNode ID=0x0000000D Parent=0x0000000B SizeRef=250,304 Split=Y Selected=0xD36850C8 - DockNode ID=0x0000000F Parent=0x0000000D SizeRef=250,459 Split=Y Selected=0xD36850C8 - DockNode ID=0x00000011 Parent=0x0000000F SizeRef=250,379 Split=Y Selected=0xD36850C8 - DockNode ID=0x00000013 Parent=0x00000011 SizeRef=250,379 Split=Y Selected=0xD36850C8 - DockNode ID=0x00000015 Parent=0x00000013 SizeRef=250,379 Split=Y Selected=0xD36850C8 - DockNode ID=0x00000017 Parent=0x00000015 SizeRef=250,379 Selected=0xD36850C8 - DockNode ID=0x00000018 Parent=0x00000015 SizeRef=250,200 Selected=0x3BEDC6B0 - DockNode ID=0x00000016 Parent=0x00000013 SizeRef=250,200 Selected=0xC7B9E77E - DockNode ID=0x00000014 Parent=0x00000011 SizeRef=250,200 Selected=0x3BEDC6B0 - DockNode ID=0x00000012 Parent=0x0000000F SizeRef=250,200 Selected=0x83A5C17B - DockNode ID=0x00000010 Parent=0x0000000D SizeRef=250,120 Selected=0xAA145F7D - DockNode ID=0x0000000E Parent=0x0000000B SizeRef=250,275 Selected=0x1834836D - DockNode ID=0x0000000C Parent=0x00000009 SizeRef=147,224 Selected=0x2554AADD - DockNode ID=0x0000000A Parent=0x7C6B3D9B SizeRef=548,581 Split=X - DockNode ID=0x00000005 Parent=0x0000000A SizeRef=288,581 Split=Y Selected=0xEE087978 - DockNode ID=0x00000007 Parent=0x00000005 SizeRef=147,427 Selected=0xEE087978 - DockNode ID=0x00000008 Parent=0x00000005 SizeRef=147,152 Selected=0x62F4D00D - DockNode ID=0x00000006 Parent=0x0000000A SizeRef=510,581 Split=X - DockNode ID=0x00000001 Parent=0x00000006 SizeRef=299,581 Split=Y Selected=0xA4B861D9 - DockNode ID=0x00000003 Parent=0x00000001 SizeRef=299,379 Selected=0xA4B861D9 - DockNode ID=0x00000004 Parent=0x00000001 SizeRef=299,200 Selected=0xEDB425AD - DockNode ID=0x00000002 Parent=0x00000006 SizeRef=499,581 CentralNode=1 Selected=0x38519A65 +[Window][###43] +Pos=307,19 +Size=493,581 +Collapsed=0 +DockId=0x00000002,0 + +[Window][###35] +Pos=0,500 +Size=305,100 +Collapsed=0 +DockId=0x0000001A,0 + +[Window][###60] +Pos=0,192 +Size=250,306 +Collapsed=0 +DockId=0x00000020,0 + +[Window][###51] +Pos=0,106 +Size=305,392 +Collapsed=0 +DockId=0x00000022,0 + +[Docking][Data] +DockSpace ID=0x7C6B3D9B Window=0xA87D555D Pos=0,19 Size=800,581 Split=X + DockNode ID=0x00000009 Parent=0x7C6B3D9B SizeRef=305,581 Split=Y Selected=0xD36850C8 + DockNode ID=0x0000000B Parent=0x00000009 SizeRef=147,355 Split=Y Selected=0xD36850C8 + DockNode ID=0x0000000D Parent=0x0000000B SizeRef=250,304 Split=Y Selected=0xD36850C8 + DockNode ID=0x0000000F Parent=0x0000000D SizeRef=250,459 Split=Y Selected=0xD36850C8 + DockNode ID=0x00000011 Parent=0x0000000F SizeRef=250,379 Split=Y Selected=0xD36850C8 + DockNode ID=0x00000013 Parent=0x00000011 SizeRef=250,379 Split=Y Selected=0xD36850C8 + DockNode ID=0x00000015 Parent=0x00000013 SizeRef=250,379 Split=Y Selected=0xD36850C8 + DockNode ID=0x00000017 Parent=0x00000015 SizeRef=250,379 Split=Y Selected=0xD36850C8 + DockNode ID=0x00000019 Parent=0x00000017 SizeRef=250,479 Split=Y Selected=0xD36850C8 + DockNode ID=0x0000001B Parent=0x00000019 SizeRef=250,161 Split=Y Selected=0xD36850C8 + DockNode ID=0x0000001D Parent=0x0000001B SizeRef=250,192 Split=Y Selected=0xD36850C8 + DockNode ID=0x0000001F Parent=0x0000001D SizeRef=250,171 Split=Y Selected=0xD36850C8 + DockNode ID=0x00000021 Parent=0x0000001F SizeRef=250,85 Selected=0xD36850C8 + DockNode ID=0x00000022 Parent=0x0000001F SizeRef=250,392 Selected=0xB4AD3310 + DockNode ID=0x00000020 Parent=0x0000001D SizeRef=250,306 Selected=0x0F59680E + DockNode ID=0x0000001E Parent=0x0000001B SizeRef=250,285 Selected=0x0F59680E + DockNode ID=0x0000001C Parent=0x00000019 SizeRef=250,316 Selected=0x0F59680E + DockNode ID=0x0000001A Parent=0x00000017 SizeRef=250,100 Selected=0x977476CD + DockNode ID=0x00000018 Parent=0x00000015 SizeRef=250,200 Selected=0x3BEDC6B0 + DockNode ID=0x00000016 Parent=0x00000013 SizeRef=250,200 Selected=0xC7B9E77E + DockNode ID=0x00000014 Parent=0x00000011 SizeRef=250,200 Selected=0x3BEDC6B0 + DockNode ID=0x00000012 Parent=0x0000000F SizeRef=250,200 Selected=0x83A5C17B + DockNode ID=0x00000010 Parent=0x0000000D SizeRef=250,120 Selected=0xAA145F7D + DockNode ID=0x0000000E Parent=0x0000000B SizeRef=250,275 Selected=0x1834836D + DockNode ID=0x0000000C Parent=0x00000009 SizeRef=147,224 Selected=0x2554AADD + DockNode ID=0x0000000A Parent=0x7C6B3D9B SizeRef=493,581 Split=X + DockNode ID=0x00000005 Parent=0x0000000A SizeRef=288,581 Split=Y Selected=0xEE087978 + DockNode ID=0x00000007 Parent=0x00000005 SizeRef=147,427 Selected=0xEE087978 + DockNode ID=0x00000008 Parent=0x00000005 SizeRef=147,152 Selected=0x62F4D00D + DockNode ID=0x00000006 Parent=0x0000000A SizeRef=510,581 Split=X + DockNode ID=0x00000001 Parent=0x00000006 SizeRef=299,581 Split=Y Selected=0xA4B861D9 + DockNode ID=0x00000003 Parent=0x00000001 SizeRef=299,379 Selected=0xA4B861D9 + DockNode ID=0x00000004 Parent=0x00000001 SizeRef=299,200 Selected=0xEDB425AD + DockNode ID=0x00000002 Parent=0x00000006 SizeRef=499,581 CentralNode=1 Selected=0x0531B3D5 diff --git a/negstation_widgets.json b/negstation_widgets.json index 2a0b909..8c7b7d7 100644 --- a/negstation_widgets.json +++ b/negstation_widgets.json @@ -10,5 +10,9 @@ { "widget_type": "PipelineStageViewer", "config": {} + }, + { + "widget_type": "OpenRawWidget", + "config": {} } ] \ No newline at end of file