Basic RAW processing added
This commit is contained in:
@ -5,6 +5,15 @@ class GlobalState:
|
||||
def __init__(self):
|
||||
self.listeners = defaultdict(list)
|
||||
|
||||
self.raw_params = {
|
||||
"use_auto_wb": False,
|
||||
"use_camera_wb": True,
|
||||
"no_auto_bright": True,
|
||||
"output_bps": 16,
|
||||
"gamma": (2.222, 4.5), # Default sRGB gamma
|
||||
}
|
||||
self.raw_image_data = None
|
||||
|
||||
def subscribe(self, event_name: str, callback):
|
||||
"""Register a function to be called when an event is dispatched."""
|
||||
logging.info(f"Subscribing '{callback.__qualname__}' to event '{event_name}'")
|
||||
@ -12,7 +21,7 @@ class GlobalState:
|
||||
|
||||
def dispatch(self, event_name: str, *args, **kwargs):
|
||||
"""Call all registered callbacks for a given event."""
|
||||
logging.info(f"Dispatching event '{event_name}' with data: {kwargs}")
|
||||
logging.debug(f"Dispatching event '{event_name}' with data: {kwargs}")
|
||||
if event_name in self.listeners:
|
||||
for callback in self.listeners[event_name]:
|
||||
try:
|
||||
|
@ -21,10 +21,10 @@ Size=400,400
|
||||
Collapsed=0
|
||||
|
||||
[Window][###31]
|
||||
Pos=0,19
|
||||
Size=344,701
|
||||
Pos=373,547
|
||||
Size=907,173
|
||||
Collapsed=0
|
||||
DockId=0x0000000D,0
|
||||
DockId=0x0000000E,0
|
||||
|
||||
[Window][###29]
|
||||
Pos=346,572
|
||||
@ -36,13 +36,13 @@ DockId=0x00000010,0
|
||||
Pos=310,19
|
||||
Size=970,532
|
||||
Collapsed=0
|
||||
DockId=0x0000000B,0
|
||||
DockId=0x0000000D,0
|
||||
|
||||
[Window][###49]
|
||||
Pos=346,19
|
||||
Size=934,551
|
||||
Collapsed=0
|
||||
DockId=0x0000000B,0
|
||||
DockId=0x0000000D,0
|
||||
|
||||
[Window][###32]
|
||||
Pos=0,19
|
||||
@ -51,35 +51,79 @@ Collapsed=0
|
||||
DockId=0x00000011,0
|
||||
|
||||
[Window][###51]
|
||||
Pos=310,19
|
||||
Size=970,532
|
||||
Pos=290,19
|
||||
Size=990,550
|
||||
Collapsed=0
|
||||
DockId=0x0000000B,0
|
||||
DockId=0x0000000D,0
|
||||
|
||||
[Window][###73]
|
||||
Pos=310,19
|
||||
Size=970,532
|
||||
Collapsed=0
|
||||
DockId=0x0000000B,0
|
||||
DockId=0x0000000D,0
|
||||
|
||||
[Window][###33]
|
||||
Pos=0,19
|
||||
Size=371,701
|
||||
Collapsed=0
|
||||
DockId=0x00000019,0
|
||||
|
||||
[Window][###55]
|
||||
Pos=0,497
|
||||
Size=288,223
|
||||
Collapsed=0
|
||||
DockId=0x00000016,0
|
||||
|
||||
[Window][###78]
|
||||
Pos=0,464
|
||||
Size=288,256
|
||||
Collapsed=0
|
||||
DockId=0x00000018,0
|
||||
|
||||
[Window][###57]
|
||||
Pos=0,674
|
||||
Size=371,355
|
||||
Collapsed=0
|
||||
DockId=0x0000001A,0
|
||||
|
||||
[Window][###53]
|
||||
Pos=373,19
|
||||
Size=907,526
|
||||
Collapsed=0
|
||||
DockId=0x0000000D,0
|
||||
|
||||
[Window][###59]
|
||||
Pos=0,19
|
||||
Size=371,701
|
||||
Collapsed=0
|
||||
DockId=0x00000019,1
|
||||
|
||||
[Docking][Data]
|
||||
DockSpace ID=0x7C6B3D9B Window=0xA87D555D Pos=0,19 Size=1280,701 Split=X
|
||||
DockNode ID=0x00000011 Parent=0x7C6B3D9B SizeRef=308,701 Selected=0x2554AADD
|
||||
DockNode ID=0x00000012 Parent=0x7C6B3D9B SizeRef=970,701 Split=X
|
||||
DockNode ID=0x0000000D Parent=0x00000012 SizeRef=344,701 Selected=0x62F4D00D
|
||||
DockNode ID=0x0000000E Parent=0x00000012 SizeRef=791,701 Split=X
|
||||
DockNode ID=0x00000009 Parent=0x0000000E SizeRef=319,701 Selected=0x5F94F9BD
|
||||
DockNode ID=0x0000000A Parent=0x0000000E SizeRef=959,701 Split=X
|
||||
DockNode ID=0x00000007 Parent=0x0000000A SizeRef=340,701 Selected=0x5F94F9BD
|
||||
DockNode ID=0x00000008 Parent=0x0000000A SizeRef=938,701 Split=X
|
||||
DockNode ID=0x00000005 Parent=0x00000008 SizeRef=300,701 Selected=0x5F94F9BD
|
||||
DockNode ID=0x00000006 Parent=0x00000008 SizeRef=978,701 Split=X
|
||||
DockNode ID=0x00000003 Parent=0x00000006 SizeRef=318,701 Selected=0x5F94F9BD
|
||||
DockNode ID=0x00000004 Parent=0x00000006 SizeRef=960,701 Split=Y
|
||||
DockNode ID=0x00000001 Parent=0x00000004 SizeRef=979,496 Split=Y
|
||||
DockNode ID=0x0000000F Parent=0x00000001 SizeRef=1123,551 Split=Y Selected=0x89CD1AA0
|
||||
DockNode ID=0x0000000B Parent=0x0000000F SizeRef=1280,532 CentralNode=1 Selected=0x89CD1AA0
|
||||
DockNode ID=0x0000000C Parent=0x0000000F SizeRef=1280,167 Selected=0x5F94F9BD
|
||||
DockNode ID=0x00000010 Parent=0x00000001 SizeRef=1123,148 Selected=0x99D84869
|
||||
DockNode ID=0x00000002 Parent=0x00000004 SizeRef=979,203 Selected=0xA4B861D9
|
||||
DockSpace ID=0x7C6B3D9B Window=0xA87D555D Pos=0,19 Size=1280,701 Split=X
|
||||
DockNode ID=0x00000013 Parent=0x7C6B3D9B SizeRef=371,701 Split=Y Selected=0x1834836D
|
||||
DockNode ID=0x00000015 Parent=0x00000013 SizeRef=288,476 Split=Y Selected=0x1834836D
|
||||
DockNode ID=0x00000017 Parent=0x00000015 SizeRef=288,443 Split=Y Selected=0x1834836D
|
||||
DockNode ID=0x00000019 Parent=0x00000017 SizeRef=288,453 Selected=0x1834836D
|
||||
DockNode ID=0x0000001A Parent=0x00000017 SizeRef=288,246 Selected=0x3BEDC6B0
|
||||
DockNode ID=0x00000018 Parent=0x00000015 SizeRef=288,256 Selected=0xF475F06A
|
||||
DockNode ID=0x00000016 Parent=0x00000013 SizeRef=288,223 Selected=0x412D95D0
|
||||
DockNode ID=0x00000014 Parent=0x7C6B3D9B SizeRef=907,701 Split=X
|
||||
DockNode ID=0x00000011 Parent=0x00000014 SizeRef=308,701 Selected=0x2554AADD
|
||||
DockNode ID=0x00000012 Parent=0x00000014 SizeRef=970,701 Split=X
|
||||
DockNode ID=0x00000009 Parent=0x00000012 SizeRef=319,701 Selected=0x5F94F9BD
|
||||
DockNode ID=0x0000000A Parent=0x00000012 SizeRef=959,701 Split=X
|
||||
DockNode ID=0x00000007 Parent=0x0000000A SizeRef=340,701 Selected=0x5F94F9BD
|
||||
DockNode ID=0x00000008 Parent=0x0000000A SizeRef=938,701 Split=X
|
||||
DockNode ID=0x00000005 Parent=0x00000008 SizeRef=300,701 Selected=0x5F94F9BD
|
||||
DockNode ID=0x00000006 Parent=0x00000008 SizeRef=978,701 Split=X
|
||||
DockNode ID=0x00000003 Parent=0x00000006 SizeRef=318,701 Selected=0x5F94F9BD
|
||||
DockNode ID=0x00000004 Parent=0x00000006 SizeRef=960,701 Split=Y
|
||||
DockNode ID=0x00000001 Parent=0x00000004 SizeRef=979,496 Split=Y
|
||||
DockNode ID=0x0000000F Parent=0x00000001 SizeRef=1123,551 Split=Y Selected=0x89CD1AA0
|
||||
DockNode ID=0x0000000B Parent=0x0000000F SizeRef=1280,532 Split=Y Selected=0xB4AD3310
|
||||
DockNode ID=0x0000000D Parent=0x0000000B SizeRef=1088,526 CentralNode=1 Selected=0xCE6D6070
|
||||
DockNode ID=0x0000000E Parent=0x0000000B SizeRef=1088,173 Selected=0x62F4D00D
|
||||
DockNode ID=0x0000000C Parent=0x0000000F SizeRef=1280,167 Selected=0x5F94F9BD
|
||||
DockNode ID=0x00000010 Parent=0x00000001 SizeRef=1123,148 Selected=0x99D84869
|
||||
DockNode ID=0x00000002 Parent=0x00000004 SizeRef=979,203 Selected=0xA4B861D9
|
||||
|
||||
|
@ -16,5 +16,11 @@
|
||||
"config": {
|
||||
"label": "ImageViewerWidget"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widget_type": "RawSettingsWidget",
|
||||
"config": {
|
||||
"label": "RawSettingsWidget"
|
||||
}
|
||||
}
|
||||
]
|
60
raw_processor.py
Normal file
60
raw_processor.py
Normal file
@ -0,0 +1,60 @@
|
||||
import rawpy
|
||||
import numpy as np
|
||||
import queue
|
||||
import threading
|
||||
import logging
|
||||
import dearpygui.dearpygui as dpg
|
||||
|
||||
class RawProcessor:
|
||||
"""
|
||||
A background service that listens for new RAW files, processes them,
|
||||
and dispatches the resulting RGB data.
|
||||
"""
|
||||
def __init__(self, global_state):
|
||||
self.global_state = global_state
|
||||
self.work_queue = queue.Queue()
|
||||
|
||||
# Subscribe to the event from the camera widget
|
||||
self.global_state.subscribe("NEW_IMAGE_CAPTURED", self.add_to_queue)
|
||||
|
||||
# Start the processor's own background thread
|
||||
self.worker_thread = threading.Thread(target=self._process_worker, daemon=True)
|
||||
self.worker_thread.start()
|
||||
logging.info("RAW Processor thread started.")
|
||||
|
||||
def add_to_queue(self, image_path: str):
|
||||
"""Adds a new RAW file path to the processing queue."""
|
||||
if image_path.lower().endswith(('.cr2')):
|
||||
logging.info(f"RAW Processor: Queued {image_path} for processing.")
|
||||
self.work_queue.put(image_path)
|
||||
else:
|
||||
# Not a supported raw file, hope for the best the viewer supports it
|
||||
try:
|
||||
width, height, channels, data = dpg.load_image(image_path)
|
||||
rgba_float32 = np.array(data, dtype=np.float32)
|
||||
rgba_float32 = rgba_float32.reshape(height, width, 4)
|
||||
self.global_state.raw_image_data = rgba_float32.copy()
|
||||
self.global_state.dispatch("PROCESSED_IMAGE_READY", image_data=rgba_float32.copy())
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to load standard image {image_path}: {e}", exc_info=True)
|
||||
|
||||
def _process_worker(self):
|
||||
"""The background thread that performs RAW processing and data conversion."""
|
||||
while True:
|
||||
raw_path = self.work_queue.get()
|
||||
try:
|
||||
logging.info(f"Processing {raw_path}...")
|
||||
with rawpy.imread(raw_path) as raw:
|
||||
rgb_uint8_potentially_corrupt = raw.postprocess(**self.global_state.raw_params)
|
||||
rgb_uint8 = rgb_uint8_potentially_corrupt.copy()
|
||||
logging.info("Defensive copy complete. Starting conversion...")
|
||||
rgb_float32 = (rgb_uint8 / pow(2,self.global_state.raw_params['output_bps'])).astype(np.float32)
|
||||
alpha_channel = np.ones((rgb_float32.shape[0], rgb_float32.shape[1], 1), dtype=np.float32)
|
||||
rgba_float32_data = np.concatenate((rgb_float32, alpha_channel), axis=2)
|
||||
logging.info(f"Processing and conversion complete for {raw_path}.")
|
||||
self.global_state.raw_image_data = rgba_float32_data.copy()
|
||||
self.global_state.dispatch("PROCESSED_IMAGE_READY", image_data=rgba_float32_data.copy())
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to process RAW file {raw_path}: {e}", exc_info=True)
|
8
ui.py
8
ui.py
@ -7,6 +7,7 @@ import inspect
|
||||
from collections import deque
|
||||
|
||||
import global_state
|
||||
import raw_processor
|
||||
from widgets.base_widget import BaseWidget
|
||||
|
||||
class DpgLogHandler(logging.Handler):
|
||||
@ -33,6 +34,9 @@ class LayoutManager:
|
||||
self.updating_widgets = []
|
||||
self.global_state = global_state.GlobalState()
|
||||
|
||||
self.texture_registry = dpg.add_texture_registry()
|
||||
self.raw_processor = raw_processor.RawProcessor(self.global_state)
|
||||
|
||||
def discover_and_register_widgets(self, directory="widgets"):
|
||||
"""Dynamically discovers and registers widgets from a given directory."""
|
||||
logging.info(f"Discovering widgets in '{directory}' directory...")
|
||||
@ -102,8 +106,6 @@ class LayoutManager:
|
||||
layout_manager = LayoutManager()
|
||||
layout_manager.discover_and_register_widgets()
|
||||
|
||||
layout_manager.texture_registry = dpg.add_texture_registry()
|
||||
|
||||
with dpg.viewport_menu_bar():
|
||||
with dpg.menu(label="File"):
|
||||
dpg.add_menu_item(label="Save Layout", callback=layout_manager.save_layout)
|
||||
@ -127,4 +129,4 @@ class LayoutManager:
|
||||
layout_manager.update_all_widgets()
|
||||
dpg.render_dearpygui_frame()
|
||||
|
||||
dpg.destroy_context()
|
||||
dpg.destroy_context()
|
||||
|
@ -2,75 +2,78 @@
|
||||
import dearpygui.dearpygui as dpg
|
||||
from .base_widget import BaseWidget
|
||||
import logging
|
||||
import numpy as np
|
||||
|
||||
class ImageViewerWidget(BaseWidget):
|
||||
"""
|
||||
Displays a zoomable image inside a plot. This definitive version implements
|
||||
programmatic pan and zoom by handling mouse events manually and repeatedly
|
||||
calling set_axis_limits, as this is the only reliable method to achieve
|
||||
both an initial fit and subsequent user interaction.
|
||||
Displays a zoomable image inside a plot. This definitive version uses a
|
||||
"reuse and reconfigure" pattern, creating DPG items only once and updating
|
||||
them on subsequent loads to ensure stability and avoid segmentation faults.
|
||||
"""
|
||||
def __init__(self, widget_type: str, config: dict, layout_manager, global_state):
|
||||
super().__init__(widget_type, config, layout_manager, global_state)
|
||||
|
||||
# Subscribe to the event that will provide new images
|
||||
self.global_state.subscribe("NEW_IMAGE_CAPTURED", self.on_new_image)
|
||||
|
||||
# Register for updates to handle the dirty flag and panning
|
||||
self.global_state.subscribe("PROCESSED_IMAGE_READY", self.on_new_image_data)
|
||||
layout_manager.updating_widgets.append("ImageViewerWidget")
|
||||
|
||||
# Initialize tags and state variables
|
||||
self.texture_tag = None
|
||||
self.image_draw_tag = None
|
||||
self.last_image_size = (1, 1) # width, height
|
||||
self.needs_fit = False # The "dirty flag" for deferred fitting
|
||||
# --- Initialize state ---
|
||||
# A flag to know if the DPG items have been created yet.
|
||||
self.is_initialized = False
|
||||
# Generate the tags once. They will be reused for the widget's lifetime.
|
||||
self.texture_tag = dpg.generate_uuid()
|
||||
self.image_draw_tag = dpg.generate_uuid()
|
||||
|
||||
self.last_image_size = (1, 1)
|
||||
self.needs_fit = False
|
||||
|
||||
def create(self):
|
||||
"""Creates the DPG window, plot, and the necessary mouse handlers."""
|
||||
"""Creates the DPG window and plot container. Does NOT create textures or drawings."""
|
||||
if dpg.does_item_exist(self.window_tag): return
|
||||
|
||||
with dpg.window(label="Image Viewer", tag=self.window_tag, on_close=self._on_window_close, width=800, height=600):
|
||||
# The plot is our canvas. `equal_aspects` is critical for preventing distortion.
|
||||
with dpg.plot(label="Image Plot", no_menus=True, height=-1, width=-1, equal_aspects=True) as self.plot_tag:
|
||||
self.xaxis_tag = dpg.add_plot_axis(dpg.mvXAxis, no_tick_labels=True, no_gridlines=True)
|
||||
self.yaxis_tag = dpg.add_plot_axis(dpg.mvYAxis, no_tick_labels=True, no_gridlines=True)
|
||||
|
||||
def on_new_image(self, image_path: str):
|
||||
"""Loads image data, deletes/recreates the drawing, and flags for a refit."""
|
||||
if not image_path: return
|
||||
logging.info(f"ImageViewer received new image: {image_path}")
|
||||
with dpg.item_handler_registry(tag=f"my_window_handler_{self.window_tag}") as handler:
|
||||
dpg.add_item_resize_handler(callback=self.fit_image_to_plot, user_data=self)
|
||||
dpg.bind_item_handler_registry(self.window_tag, handler)
|
||||
|
||||
def on_new_image_data(self, image_data: np.ndarray):
|
||||
"""Handles receiving a processed NumPy array, creating/updating items safely."""
|
||||
logging.info("ImageViewer received new processed image data.")
|
||||
try:
|
||||
width, height, channels, data = dpg.load_image(image_path)
|
||||
height, width, channels = image_data.shape
|
||||
self.last_image_size = (width, height)
|
||||
|
||||
# Create or update the texture in the registry
|
||||
if self.texture_tag is not None:
|
||||
dpg.delete_item(self.texture_tag)
|
||||
|
||||
self.texture_tag = dpg.generate_uuid()
|
||||
dpg.add_static_texture(width, height, data, tag=self.texture_tag, parent=self.layout_manager.texture_registry)
|
||||
# --- THE "REUSE AND RECONFIGURE" LOGIC ---
|
||||
if not self.is_initialized:
|
||||
# FIRST RUN: Create the texture and drawing items for the first time.
|
||||
logging.info("First image load: creating new texture and drawing items.")
|
||||
dpg.add_dynamic_texture(width, height, image_data, tag=self.texture_tag, parent=self.layout_manager.texture_registry)
|
||||
dpg.draw_image(self.texture_tag, (0, height), (width, 0), tag=self.image_draw_tag, parent=self.plot_tag)
|
||||
self.is_initialized = True
|
||||
else:
|
||||
# SUBSEQUENT RUNS: Update the existing items. NO DELETION.
|
||||
logging.info("Subsequent image load: updating existing texture and drawing.")
|
||||
dpg.set_value(self.texture_tag, image_data)
|
||||
dpg.configure_item(self.image_draw_tag, pmin=(0, height), pmax=(width, 0))
|
||||
|
||||
if self.image_draw_tag and dpg.does_item_exist(self.image_draw_tag):
|
||||
dpg.delete_item(self.image_draw_tag)
|
||||
|
||||
self.image_draw_tag = dpg.draw_image(self.texture_tag, (0, height), (width, 0), parent=self.plot_tag)
|
||||
|
||||
# Set the dirty flag to trigger a fit on the next frame
|
||||
# Set the dirty flag to trigger a fit on the next frame in all cases.
|
||||
self.needs_fit = True
|
||||
|
||||
dpg.configure_item(self.window_tag, show=True)
|
||||
dpg.configure_item(self.plot_tag, label=image_path)
|
||||
dpg.focus_item(self.window_tag)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"ImageViewer failed to process image '{image_path}': {e}", exc_info=True)
|
||||
logging.error(f"ImageViewer failed to process image data: {e}", exc_info=True)
|
||||
|
||||
def fit_image_to_plot(self):
|
||||
"""Calculates and MANUALLY sets the plot's axis limits for the initial fit."""
|
||||
# This function is correct and necessary.
|
||||
plot_width = dpg.get_item_width(self.window_tag)
|
||||
plot_height = dpg.get_item_height(self.window_tag)
|
||||
if plot_width <= 0 or plot_height <= 0: return
|
||||
|
||||
img_width, img_height = self.last_image_size
|
||||
if img_width <= 0 or img_height <= 0: return
|
||||
|
||||
|
36
widgets/raw_settings_widget.py
Normal file
36
widgets/raw_settings_widget.py
Normal file
@ -0,0 +1,36 @@
|
||||
# in widgets/raw_settings_widget.py
|
||||
import dearpygui.dearpygui as dpg
|
||||
from .base_widget import BaseWidget
|
||||
|
||||
class RawSettingsWidget(BaseWidget):
|
||||
"""A widget to control the rawpy processing parameters stored in GlobalState."""
|
||||
def create(self):
|
||||
if dpg.does_item_exist(self.window_tag): return
|
||||
|
||||
with dpg.window(label="RAW Development", tag=self.window_tag, on_close=self._on_window_close):
|
||||
dpg.add_text("rawpy Postprocessing Settings")
|
||||
dpg.add_separator()
|
||||
|
||||
# Create UI elements that directly modify the shared state dictionary
|
||||
dpg.add_checkbox(
|
||||
label="Auto White Balance",
|
||||
default_value=self.global_state.raw_params["use_auto_wb"],
|
||||
callback=lambda s, a, u: self.global_state.raw_params.update({"use_auto_wb": a})
|
||||
)
|
||||
dpg.add_checkbox(
|
||||
label="Use Camera White Balance",
|
||||
default_value=self.global_state.raw_params["use_camera_wb"],
|
||||
callback=lambda s, a, u: self.global_state.raw_params.update({"use_camera_wb": a})
|
||||
)
|
||||
dpg.add_checkbox(
|
||||
label="Disable Auto-Brightness",
|
||||
default_value=self.global_state.raw_params["no_auto_bright"],
|
||||
callback=lambda s, a, u: self.global_state.raw_params.update({"no_auto_bright": a})
|
||||
)
|
||||
dpg.add_radio_button(
|
||||
label="Output BPS",
|
||||
items=["8", "16"],
|
||||
default_value=str(self.global_state.raw_params["output_bps"]),
|
||||
callback=lambda s, a, u: self.global_state.raw_params.update({"output_bps": int(a)}),
|
||||
horizontal=True
|
||||
)
|
Reference in New Issue
Block a user