Compare commits

...

11 Commits

Author SHA1 Message Date
66d542f742 Framing widget done 2025-08-04 18:12:08 +02:00
2a6498f117 Start of framing widget 2025-08-04 12:04:54 +02:00
447354266c Start of crop widget 2025-08-03 21:23:42 +02:00
a423ceb669 Added scrolling as well 2025-08-03 20:29:04 +02:00
34cc28897d Added image clicking, dragging and orientation 2025-08-03 20:17:33 +02:00
1a5abca8e1 Exporting fixed 2025-08-03 18:39:00 +02:00
bbedfe6d35 RAW config is stored in config json 2025-08-03 17:47:27 +02:00
a0ba27b35c Fixed logging 2025-08-03 17:30:12 +02:00
ac33ec5d8d Config saving of pipeline in/out 2025-08-03 17:16:48 +02:00
9801f69d0b Added high-res pass 2025-08-03 12:55:31 +02:00
60d28e92d0 Added empty README 2025-08-02 17:30:28 +02:00
21 changed files with 1697 additions and 223 deletions

3
.gitignore vendored
View File

@ -1,2 +1,3 @@
__pycache__
env
env
out.png

0
README.md Normal file
View File

169
negative_raw.nef.xmp Normal file
View File

@ -0,0 +1,169 @@
<?xml version="1.0" encoding="UTF-8"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 4.4.0-Exiv2">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:exif="http://ns.adobe.com/exif/1.0/"
xmlns:xmp="http://ns.adobe.com/xap/1.0/"
xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/"
xmlns:darktable="http://darktable.sf.net/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:lr="http://ns.adobe.com/lightroom/1.0/"
exif:DateTimeOriginal="2025:07:26 16:46:04.370"
xmp:Rating="0"
xmpMM:DerivedFrom="negative_raw.nef"
darktable:import_timestamp="63889663903342009"
darktable:change_timestamp="-1"
darktable:export_timestamp="63889663939611277"
darktable:print_timestamp="-1"
darktable:xmp_version="5"
darktable:raw_params="0"
darktable:auto_presets_applied="1"
darktable:history_end="11"
darktable:iop_order_version="4"
darktable:history_auto_hash="d760eeba94580ffacc8656d2f1b16c05"
darktable:history_current_hash="d760eeba94580ffacc8656d2f1b16c05">
<darktable:masks_history>
<rdf:Seq/>
</darktable:masks_history>
<darktable:history>
<rdf:Seq>
<rdf:li
darktable:num="0"
darktable:operation="rawprepare"
darktable:enabled="1"
darktable:modversion="2"
darktable:params="000000000000000000000000000000005802580258025802143e000000000000"
darktable:multi_name=""
darktable:multi_name_hand_edited="0"
darktable:multi_priority="0"
darktable:blendop_version="14"
darktable:blendop_params="gz11eJxjYIAACQYYOOHEgAZY0QWAgBGLGANDgz0Ej1Q+dcF/IADRAGpyHQU="/>
<rdf:li
darktable:num="1"
darktable:operation="demosaic"
darktable:enabled="1"
darktable:modversion="4"
darktable:params="0000000000000000000000000500000001000000cdcc4c3e"
darktable:multi_name=""
darktable:multi_name_hand_edited="0"
darktable:multi_priority="0"
darktable:blendop_version="14"
darktable:blendop_params="gz11eJxjYIAACQYYOOHEgAZY0QWAgBGLGANDgz0Ej1Q+dcF/IADRAGpyHQU="/>
<rdf:li
darktable:num="2"
darktable:operation="colorin"
darktable:enabled="1"
darktable:modversion="7"
darktable:params="gz48eJzjZhgFowABWAbaAaNgwAEAOQAAEA=="
darktable:multi_name=""
darktable:multi_name_hand_edited="0"
darktable:multi_priority="0"
darktable:blendop_version="14"
darktable:blendop_params="gz11eJxjYIAACQYYOOHEgAZY0QWAgBGLGANDgz0Ej1Q+dcF/IADRAGpyHQU="/>
<rdf:li
darktable:num="3"
darktable:operation="colorout"
darktable:enabled="1"
darktable:modversion="5"
darktable:params="gz35eJxjZBgFo4CBAQAEEAAC"
darktable:multi_name=""
darktable:multi_name_hand_edited="0"
darktable:multi_priority="0"
darktable:blendop_version="14"
darktable:blendop_params="gz11eJxjYIAACQYYOOHEgAZY0QWAgBGLGANDgz0Ej1Q+dcF/IADRAGpyHQU="/>
<rdf:li
darktable:num="4"
darktable:operation="gamma"
darktable:enabled="1"
darktable:modversion="1"
darktable:params="0000000000000000"
darktable:multi_name=""
darktable:multi_name_hand_edited="0"
darktable:multi_priority="0"
darktable:blendop_version="14"
darktable:blendop_params="gz11eJxjYIAACQYYOOHEgAZY0QWAgBGLGANDgz0Ej1Q+dcF/IADRAGpyHQU="/>
<rdf:li
darktable:num="5"
darktable:operation="temperature"
darktable:enabled="1"
darktable:modversion="4"
darktable:params="004003400000803f0080b33f0000000004000000"
darktable:multi_name=""
darktable:multi_name_hand_edited="0"
darktable:multi_priority="0"
darktable:blendop_version="14"
darktable:blendop_params="gz11eJxjYIAACQYYOOHEgAZY0QWAgBGLGANDgz0Ej1Q+dcF/IADRAGpyHQU="/>
<rdf:li
darktable:num="6"
darktable:operation="highlights"
darktable:enabled="1"
darktable:modversion="4"
darktable:params="050000000000803f00000000000000000000803f000000001e00000006000000cdcccc3e000000400000000000000000"
darktable:multi_name=""
darktable:multi_name_hand_edited="0"
darktable:multi_priority="0"
darktable:blendop_version="14"
darktable:blendop_params="gz11eJxjYGBgYARiCQYYOOHEgAZY0QWgejBBgz0Ej1Q+dcF/IADRAGwSHQY="/>
<rdf:li
darktable:num="7"
darktable:operation="channelmixerrgb"
darktable:enabled="1"
darktable:modversion="3"
darktable:params="gz04eJxjYGiwZ8AAxIqRD9iAmAmIWYCYEYifft9jZ6e11076Z6sryC5GqDwAs0IJJA=="
darktable:multi_name="_builtin_scene-referred default"
darktable:multi_name_hand_edited="0"
darktable:multi_priority="0"
darktable:blendop_version="14"
darktable:blendop_params="gz08eJxjYGBgYAFiCQYYOOHEgAZY0QWAgBGLGANDgz0Ej1Q+dlAx68oBEMbFxwX+AwGIBgCbGCeh"/>
<rdf:li
darktable:num="8"
darktable:operation="exposure"
darktable:enabled="1"
darktable:modversion="6"
darktable:params="00000000000080b93333333f00004842000080c001000000"
darktable:multi_name="_builtin_scene-referred default"
darktable:multi_name_hand_edited="0"
darktable:multi_priority="0"
darktable:blendop_version="14"
darktable:blendop_params="gz08eJxjYGBgYAFiCQYYOOHEgAZY0QWAgBGLGANDgz0Ej1Q+dlAx68oBEMbFxwX+AwGIBgCbGCeh"/>
<rdf:li
darktable:num="9"
darktable:operation="filmicrgb"
darktable:enabled="1"
darktable:modversion="6"
darktable:params="gz02eJybNXOy49kzXw60vp7owAAGDkD6hBMEQ8AsoBr9ZRU2IDG1yHQHruvKQHaDPUz+7BkfO2YgzQLEjFAxRiQ2DDBBaQC8LhQY"
darktable:multi_name="_builtin_scene-referred default"
darktable:multi_name_hand_edited="0"
darktable:multi_priority="0"
darktable:blendop_version="14"
darktable:blendop_params="gz08eJxjYGBgYAFiCQYYOOHEgAZY0QWAgBGLGANDgz0Ej1Q+dlAx68oBEMbFxwX+AwGIBgCbGCeh"/>
<rdf:li
darktable:num="10"
darktable:operation="flip"
darktable:enabled="1"
darktable:modversion="2"
darktable:params="ffffffff"
darktable:multi_name="_builtin_auto"
darktable:multi_name_hand_edited="0"
darktable:multi_priority="0"
darktable:blendop_version="14"
darktable:blendop_params="gz11eJxjYIAACQYYOOHEgAZY0QWAgBGLGANDgz0Ej1Q+dcF/IADRAGpyHQU="/>
</rdf:Seq>
</darktable:history>
<dc:subject>
<rdf:Bag>
<rdf:li>darktable</rdf:li>
<rdf:li>exported</rdf:li>
<rdf:li>format</rdf:li>
<rdf:li>nef</rdf:li>
</rdf:Bag>
</dc:subject>
<lr:hierarchicalSubject>
<rdf:Bag>
<rdf:li>darktable|exported</rdf:li>
<rdf:li>darktable|format|nef</rdf:li>
</rdf:Bag>
</lr:hierarchicalSubject>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>

View File

@ -9,10 +9,22 @@ class ImagePipeline:
self.id_counter = 0
self.stages = {}
self.stagedata = {}
self.stagedata_full = {}
def load_stages(self, stages:dict):
self.stages = stages
self.stagedata.clear()
self.stagedata_full.clear()
self.id_counter = len(stages)
for id, stage in self.stages.items():
print(id, stage)
self.stagedata[id] = None
self.stagedata_full[id] = None
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,18 +34,32 @@ 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):
if id in self.stagedata:
return self.stagedata[id]
else:
return None
def get_stage_data_full(self, id: int):
if id in self.stagedata_full:
return self.stagedata_full[id]
else:
return None
def get_stage_name(self, id: int):
if id >= 0 and id < len(self.stages):
if id in self.stages:
return self.stages[id]
else:
return None

View File

@ -20,12 +20,15 @@ class LayoutManager:
def save_layout(self):
self.logger.info("Saving layout...")
dpg.save_init_file(self.INI_PATH)
widget_data = [
{"widget_type": type(w).__name__, "config": w.get_config()}
for w in self.manager.widgets
]
layout_data = {
"pipeline_order" : { k:v for k, v in self.manager.pipeline.stages.items() },
"widgets": [
{"widget_type": type(w).__name__, "config": w.get_config()}
for w in self.manager.widgets
]
}
with open(self.WIDGET_DATA_PATH, "w") as f:
json.dump(widget_data, f, indent=4)
json.dump(layout_data, f, indent=4)
self.logger.info("Layout saved successfully.")
def load_layout(self):
@ -33,10 +36,17 @@ class LayoutManager:
if not os.path.exists(self.WIDGET_DATA_PATH):
return
with open(self.WIDGET_DATA_PATH, "r") as f:
widget_data = json.load(f)
layout_data = json.load(f)
# Load all widgets
widget_data = layout_data["widgets"]
for data in widget_data:
if data.get("widget_type") in self.manager.widget_classes:
self.manager._add_widget(widget_type=data.get("widget_type"))
self.manager._add_widget(widget_type=data.get("widget_type"), config=data.get("config"))
# Reset the image pipeline and reload it
pipelinestages = { int(k):v for k, v in layout_data["pipeline_order"].items() }
self.manager.pipeline.load_stages(pipelinestages)
if os.path.exists(self.INI_PATH):
dpg.configure_app(init_file=self.INI_PATH)

View File

@ -13,8 +13,7 @@ from .layout_manager import LayoutManager
from .widgets.base_widget import BaseWidget
logging.basicConfig(level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s")
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger(__name__)
@ -62,24 +61,51 @@ class EditorManager:
and cls is not ModuleBaseWidget
and cls.register
):
logging.info(
f" -> Found and registered widget: {name}")
logging.info(f" -> Found and registered widget: {name}")
self._register_widget(name, cls)
except Exception as e:
logging.error(f"Failed to import widget '{py_file.name}': {e}")
def _register_widget(self, name: str, widget_class: object):
if name in self.widget_classes:
logging.warning(
f"Widget '{name}' is already registered. Overwriting.")
logging.warning(f"Widget '{name}' is already registered. Overwriting.")
self.widget_classes[name] = widget_class
def _add_widget(self, widget_type: str):
def _add_widget(self, widget_type: str, config: dict = {}):
WidgetClass = self.widget_classes[widget_type]
instance = WidgetClass(self, logger)
logger.info(f'Created instance: {str(instance)}')
logger.info(f"Created instance: {str(instance)}")
self.widgets.append(instance)
instance.create()
instance.set_config(config)
def _on_drag(self, sender, app_data, user_data):
self.bus.publish_deferred(
"mouse_dragged",
{
"button": (
"right"
if app_data[0] == 0
else ("left" if app_data[0] == 1 else ("middle"))
),
"delta": (app_data[1], app_data[2]),
},
)
def _on_scroll(self, sender, app_data, user_data):
self.bus.publish_deferred("mouse_scrolled", app_data)
def _on_release(self, sender, app_data, user_data):
self.bus.publish_deferred(
"mouse_released",
{
"button": (
"right"
if app_data == 0
else ("left" if app_data == 1 else ("middle"))
)
},
)
def setup(self):
self._discover_and_register_widgets(
@ -96,8 +122,12 @@ class EditorManager:
label="Save Layout", callback=self.layout_manager.save_layout
)
dpg.add_menu_item(
label="Quit", callback=lambda: dpg.stop_dearpygui()
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())
with dpg.menu(label="View"):
for widget_name in sorted(self.widget_classes.keys()):
@ -107,6 +137,11 @@ class EditorManager:
user_data=widget_name,
)
with dpg.handler_registry() as self.handler_registry:
dpg.add_mouse_drag_handler(callback=self._on_drag, threshold=1.0)
dpg.add_mouse_wheel_handler(callback=self._on_scroll)
dpg.add_mouse_release_handler(callback=self._on_release)
def run(self):
self.setup()
dpg.setup_dearpygui()

View File

@ -63,6 +63,10 @@ class BaseWidget:
def get_config(self):
"""Caled by negstation itself, returns the saved widget config"""
return self.config
def set_config(self, config):
"""Called by negstation itself but can be overridden by a widget"""
self.config = config
# Callbacks

View File

@ -0,0 +1,142 @@
# 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}")
import os
import dearpygui.dearpygui as dpg
import numpy as np
from PIL import Image
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):
# we dont register an output stage — this widget only consumes
super().__init__(manager, logger, default_stage_out="unused")
# tags for our “Save As” dialog
self._save_dialog_tag = dpg.generate_uuid()
self._save_path = f"{os.getcwd()}/out.png"
def create_pipeline_stage_content(self):
# Button to pop up the file-save dialog
dpg.add_button(label="Save As…",
callback=lambda s,a,u: dpg.configure_item(self._save_dialog_tag, show=True))
# File dialog for choosing export path & extension
with dpg.file_dialog(
directory_selector=False,
show=False,
callback=self._on_save_selected,
tag=self._save_dialog_tag,
width=400,
height=300
):
dpg.add_file_extension("PNG {.png}")
dpg.add_file_extension("JPEG {.jpg,.jpeg}")
dpg.add_file_extension("TIFF {.tif,.tiff}")
dpg.add_file_extension("All files {.*}")
with dpg.child_window(autosize_x=True, autosize_y=True, horizontal_scrollbar=True):
self.path_label = dpg.add_text("out.png")
def _on_save_selected(self, sender, app_data):
"""
Called when the user picks a filename in the Save As… dialog.
Stores the path for the next full-res pass.
"""
# app_data is a dict with 'current_path' and 'selections'
path = os.path.join(
app_data["current_path"],
app_data["file_name"]
)
self._save_path = path
self.logger.info(f"Export path set to: {path}")
dpg.set_value(self.path_label, path)
def on_pipeline_data(self, img: np.ndarray):
# ignore all previews
return
def on_full_res_pipeline_data(self, img: np.ndarray):
"""
Receives the full-resolution NumPy image when the user fires
the “Run full-res pipeline” action. Saves via Pillow.
"""
if img is None:
self.logger.error("on_full_res_pipeline_data called with None image")
return
if not self._save_path:
self.logger.warning("No export path set — click Save As… first")
return
# Decide bit depth by extension
ext = os.path.splitext(self._save_path)[-1].lower()
# Convert floats → uint; or leave ints alone
if np.issubdtype(img.dtype, np.floating):
if ext in (".tif", ".tiff"):
arr = np.clip(img * 65535.0, 0, 65535).astype(np.uint16)
else:
arr = np.clip(img * 255.0, 0, 255).astype(np.uint8)
else:
arr = img
# Determine PIL mode
mode = None
if arr.ndim == 2:
mode = "L"
elif arr.ndim == 3:
c = arr.shape[2]
if c == 3:
mode = "RGB"
elif c == 4:
mode = "RGBA"
try:
im = Image.fromarray(arr, mode) if mode else Image.fromarray(arr)
# JPEG doesnt support alpha — drop it
if ext in (".jpg", ".jpeg") and im.mode == "RGBA":
im = im.convert("RGB")
im.save(self._save_path)
except Exception as e:
self.logger.error(f"Failed to save image to {self._save_path}: {e}")
else:
self.logger.info(f"Saved full-resolution image to {self._save_path}")

View File

@ -0,0 +1,178 @@
import dearpygui.dearpygui as dpg
import numpy as np
import time
import scipy.ndimage as snd
from .stage_viewer_widget import PipelineStageViewer
class FramingWidget(PipelineStageViewer):
name = "Framing"
register = True
has_pipeline_in = True
has_pipeline_out = True
def __init__(self, manager, logger):
super().__init__(manager, logger)
self.needs_publishing = False
# Rotation line endpoints (canvas coords)
self.rot_start = None # (x, y)
self.rot_end = None # (x, y)
self.angle = 0.0 # computed deskew angle
# Crop rect endpoints (canvas coords)
self.crop_start = None # (x, y)
self.crop_end = None # (x, y)
self.manager.bus.subscribe("img_clicked", self.on_click)
self.manager.bus.subscribe("img_dragged", self.on_drag)
self.manager.bus.subscribe("img_scrolled", self.on_scroll)
self.manager.bus.subscribe("img_released", self.on_release)
def create_pipeline_stage_content(self):
super().create_pipeline_stage_content()
def on_full_res_pipeline_data(self, img):
if img is None:
return
img = self.rotate(img)
self.publish_stage(self.crop(img))
def on_pipeline_data(self, img):
if img is None:
return
self.original_img = img.copy()
self.needs_update = True
# Apply transformation and publish
self.img = self.rotate(img)
self.publish_stage(self.crop(self.img))
def update_texture(self, img):
super().update_texture(img)
# Draw rotation guide
if self.rot_start and self.rot_end:
p0 = self._pos_to_canvas(self.rot_start)
p1 = self._pos_to_canvas(self.rot_end)
dpg.draw_line(
p1=p0,
p2=p1,
color=(255, 0, 0, 255),
thickness=2,
parent=self.drawlist,
)
# Draw crop rect
if self.crop_start and self.crop_end:
p0 = self._pos_to_canvas(self.crop_start)
p1 = self._pos_to_canvas(self.crop_end)
dpg.draw_rectangle(
pmin=p0,
pmax=p1,
color=(255, 0, 255, 255),
thickness=2,
parent=self.drawlist,
)
if self.needs_publishing:
img = self.crop(self.img)
self.publish_stage(img)
def _pos_to_canvas(self, img_pos):
x, y = img_pos
ix, iy = self.image_position
iw, ih = self.img.shape[1], self.img.shape[0]
sw, sh = self.scaled_size
return (ix + x / iw * sw, iy + y / ih * sh)
def on_click(self, data):
if data.get("obj") is not self:
return
x, y = data.get("pos")
if data["button"] == "left":
self.rot_start = (x, y)
self.rot_end = None
elif data["button"] == "right":
self.crop_start = (x,y)
self.crop_end = None
self.needs_update = True
def on_drag(self, data):
if data.get("obj") is not self:
return
x, y = data.get("pos")
if data["button"] == "left":
self.rot_end = (x, y)
elif data["button"] == "right":
self.crop_end = (x,y)
self.needs_update = True
def on_scroll(self, data):
if data.get("obj") is not self:
return
def on_release(self, data):
if data.get("obj") is not self:
return
if data["button"] == "left":
# End of rotation line dragging
dx = self.rot_end[0] - self.rot_start[0]
dy = self.rot_end[1] - self.rot_start[1]
self.angle += np.degrees(np.arctan2(dy, dx))
# Do the rotation
self.img = self.rotate(self.original_img)
# Delete lines
self.rot_start = self.rot_end = None
self.needs_publishing = True
elif data["button"] == "right":
self.needs_publishing = True
self.needs_update = True
def rotate(self, img):
img = snd.rotate(img, self.angle, reshape=False)
return img
def crop(self, img):
"""
Crop `img` according to a rectangle drawn in the DISPLAY,
by converting displaycoords to imagecoords via percentages.
"""
# only crop if user has dragged out a box
if not (self.crop_start and self.crop_end):
return img
# 1) get actual image dims
h, w = img.shape[:2]
# 2) get display info (set in update_texture)
disp_h, disp_w, _ = self.img.shape
# 3) unpack the two drag points (in screen coords)
sx, sy = self.crop_start
ex, ey = self.crop_end
# 4) normalize into [0..1]
nx0 = np.clip(sx / disp_w, 0.0, 1.0)
ny0 = np.clip(sy / disp_h, 0.0, 1.0)
nx1 = np.clip(ex / disp_w, 0.0, 1.0)
ny1 = np.clip(ey / disp_h, 0.0, 1.0)
# 5) map to imagepixel coords
x0 = int(nx0 * w)
y0 = int(ny0 * h)
x1 = int(nx1 * w)
y1 = int(ny1 * h)
# 6) sort & clamp again just in case
x0, x1 = sorted((max(0, x0), min(w, x1)))
y0, y1 = sorted((max(0, y0), min(h, y1)))
# 7) avoid zeroarea
if x0 == x1 or y0 == y1:
return img
return img[y0:y1, x0:x1]

View File

@ -41,6 +41,9 @@ class HistogramWidget(PipelineStageWidget):
self.img = img
self.needs_redraw = True
def on_full_res_pipeline_data(self, img):
pass
def update(self):
# TODO move calculations to on_pipeline_data
if not self.needs_redraw or self.img is None:

View File

@ -14,7 +14,7 @@ class InvertStage(PipelineStageWidget):
super().__init__(manager, logger, default_stage_out="inverted_image")
def create_pipeline_stage_content(self):
dpg.add_text("Inversion is happening here")
pass
def on_pipeline_data(self, img):
if img is None:

View File

@ -22,6 +22,8 @@ class LogWindowWidget(BaseWidget):
self.initialized = False
self.log_tag = dpg.generate_uuid()
self.log_lines = []
self.update_counter = 0
self.need_update = False
# Create and attach handler
self.handler = DPGLogHandler(self._on_log)
@ -40,7 +42,7 @@ class LogWindowWidget(BaseWidget):
self.log_lines.append(msg)
if self.initialized:
dpg.add_text(msg, parent=self.log_tag)
dpg.set_y_scroll(self.log_tag, dpg.get_y_scroll_max(self.log_tag))
self.need_update = True
def on_resize(self, width: int, height: int):
# Optional: could resize child window here if needed
@ -51,3 +53,11 @@ class LogWindowWidget(BaseWidget):
self.logger.removeHandler(self.handler)
self.handler = None
super()._on_window_close()
def update(self):
if self.need_update:
self.update_counter += 1
if self.update_counter == 10:
dpg.set_y_scroll(self.log_tag, dpg.get_y_scroll_max(self.log_tag))
self.update_counter = 0
self.need_update = False

View File

@ -14,7 +14,7 @@ class MonochromeStage(PipelineStageWidget):
super().__init__(manager, logger, default_stage_out="monochrome")
def create_pipeline_stage_content(self):
dpg.add_text("Converting to grayscale...")
pass
def on_pipeline_data(self, img):
if img is None:

View File

@ -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 0255 uint8, resize with PIL, back to float32 [01]
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)

View File

@ -1,6 +1,8 @@
import dearpygui.dearpygui as dpg
import rawpy
import numpy as np
import ast
from PIL import Image
from .pipeline_stage_widget import PipelineStageWidget
@ -18,7 +20,9 @@ class OpenRawWidget(PipelineStageWidget):
self.config_group = dpg.generate_uuid()
self.busy_group = dpg.generate_uuid()
self.raw_path = None
self.config = {
self.img = None
self.img_full = None
self.rawconfig = {
# Demosaic algorithm
"demosaic_algorithm": rawpy.DemosaicAlgorithm.AHD,
# Output color space
@ -39,8 +43,22 @@ class OpenRawWidget(PipelineStageWidget):
"four_color_rgb": False,
}
def get_config(self):
return {}
self.demosaic_combo_tag = dpg.generate_uuid()
self.color_space_combo_tag = dpg.generate_uuid()
self.output_bps_combo_tag = dpg.generate_uuid()
self.use_cam_wb_tag = dpg.generate_uuid()
self.auto_wb_tag = dpg.generate_uuid()
self.wb_r_slider_tag = dpg.generate_uuid()
self.wb_g_slider_tag = dpg.generate_uuid()
self.wb_b_slider_tag = dpg.generate_uuid()
self.bright_slider_tag = dpg.generate_uuid()
self.no_auto_bright_tag = dpg.generate_uuid()
self.gamma_slider_tag = dpg.generate_uuid()
self.half_size_tag = dpg.generate_uuid()
self.four_color_tag = dpg.generate_uuid()
self.manager.bus.subscribe(
"process_full_res", self._on_process_full_res, True)
def create_pipeline_stage_content(self):
with dpg.file_dialog(
@ -57,90 +75,125 @@ class OpenRawWidget(PipelineStageWidget):
dpg.add_file_extension(".*")
with dpg.group(tag=self.config_group):
# -- Open / Reprocess buttons --
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_button(label="Open File...", callback=self._on_open_file)
dpg.add_button(label="Reprocess", callback=self._process_and_publish)
# -- Demosaic combo --
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])
default_value=self.rawconfig["demosaic_algorithm"].name,
callback=lambda s,a,u: self.rawconfig.__setitem__(
"demosaic_algorithm", rawpy.DemosaicAlgorithm[a]
),
tag=self.demosaic_combo_tag
)
# -- Color space combo --
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])
default_value=self.rawconfig["output_color"].name,
callback=lambda s,a,u: self.rawconfig.__setitem__(
"output_color", rawpy.ColorSpace[a]
),
tag=self.color_space_combo_tag
)
# -- Bits per sample --
dpg.add_combo(
label="Output Bits",
items=["8", "16"],
default_value="16",
callback=lambda s, a, u: self.config.__setitem__(
"output_bps", int(a))
items=["8","16"],
default_value=str(self.rawconfig["output_bps"]),
callback=lambda s,a,u: self.rawconfig.__setitem__(
"output_bps", int(a)
),
tag=self.output_bps_combo_tag
)
# -- Checkboxes & sliders --
dpg.add_checkbox(
label="Use Camera WB",
default_value=True,
callback=lambda s, a, u: self.config.__setitem__(
"use_camera_wb", a)
default_value=self.rawconfig["use_camera_wb"],
callback=lambda s,a,u: self.rawconfig.__setitem__("use_camera_wb", a),
tag=self.use_cam_wb_tag
)
dpg.add_checkbox(
label="Auto WB",
default_value=False,
callback=lambda s, a, u: self.config.__setitem__(
"use_auto_wb", a)
default_value=self.rawconfig["use_auto_wb"],
callback=lambda s,a,u: self.rawconfig.__setitem__("use_auto_wb", a),
tag=self.auto_wb_tag
)
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]))
default_value=self.rawconfig["user_wb"][0],
min_value=0.1, max_value=4.0,
callback=lambda s,a,u: self.rawconfig.__setitem__(
"user_wb", (a, self.rawconfig["user_wb"][1],
self.rawconfig["user_wb"][2], self.rawconfig["user_wb"][3])
),
tag=self.wb_r_slider_tag
)
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]))
default_value=self.rawconfig["user_wb"][1],
min_value=0.1, max_value=4.0,
callback=lambda s,a,u: self.rawconfig.__setitem__(
"user_wb", (self.rawconfig["user_wb"][0], a,
self.rawconfig["user_wb"][2], self.rawconfig["user_wb"][3])
),
tag=self.wb_g_slider_tag
)
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))
default_value=self.rawconfig["user_wb"][2],
min_value=0.1, max_value=4.0,
callback=lambda s,a,u: self.rawconfig.__setitem__(
"user_wb", (self.rawconfig["user_wb"][0],
self.rawconfig["user_wb"][1], a,
self.rawconfig["user_wb"][3])
),
tag=self.wb_b_slider_tag
)
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)
default_value=self.rawconfig["bright"],
min_value=0.1, max_value=4.0,
callback=lambda s,a,u: self.rawconfig.__setitem__("bright", a),
tag=self.bright_slider_tag
)
dpg.add_checkbox(
label="No Auto Bright",
default_value=False,
callback=lambda s, a, u: self.config.__setitem__(
"no_auto_bright", a)
default_value=self.rawconfig["no_auto_bright"],
callback=lambda s,a,u: self.rawconfig.__setitem__(
"no_auto_bright", a
),
tag=self.no_auto_bright_tag
)
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)
default_value=self.rawconfig["gamma"],
min_value=0.1, max_value=3.0,
callback=lambda s,a,u: self.rawconfig.__setitem__("gamma", a),
tag=self.gamma_slider_tag
)
dpg.add_checkbox(
label="Half-size",
default_value=False,
callback=lambda s, a, u: self.config.__setitem__(
"half_size", a)
default_value=self.rawconfig["half_size"],
callback=lambda s,a,u: self.rawconfig.__setitem__(
"half_size", a
),
tag=self.half_size_tag
)
dpg.add_checkbox(
label="4-color RGB",
default_value=False,
callback=lambda s, a, u: self.config.__setitem__(
"four_color_rgb", a)
default_value=self.rawconfig["four_color_rgb"],
callback=lambda s,a,u: self.rawconfig.__setitem__(
"four_color_rgb", a
),
tag=self.four_color_tag
)
with dpg.group(tag=self.busy_group, show=False):
@ -173,28 +226,28 @@ class OpenRawWidget(PipelineStageWidget):
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"],
'demosaic_algorithm': self.rawconfig["demosaic_algorithm"],
'output_color': self.rawconfig["output_color"],
'output_bps': self.rawconfig["output_bps"],
'bright': self.rawconfig["bright"],
'no_auto_bright': self.rawconfig["no_auto_bright"],
'gamma': (1.0, self.rawconfig["gamma"]),
'half_size': self.rawconfig["half_size"],
'four_color_rgb': self.rawconfig["four_color_rgb"],
}
if self.config["use_camera_wb"]:
if self.rawconfig["use_camera_wb"]:
postprocess_args['use_camera_wb'] = True
elif self.config["use_auto_wb"]:
elif self.rawconfig["use_auto_wb"]:
postprocess_args['use_auto_wb'] = True
else:
postprocess_args['user_wb'] = self.config["user_wb"]
postprocess_args['user_wb'] = self.rawconfig["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
max_val = (2 ** self.rawconfig["output_bps"]) - 1
rgb_float = rgb.astype(np.float32) / max_val
# Add alpha channel (fully opaque)
@ -203,6 +256,76 @@ 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 0255 uint8, resize with PIL, back to float32 [01]
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)
def get_config(self):
config = super().get_config()
config["raw_config"] = { k:str(v) for k, v in self.rawconfig.items() }
return config
def set_config(self, config):
super().set_config(config)
raw_cfg = config.get("raw_config", {})
if raw_cfg:
# parse each back into Python types
for k, v in raw_cfg.items():
if k == "demosaic_algorithm":
# "DemosaicAlgorithm.AHD" → "AHD"
name = v.split(".", 1)[1]
self.rawconfig[k] = rawpy.DemosaicAlgorithm[name]
elif k == "output_color":
name = v.split(".", 1)[1]
self.rawconfig[k] = rawpy.ColorSpace[name]
elif k == "output_bps":
self.rawconfig[k] = int(v)
elif k in ("use_camera_wb","use_auto_wb",
"no_auto_bright","half_size","four_color_rgb"):
self.rawconfig[k] = (v == "True")
elif k in ("bright","gamma"):
self.rawconfig[k] = float(v)
elif k == "user_wb":
self.rawconfig[k] = tuple(ast.literal_eval(v))
# now that rawconfig is back to real types, update the UI
self._update_raw_ui()
def _update_raw_ui(self):
"""Push current self.rawconfig values back into all controls."""
# combos want the enum.name or string
dpg.set_value(self.demosaic_combo_tag, self.rawconfig["demosaic_algorithm"].name)
dpg.set_value(self.color_space_combo_tag, self.rawconfig["output_color"].name)
dpg.set_value(self.output_bps_combo_tag, str(self.rawconfig["output_bps"]))
# checkboxes & sliders
dpg.set_value(self.use_cam_wb_tag, self.rawconfig["use_camera_wb"])
dpg.set_value(self.auto_wb_tag, self.rawconfig["use_auto_wb"])
dpg.set_value(self.wb_r_slider_tag, self.rawconfig["user_wb"][0])
dpg.set_value(self.wb_g_slider_tag, self.rawconfig["user_wb"][1])
dpg.set_value(self.wb_b_slider_tag, self.rawconfig["user_wb"][2])
dpg.set_value(self.bright_slider_tag, self.rawconfig["bright"])
dpg.set_value(self.no_auto_bright_tag, self.rawconfig["no_auto_bright"])
dpg.set_value(self.gamma_slider_tag, self.rawconfig["gamma"])
dpg.set_value(self.half_size_tag, self.rawconfig["half_size"])
dpg.set_value(self.four_color_tag, self.rawconfig["four_color_rgb"])

View File

@ -0,0 +1,121 @@
import dearpygui.dearpygui as dpg
import numpy as np
from .pipeline_stage_widget import PipelineStageWidget
class OrientationStage(PipelineStageWidget):
name = "Orient Image"
register = True
has_pipeline_in = True
has_pipeline_out = True
def __init__(self, manager, logger):
super().__init__(manager, logger, default_stage_out="oriented_image")
self.rotation = 0
self.mirror_h = False
self.mirror_v = False
self.rotation_combo_tag = dpg.generate_uuid()
self.mirror_h_tag = dpg.generate_uuid()
self.mirror_v_tag = dpg.generate_uuid()
self.last_image = None
def create_pipeline_stage_content(self):
dpg.add_combo(
label="Rotation",
items=["", "90°", "180°", "270°"],
default_value="",
callback=self._on_rotation_change,
tag=self.rotation_combo_tag
)
dpg.add_checkbox(
label="Mirror Horizontal",
default_value=False,
callback=self._on_mirror_h_change,
tag=self.mirror_h_tag
)
dpg.add_checkbox(
label="Mirror Vertical",
default_value=False,
callback=self._on_mirror_v_change,
tag=self.mirror_v_tag
)
def _on_rotation_change(self, sender, value, user_data):
degree_map = {
"": 0,
"90°": 90,
"180°": 180,
"270°": 270
}
self.rotation = degree_map.get(value, 0)
self.on_pipeline_data(self.last_img)
def _on_mirror_h_change(self, sender, value, user_data):
self.mirror_h = value
self.on_pipeline_data(self.last_img)
def _on_mirror_v_change(self, sender, value, user_data):
self.mirror_v = value
self.on_pipeline_data(self.last_img)
def on_pipeline_data(self, img):
if img is None:
return
self.last_img = img
img_out = img.copy()
# Apply rotation
if self.rotation == 90:
img_out = np.rot90(img_out, k=3)
elif self.rotation == 180:
img_out = np.rot90(img_out, k=2)
elif self.rotation == 270:
img_out = np.rot90(img_out, k=1)
# Apply mirroring
if self.mirror_h:
img_out = np.fliplr(img_out)
if self.mirror_v:
img_out = np.flipud(img_out)
self.publish_stage(img_out)
def get_config(self):
config = super().get_config()
config["orientation"] = {
"rotation": self.rotation,
"mirror_h": str(self.mirror_h),
"mirror_v": str(self.mirror_v),
}
return config
def set_config(self, config):
super().set_config(config)
orient_cfg = config.get("orientation", {})
self.rotation = int(orient_cfg.get("rotation", 0))
self.mirror_h = orient_cfg.get("mirror_h", "False") == "True"
self.mirror_v = orient_cfg.get("mirror_v", "False") == "True"
self._update_ui()
def _update_ui(self):
# Update rotation combo
reverse_map = {
0: "",
90: "90°",
180: "180°",
270: "270°"
}
dpg.set_value(self.rotation_combo_tag, reverse_map.get(self.rotation, ""))
# Update checkboxes
dpg.set_value(self.mirror_h_tag, self.mirror_h)
dpg.set_value(self.mirror_v_tag, self.mirror_v)

View File

@ -23,6 +23,8 @@ 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.stage_out_input = dpg.generate_uuid()
self._last_full = False
if self.has_pipeline_out:
self.pipeline_stage_out_id = self.manager.pipeline.register_stage(
@ -33,6 +35,9 @@ class PipelineStageWidget(BaseWidget):
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_full", self._on_stage_data_full, True
)
# force getting all available pipeline stages
self.manager.pipeline.republish_stages()
@ -43,8 +48,9 @@ class PipelineStageWidget(BaseWidget):
label="Stage In",
items=[],
callback=self._on_stage_in_select,
default_value=f"{self.manager.pipeline.get_stage_name(0)} : 0",
tag=self.stage_in_combo
default_value=f"{
self.manager.pipeline.get_stage_name(0)} : 0",
tag=self.stage_in_combo,
)
if self.has_pipeline_out:
dpg.add_input_text(
@ -55,14 +61,11 @@ class PipelineStageWidget(BaseWidget):
callback=lambda s, a, u: self.manager.pipeline.rename_stage(
self.pipeline_stage_out_id, a
),
tag=self.stage_out_input,
)
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)
with dpg.group():
self.create_pipeline_stage_content()
def create_pipeline_stage_content(self):
"""Must be implemented by the widget, creates the content of the window"""
@ -72,6 +75,65 @@ 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
)
# Reset last_full
self._last_full = False
def get_config(self):
return {
"pipeline_config": {
"stage_in": self.pipeline_stage_in_id,
"stage_out": self.pipeline_stage_out_id,
}
}
def set_config(self, config):
# Set pipelinedata
if "pipeline_config" in config:
if self.has_pipeline_in:
self.pipeline_stage_in_id = config["pipeline_config"]["stage_in"]
if self.has_pipeline_out:
self.pipeline_stage_out_id = config["pipeline_config"]["stage_out"]
self._update_ui_from_state()
def _update_ui_from_state(self):
"""
Refresh the Stage In combo (and Stage Out input) so
that:
1. its items list matches the current pipeline stages, and
2. its displayed value matches the saved ID.
"""
# --- Build the ordered list of "name : id" labels ---
ordered = sorted(self.manager.pipeline.stages.items(), key=lambda kv: kv[0])
labels = [f"{name} : {sid}" for sid, name in ordered]
# --- Update Stage In combo ---
if self.has_pipeline_in:
dpg.configure_item(self.stage_in_combo, items=labels)
# set the combo's value if the saved ID still exists
sid = self.pipeline_stage_in_id
if sid in self.manager.pipeline.stages:
name = self.manager.pipeline.get_stage_name(sid)
dpg.set_value(self.stage_in_combo, f"{name} : {sid}")
else:
# clear if it no longer exists
dpg.set_value(self.stage_in_combo, "")
# --- Update Stage Out input text ---
if self.has_pipeline_out:
# show the stage name (without ID) or blank if missing
sid = self.pipeline_stage_out_id
if sid in self.manager.pipeline.stages:
name = self.manager.pipeline.get_stage_name(sid)
dpg.set_value(self.stage_out_input, name)
else:
dpg.set_value(self.stage_out_input, "")
# Callbacks
def _on_window_close(self):
@ -81,8 +143,7 @@ class PipelineStageWidget(BaseWidget):
def _on_stage_list(self, stagelist):
if self.has_pipeline_in:
stages = [f"{stage} : {id}" for id, stage in stagelist.items()]
dpg.configure_item(self.stage_in_combo, items=stages)
self._update_ui_from_state()
def _on_stage_in_select(self, sender, selected_stage: str):
d = selected_stage.split(" : ")
@ -97,14 +158,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_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
self.window_offset_y = group_h + group_y + 3
self.on_resize(win_w, win_h)

View File

@ -1,6 +1,5 @@
import dearpygui.dearpygui as dpg
import numpy as np
from PIL import Image
from .pipeline_stage_widget import PipelineStageWidget
@ -13,77 +12,219 @@ class PipelineStageViewer(PipelineStageWidget):
def __init__(self, manager, logger):
super().__init__(manager, logger, default_stage_in="pipeline_out")
self.texture_tag = dpg.generate_uuid()
self.drawlist = None
self.img = None
self.needs_update = False
self.registry = manager.texture_registry
self.needs_update = False
self.canvas_handler = None
self.scaled_size = (0, 0)
self.image_position = (0, 0)
self._last_tex_size = (0, 0)
self.manager.bus.subscribe("mouse_dragged", self._on_mouse_drag, False)
self.manager.bus.subscribe("mouse_scrolled", self._on_mouse_scroll, False)
self.manager.bus.subscribe("mouse_released", self._on_mouse_release, False)
def create_pipeline_stage_content(self):
# Create an empty dynamic texture
dpg.add_dynamic_texture(
1, 1, [0, 0, 0, 0], tag=self.texture_tag, parent=self.registry
)
self.image_item = dpg.add_image(self.texture_tag)
# Add drawlist
with dpg.drawlist(width=-1, height=-1) as self.drawlist:
pass
# Register click handler
with dpg.item_handler_registry() as self.canvas_handler:
dpg.add_item_clicked_handler(callback=self.on_canvas_click)
dpg.bind_item_handler_registry(self.drawlist, self.canvas_handler)
def on_canvas_click(self, sender, app_data, user_data):
mouse_x, mouse_y = dpg.get_mouse_pos(local=False)
canvas_x, canvas_y = dpg.get_item_rect_min(self.drawlist)
local_x = mouse_x - canvas_x
local_y = mouse_y - canvas_y
img_x, img_y = self.image_position
img_w, img_h = self.scaled_size
if (
local_x >= img_x
and local_x < img_x + img_w
and local_y >= img_y
and local_y < img_y + img_h
and dpg.is_item_focused(self.window_tag)
):
# calculate the image coordinate
x = int((local_x - img_x) * self.img.shape[1] / img_w)
y = int((local_y - img_y) * self.img.shape[0] / img_h)
self.manager.bus.publish_deferred(
"img_clicked",
{
"stage_id": self.pipeline_stage_in_id,
"pos": (x, y),
"button": (
"right"
if app_data[0] == 0
else ("left" if app_data[0] == 1 else ("middle"))
),
"obj":self,
},
)
def _on_mouse_drag(self, data):
mouse_x, mouse_y = dpg.get_mouse_pos(local=False)
canvas_x, canvas_y = dpg.get_item_rect_min(self.drawlist)
local_x = mouse_x - canvas_x
local_y = mouse_y - canvas_y
img_x, img_y = self.image_position
img_w, img_h = self.scaled_size
if (
local_x >= img_x
and local_x < img_x + img_w
and local_y >= img_y
and local_y < img_y + img_h
and dpg.is_item_focused(self.window_tag)
):
# calculate the image coordinate
x = int((local_x - img_x) * self.img.shape[1] / img_w)
y = int((local_y - img_y) * self.img.shape[0] / img_h)
self.manager.bus.publish_deferred(
"img_dragged",
{
"stage_id": self.pipeline_stage_in_id,
"pos": (x, y),
"button": data['button'],
"delta": data['delta'],
"obj":self,
},
)
def _on_mouse_scroll(self, data):
mouse_x, mouse_y = dpg.get_mouse_pos(local=False)
canvas_x, canvas_y = dpg.get_item_rect_min(self.drawlist)
local_x = mouse_x - canvas_x
local_y = mouse_y - canvas_y
img_x, img_y = self.image_position
img_w, img_h = self.scaled_size
if (
local_x >= img_x
and local_x < img_x + img_w
and local_y >= img_y
and local_y < img_y + img_h
and dpg.is_item_focused(self.window_tag)
):
# calculate the image coordinate
x = int((local_x - img_x) * self.img.shape[1] / img_w)
y = int((local_y - img_y) * self.img.shape[0] / img_h)
self.manager.bus.publish_deferred(
"img_scrolled",
{
"stage_id": self.pipeline_stage_in_id,
"pos": (x, y),
"delta": data,
"obj":self,
},
)
def _on_mouse_release(self, data):
mouse_x, mouse_y = dpg.get_mouse_pos(local=False)
canvas_x, canvas_y = dpg.get_item_rect_min(self.drawlist)
local_x = mouse_x - canvas_x
local_y = mouse_y - canvas_y
img_x, img_y = self.image_position
img_w, img_h = self.scaled_size
if (
local_x >= img_x
and local_x < img_x + img_w
and local_y >= img_y
and local_y < img_y + img_h
and dpg.is_item_focused(self.window_tag)
):
# calculate the image coordinate
x = int((local_x - img_x) * self.img.shape[1] / img_w)
y = int((local_y - img_y) * self.img.shape[0] / img_h)
self.manager.bus.publish_deferred(
"img_released",
{
"stage_id": self.pipeline_stage_in_id,
"pos": (x, y),
"button": data['button'],
"obj":self,
},
)
def on_resize(self, width, height):
self.needs_update = True
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)
if scale < 1.0:
# convert to 0255 uint8, resize with PIL, back to float32 [01]
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
if img is None:
dpg.configure_item(self.image_item, show=False)
return
# Only recreate the texture if its size changed
h, w, _ = img.shape
flat = img.flatten().tolist()
if (w, h) != self._last_tex_size or not dpg.does_item_exist(self.texture_tag):
# remove old one if present
if dpg.does_item_exist(self.texture_tag):
dpg.delete_item(self.texture_tag)
# create a new dynamic texture at the new size
dpg.add_dynamic_texture(
width=w,
height=h,
default_value=flat,
tag=self.texture_tag,
parent=self.registry,
)
self._last_tex_size = (w, h)
else:
# just upload new pixels into the existing texture
dpg.set_value(self.texture_tag, flat)
if dpg.does_item_exist(self.texture_tag):
dpg.delete_item(self.texture_tag)
dpg.add_dynamic_texture(
width=w,
height=h,
default_value=flat,
tag=self.texture_tag,
parent=self.registry,
)
# Clear old drawings
dpg.delete_item(self.drawlist, children_only=True)
# Draw the image to the screen
win_w, win_h = self.window_width, self.window_height
avail_w = win_w
avail_h = win_h
scale = min(avail_w / w, avail_h / h) # , 1.0)
scale = min(win_w / w, win_h / h)
disp_w = int(w * scale)
disp_h = int(h * scale)
x_off = (avail_w - disp_w) / 2
y_off = self.window_offset_y
x_off = (win_w - disp_w) / 2
y_off = (win_h - disp_h) / 2
dpg.configure_item(
self.image_item,
texture_tag=self.texture_tag,
pos=(x_off, y_off),
width=disp_w,
height=disp_h,
show=True
self.scaled_size = (disp_w, disp_h)
self.image_position = (x_off, y_off)
# Draw image
dpg.draw_image(
self.texture_tag,
pmin=(x_off, y_off),
pmax=(x_off + disp_w, y_off + disp_h),
uv_min=(0, 0),
uv_max=(1, 1),
parent=self.drawlist,
)
# Resize drawlist
dpg.configure_item(self.drawlist, width=win_w, height=win_h)
def update(self):
if self.needs_update:
self.needs_update = False

View File

@ -17,9 +17,9 @@ DockId=0x00000007,0
[Window][###51]
Pos=0,19
Size=270,469
Size=270,691
Collapsed=0
DockId=0x0000001F,1
DockId=0x0000005B,1
[Window][###59]
Pos=0,494
@ -46,9 +46,9 @@ DockId=0x0000000E,0
[Window][###23]
Pos=0,19
Size=270,469
Size=270,437
Collapsed=0
DockId=0x0000001F,0
DockId=0x0000005B,0
[Window][###29]
Pos=0,19
@ -60,7 +60,7 @@ DockId=0x00000012,0
Pos=198,19
Size=602,425
Collapsed=0
DockId=0x00000011,0
DockId=0x00000014,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=0x00000014,0
[Window][###60]
Pos=0,19
@ -81,16 +81,16 @@ Collapsed=0
DockId=0x00000012,1
[Window][###107]
Pos=246,400
Size=554,200
Pos=958,19
Size=242,460
Collapsed=0
DockId=0x00000016,0
DockId=0x00000059,0
[Window][###35]
Pos=272,19
Size=710,625
Size=698,591
Collapsed=0
DockId=0x00000011,0
DockId=0x00000014,0
[Window][###43]
Pos=0,490
@ -99,8 +99,8 @@ Collapsed=0
DockId=0x00000020,0
[Window][###81]
Pos=272,646
Size=710,154
Pos=272,609
Size=700,191
Collapsed=0
DockId=0x00000010,0
@ -108,25 +108,25 @@ DockId=0x00000010,0
Pos=0,646
Size=270,154
Collapsed=0
DockId=0x00000015,1
DockId=0x00000013,1
[Window][###87]
Pos=0,646
Size=270,154
Pos=272,646
Size=710,154
Collapsed=0
DockId=0x00000015,0
DockId=0x00000046,0
[Window][###111]
Pos=900,19
Size=300,781
Pos=974,19
Size=226,641
Collapsed=0
DockId=0x0000001A,0
DockId=0x0000004B,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,365 @@ 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,634
Collapsed=0
DockId=0x0000003F,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,596
Size=216,204
Collapsed=0
DockId=0x0000004E,0
[Window][###129]
Pos=939,555
Size=261,245
Collapsed=0
DockId=0x0000002C,0
[Window][###97]
Pos=972,19
Size=228,560
Collapsed=0
DockId=0x0000002F,0
[Window][###113]
Pos=972,581
Size=228,219
Collapsed=0
DockId=0x00000030,0
[Window][###44]
Pos=0,712
Size=270,88
Collapsed=0
DockId=0x00000036,0
[Window][###52]
Pos=0,19
Size=270,567
Collapsed=0
DockId=0x0000005B,1
[Window][###82]
Pos=272,601
Size=710,199
Collapsed=0
DockId=0x00000034,0
[Window][###88]
Pos=984,19
Size=216,628
Collapsed=0
DockId=0x00000031,0
[Window][###95]
Pos=0,700
Size=270,100
Collapsed=0
DockId=0x00000042,0
[Window][###120]
Pos=984,582
Size=216,218
Collapsed=0
DockId=0x0000003A,0
[Window][###100]
Pos=984,19
Size=216,561
Collapsed=0
DockId=0x00000039,0
[Window][###123]
Pos=984,655
Size=216,145
Collapsed=0
DockId=0x0000003E,0
[Window][###102]
Pos=984,19
Size=216,467
Collapsed=0
DockId=0x00000051,0
[Window][###124]
Pos=984,700
Size=216,100
Collapsed=0
DockId=0x00000040,0
[Window][###122]
Pos=987,597
Size=216,100
Collapsed=0
[Window][###46]
Pos=0,700
Size=270,100
Collapsed=0
DockId=0x00000042,1
[Window][###53]
Pos=0,19
Size=270,679
Collapsed=0
DockId=0x0000005B,1
[Window][###83]
Pos=272,612
Size=698,188
Collapsed=0
DockId=0x00000044,0
[Window][###89]
Pos=272,605
Size=710,195
Collapsed=0
DockId=0x00000015,0
[Window][###119]
Pos=984,653
Size=216,147
Collapsed=0
DockId=0x0000004A,0
[Window][###36]
Pos=0,458
Size=270,254
Collapsed=0
DockId=0x0000005C,0
[Window][###48]
Pos=0,714
Size=270,86
Collapsed=0
DockId=0x00000036,1
[Window][###56]
Pos=0,19
Size=270,437
Collapsed=0
DockId=0x0000005B,1
[Window][###93]
Pos=0,714
Size=270,86
Collapsed=0
DockId=0x00000036,0
[Window][###133]
Pos=974,645
Size=226,155
Collapsed=0
DockId=0x00000048,0
[Window][###110]
Pos=974,19
Size=226,624
Collapsed=0
DockId=0x00000047,0
[Window][###109]
Pos=958,19
Size=242,262
Collapsed=0
DockId=0x00000057,0
[Window][###131]
Pos=958,664
Size=242,136
Collapsed=0
DockId=0x00000016,0
[Window][###128]
Pos=60,60
Size=141,100
Collapsed=0
[Window][###50]
Pos=0,700
Size=270,100
Collapsed=0
DockId=0x00000042,1
[Window][###58]
Pos=0,19
Size=270,679
Collapsed=0
DockId=0x0000005B,1
[Window][###104]
Pos=984,19
Size=216,575
Collapsed=0
DockId=0x0000004D,0
[Window][###156]
Pos=984,451
Size=216,200
Collapsed=0
DockId=0x00000050,0
[Window][###136]
Pos=984,488
Size=216,163
Collapsed=0
DockId=0x00000052,0
[Window][###170]
Pos=272,19
Size=710,625
Collapsed=0
DockId=0x00000014,1
[Window][###148]
Pos=272,19
Size=710,625
Collapsed=0
DockId=0x00000014,0
[Window][###169]
Pos=272,19
Size=684,625
Collapsed=0
DockId=0x00000014,0
[Window][###152]
Pos=958,283
Size=242,368
Collapsed=0
DockId=0x00000058,0
[Window][###157]
Pos=958,481
Size=242,181
Collapsed=0
DockId=0x0000005A,0
[Window][###178]
Pos=60,60
Size=148,100
Collapsed=0
[Window][###184]
Pos=272,19
Size=710,625
Collapsed=0
DockId=0x00000014,1
[Docking][Data]
DockSpace ID=0x7C6B3D9B Window=0xA87D555D Pos=0,19 Size=1200,781 Split=X
DockNode ID=0x00000053 Parent=0x7C6B3D9B SizeRef=956,781 Split=X
DockNode ID=0x00000037 Parent=0x00000053 SizeRef=982,781 Split=X
DockNode ID=0x0000002D Parent=0x00000037 SizeRef=970,781 Split=X
DockNode ID=0x0000002B Parent=0x0000002D 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=972,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=0x0000001F Parent=0x00000013 SizeRef=270,469 Split=Y Selected=0xD36850C8
DockNode ID=0x00000035 Parent=0x0000001F SizeRef=270,693 Split=Y Selected=0xD36850C8
DockNode ID=0x00000041 Parent=0x00000035 SizeRef=270,679 Split=Y Selected=0x068DEF00
DockNode ID=0x0000005B Parent=0x00000041 SizeRef=270,437 Selected=0x068DEF00
DockNode ID=0x0000005C Parent=0x00000041 SizeRef=270,254 Selected=0xD0D40C1D
DockNode ID=0x00000042 Parent=0x00000035 SizeRef=270,100 Selected=0x89CD1AA0
DockNode ID=0x00000036 Parent=0x0000001F SizeRef=270,86 Selected=0xB9AFA00B
DockNode ID=0x00000020 Parent=0x00000013 SizeRef=270,154 Selected=0x0531B3D5
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=684,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,588 Split=Y Selected=0x977476CD
DockNode ID=0x00000033 Parent=0x0000000F SizeRef=710,580 Split=Y Selected=0x977476CD
DockNode ID=0x00000043 Parent=0x00000033 SizeRef=710,591 Split=Y Selected=0x4EE4732B
DockNode ID=0x00000045 Parent=0x00000043 SizeRef=710,625 Split=Y Selected=0xD0D40C1D
DockNode ID=0x00000014 Parent=0x00000045 SizeRef=710,584 CentralNode=1 Selected=0x2349CB28
DockNode ID=0x00000015 Parent=0x00000045 SizeRef=710,195 Selected=0x38436B0F
DockNode ID=0x00000046 Parent=0x00000043 SizeRef=710,154 Selected=0x8773D56E
DockNode ID=0x00000044 Parent=0x00000033 SizeRef=710,188 Selected=0x72F373AE
DockNode ID=0x00000034 Parent=0x0000000F SizeRef=710,199 Selected=0x4F935A1E
DockNode ID=0x00000010 Parent=0x00000001 SizeRef=602,191 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=226,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 Split=Y Selected=0x7E9438EA
DockNode ID=0x0000004B Parent=0x0000001A SizeRef=226,641 Selected=0x7E9438EA
DockNode ID=0x0000004C Parent=0x0000001A SizeRef=226,138 Split=Y Selected=0x499CCA81
DockNode ID=0x00000047 Parent=0x0000004C SizeRef=226,624 Selected=0x43F4115A
DockNode ID=0x00000048 Parent=0x0000004C SizeRef=226,155 Selected=0x499CCA81
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 Split=Y Selected=0xCF08B82F
DockNode ID=0x00000031 Parent=0x00000025 SizeRef=216,628 Selected=0x052342BF
DockNode ID=0x00000032 Parent=0x00000025 SizeRef=216,151 Split=Y Selected=0xCF08B82F
DockNode ID=0x0000003B Parent=0x00000032 SizeRef=216,634 Split=Y Selected=0xCF08B82F
DockNode ID=0x0000003F Parent=0x0000003B SizeRef=216,679 Selected=0xCF08B82F
DockNode ID=0x00000040 Parent=0x0000003B SizeRef=216,100 Selected=0x30E0C534
DockNode ID=0x0000003C Parent=0x00000032 SizeRef=216,145 Split=Y Selected=0x82C01924
DockNode ID=0x0000003D Parent=0x0000003C SizeRef=216,613 Split=Y Selected=0xF268919F
DockNode ID=0x00000049 Parent=0x0000003D SizeRef=216,632 Split=Y Selected=0xF268919F
DockNode ID=0x0000004F Parent=0x00000049 SizeRef=216,430 Split=Y Selected=0xF268919F
DockNode ID=0x00000051 Parent=0x0000004F SizeRef=216,467 Selected=0xF268919F
DockNode ID=0x00000052 Parent=0x0000004F SizeRef=216,163 Selected=0x817C45F1
DockNode ID=0x00000050 Parent=0x00000049 SizeRef=216,200 Selected=0x5725A6EC
DockNode ID=0x0000004A Parent=0x0000003D SizeRef=216,147 Selected=0x4EE4732B
DockNode ID=0x0000003E Parent=0x0000003C SizeRef=216,166 Selected=0x82C01924
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 Split=Y Selected=0xF8004A44
DockNode ID=0x0000004D Parent=0x0000002A SizeRef=216,575 Selected=0x7D28643F
DockNode ID=0x0000004E Parent=0x0000002A SizeRef=216,204 Selected=0xF8004A44
DockNode ID=0x0000002C Parent=0x0000002D SizeRef=261,781 Selected=0xC8700185
DockNode ID=0x0000002E Parent=0x00000037 SizeRef=228,781 Split=Y Selected=0x4C2F06CB
DockNode ID=0x0000002F Parent=0x0000002E SizeRef=216,560 Selected=0x4C2F06CB
DockNode ID=0x00000030 Parent=0x0000002E SizeRef=216,219 Selected=0x04546B8A
DockNode ID=0x00000038 Parent=0x00000053 SizeRef=216,781 Split=Y Selected=0x88A8C2FF
DockNode ID=0x00000039 Parent=0x00000038 SizeRef=216,561 Selected=0x88A8C2FF
DockNode ID=0x0000003A Parent=0x00000038 SizeRef=216,218 Selected=0xC56063F4
DockNode ID=0x00000054 Parent=0x7C6B3D9B SizeRef=242,781 Split=Y Selected=0xA2A5002C
DockNode ID=0x00000055 Parent=0x00000054 SizeRef=158,632 Split=Y Selected=0xA2A5002C
DockNode ID=0x00000057 Parent=0x00000055 SizeRef=158,262 Selected=0x85B8A08E
DockNode ID=0x00000058 Parent=0x00000055 SizeRef=158,368 Selected=0xA2A5002C
DockNode ID=0x00000056 Parent=0x00000054 SizeRef=158,147 Split=Y Selected=0x335C99E1
DockNode ID=0x00000011 Parent=0x00000056 SizeRef=242,643 Split=Y Selected=0x3A881EEF
DockNode ID=0x00000059 Parent=0x00000011 SizeRef=242,460 Selected=0x3A881EEF
DockNode ID=0x0000005A Parent=0x00000011 SizeRef=242,181 Selected=0x6A458F5C
DockNode ID=0x00000016 Parent=0x00000056 SizeRef=242,136 Selected=0x335C99E1

View File

@ -1,30 +1,115 @@
[
{
"widget_type": "OpenImageWidget",
"config": {}
{
"pipeline_order": {
"0": "opened_image",
"1": "inverted_image",
"2": "opened_raw",
"3": "monochrome",
"4": "oriented_image",
"6": "framed_image"
},
{
"widget_type": "PipelineStageViewer",
"config": {}
},
{
"widget_type": "InvertStage",
"config": {}
},
{
"widget_type": "OpenRawWidget",
"config": {}
},
{
"widget_type": "LogWindowWidget",
"config": {}
},
{
"widget_type": "MonochromeStage",
"config": {}
},
{
"widget_type": "HistogramWidget",
"config": {}
}
]
"widgets": [
{
"widget_type": "OpenImageWidget",
"config": {
"pipeline_config": {
"stage_in": null,
"stage_out": 0
}
}
},
{
"widget_type": "PipelineStageViewer",
"config": {
"pipeline_config": {
"stage_in": 6,
"stage_out": null
}
}
},
{
"widget_type": "InvertStage",
"config": {
"pipeline_config": {
"stage_in": 3,
"stage_out": 1
}
}
},
{
"widget_type": "OpenRawWidget",
"config": {
"pipeline_config": {
"stage_in": null,
"stage_out": 2
},
"raw_config": {
"demosaic_algorithm": "DemosaicAlgorithm.AHD",
"output_color": "ColorSpace.sRGB",
"output_bps": "16",
"use_camera_wb": "True",
"use_auto_wb": "False",
"user_wb": "(1.0, 1.0, 1.0, 1.0)",
"bright": "1.0",
"no_auto_bright": "False",
"gamma": "1.0",
"half_size": "False",
"four_color_rgb": "False"
}
}
},
{
"widget_type": "LogWindowWidget",
"config": {}
},
{
"widget_type": "MonochromeStage",
"config": {
"pipeline_config": {
"stage_in": 2,
"stage_out": 3
}
}
},
{
"widget_type": "HistogramWidget",
"config": {
"pipeline_config": {
"stage_in": 6,
"stage_out": null
}
}
},
{
"widget_type": "ExportStage",
"config": {
"pipeline_config": {
"stage_in": 6,
"stage_out": null
}
}
},
{
"widget_type": "OrientationStage",
"config": {
"pipeline_config": {
"stage_in": 1,
"stage_out": 4
},
"orientation": {
"rotation": 180,
"mirror_h": "False",
"mirror_v": "False"
}
}
},
{
"widget_type": "FramingWidget",
"config": {
"pipeline_config": {
"stage_in": 4,
"stage_out": 6
}
}
}
]
}

View File

@ -2,4 +2,5 @@ pillow
gphoto2
dearpygui
numpy
rawpy
rawpy
scipy