1+ import sys
2+ import os
3+ import numpy as np
4+ from PySide6 .QtWidgets import (
5+ QApplication , QMainWindow , QWidget , QVBoxLayout , QPushButton ,
6+ QFileDialog , QListWidget , QLabel , QSlider , QSpinBox ,
7+ QHBoxLayout , QFormLayout , QComboBox , QProgressBar , QMessageBox
8+ )
9+ from PySide6 .QtGui import QPixmap , QIcon
10+ from PySide6 .QtCore import Qt , Slot
11+
12+ # Import utils
13+ from RawRefinery .application .viewing_utils import numpy_to_qimage_rgb
14+ from RawRefinery .application .ModelHandler import ModelController , MODEL_REGISTRY
15+ from RawRefinery .application .ClickableImageLabel import ClickableImageLabel
16+ from RawRefinery .application .dng_utils import to_dng , convert_ccm_to_rational
17+ from RawRefinery .application .LogarithmicSlider import LogarithmicSlider
18+
19+ class RawRefineryApp (QMainWindow ):
20+ def __init__ (self ):
21+ super ().__init__ ()
22+ self .setWindowTitle ("Raw Refinery" )
23+ self .resize (1000 , 700 )
24+
25+ # Logic Controller
26+ self .controller = ModelController ()
27+
28+ # Connect Signals
29+ self .controller .progress_update .connect (self .update_progress )
30+ self .controller .preview_ready .connect (self .display_result )
31+ self .controller .save_finished .connect (self .reset_after_save )
32+ self .controller .error_occurred .connect (self .show_error )
33+ self .controller .model_loaded .connect (lambda n : self .status_label .setText (f"Model Loaded: { n } " ))
34+ self .setup_ui ()
35+
36+ # Load default model
37+ self .controller .load_model ("Tree Net Denoise" )
38+
39+ def loading_popup (self ):
40+ popup = QMessageBox ()
41+ popup .setWindowTitle ("Notice" )
42+ popup .setIcon (QMessageBox .Icon .Information )
43+ popup .setText (
44+ """
45+ <h3>Alpha Release Notice</h3>
46+ <p>This software is currently in Alpha. Please note:</p>
47+ <ul>
48+ <li>Expect memory usage up to <b>3 GB</b>.</li>
49+ <li>CPU processing is slow. Use <b>MPS</b> or <b>CUDA</b> for best results.</li>
50+ </ul>
51+ """
52+ )
53+ popup .exec ()
54+
55+ def setup_ui (self ):
56+ self .central_widget = QWidget ()
57+ self .setCentralWidget (self .central_widget )
58+ self .main_layout = QHBoxLayout (self .central_widget )
59+
60+ ## Left Panel
61+ self .left_panel = QWidget ()
62+ self .left_layout = QVBoxLayout (self .left_panel )
63+
64+ self .btn_open_folder = QPushButton ("Open Folder" )
65+ self .btn_open_folder .clicked .connect (self .open_folder_dialog )
66+
67+ self .file_list = QListWidget ()
68+ self .file_list .currentItemChanged .connect (self .on_file_selected )
69+
70+ self .left_layout .addWidget (self .btn_open_folder )
71+ self .left_layout .addWidget (self .file_list )
72+ self .main_layout .addWidget (self .left_panel , 1 )
73+
74+ ## Right Panel
75+ self .right_panel = QWidget ()
76+ self .right_layout = QVBoxLayout (self .right_panel )
77+
78+ # Previews
79+ self .preview_container = QWidget ()
80+ self .preview_layout = QHBoxLayout (self .preview_container )
81+ self .thumb_label = ClickableImageLabel ("Thumbnail" , proportional = True )
82+ self .preview_label = QLabel ("Denoised Preview" )
83+ self .preview_label .setAlignment (Qt .AlignCenter )
84+ self .preview_label .setMinimumSize (400 , 400 )
85+
86+ # Connect thumbnail click
87+ self .thumb_label .imageClicked .connect (self .on_thumbnail_click )
88+
89+ self .preview_layout .addWidget (self .thumb_label )
90+ self .preview_layout .addWidget (self .preview_label )
91+ self .right_layout .addWidget (self .preview_container )
92+
93+ ## Controls
94+ self .controls_layout = QFormLayout ()
95+
96+ # Model Selector
97+ self .model_combo = QComboBox ()
98+ self .model_combo .addItems (MODEL_REGISTRY .keys ())
99+ self .model_combo .currentTextChanged .connect (self .controller .load_model )
100+ self .controls_layout .addRow ("Model:" , self .model_combo )
101+
102+ # Device Selector
103+ self .device_combo = QComboBox ()
104+ self .device_combo .addItems (self .controller .devices )
105+ self .device_combo .currentTextChanged .connect (self .controller .set_device )
106+ self .controls_layout .addRow ("Device:" , self .device_combo )
107+
108+ # ISO
109+ self .iso_slider = QSlider (Qt .Horizontal )
110+ self .iso_slider .setRange (0 , 65534 )
111+ self .iso_spin = QSpinBox ()
112+ self .iso_spin .setRange (0 , 65534 )
113+ self .iso_slider .valueChanged .connect (self .iso_spin .setValue )
114+ self .iso_spin .valueChanged .connect (self .iso_slider .setValue )
115+ self .controls_layout .addRow ("ISO:" , self .iso_slider )
116+ self .controls_layout .addRow ("" , self .iso_spin )
117+
118+ # Grain
119+ self .grain_slider = QSlider (Qt .Horizontal )
120+ self .grain_slider .setRange (0 , 100 )
121+ self .grain_spin = QSpinBox ()
122+ self .grain_spin .setRange (0 , 100 )
123+ self .grain_slider .valueChanged .connect (self .grain_spin .setValue )
124+ self .grain_spin .valueChanged .connect (self .grain_slider .setValue )
125+ self .controls_layout .addRow ("Grain:" , self .grain_slider )
126+ self .controls_layout .addRow ("" , self .grain_spin )
127+
128+ #Exposure
129+ self .exposure_slider = LogarithmicSlider (0.1 , 10. )
130+ self .exposure_slider .setNaturalValue (1. )
131+ self .exposure_label = QLabel (f"Exposure adjustment (for visualization only): { self .exposure_slider .get_natural_value ():.1f} " )
132+ self .exposure_slider .naturalValueChanged .connect (self .on_exposure_change )
133+ self .controls_layout .addRow (self .exposure_label , self .exposure_slider )
134+
135+ self .right_layout .addLayout (self .controls_layout )
136+
137+ # Action Buttons
138+ self .btn_preview = QPushButton ("Update Preview" )
139+ self .btn_preview .clicked .connect (self .trigger_preview )
140+ self .right_layout .addWidget (self .btn_preview )
141+
142+ self .btn_save_cfa = QPushButton ("Save CFA dng" )
143+ self .btn_save_cfa .clicked .connect (self .trigger_save )
144+ self .right_layout .addWidget (self .btn_save_cfa )
145+
146+ self .btn_save_test_patch = QPushButton ("Save Test Patch" )
147+ self .btn_save_test_patch .clicked .connect (self .trigger_save_test_patch )
148+ self .right_layout .addWidget (self .btn_save_test_patch )
149+
150+ # Status
151+ self .progress_bar = QProgressBar ()
152+ self .status_label = QLabel ("Ready" )
153+ self .right_layout .addWidget (self .progress_bar )
154+ self .right_layout .addWidget (self .status_label )
155+
156+ # Add Right Panel
157+ self .main_layout .addWidget (self .right_panel , 2 )
158+
159+ ## State
160+ self .dims = None
161+ self .current_folder = None
162+ self .current_file_path = None
163+
164+ def on_exposure_change (self , exposure ):
165+ # Set text
166+ self .exposure_label .setText (f"Exposure adjustment (for visualization only): { exposure :.1f} " )
167+ # Show thumbnail
168+ thumb_rgb = self .controller .generate_thumbnail ()
169+ qimg = numpy_to_qimage_rgb (thumb_rgb , exposure = exposure )
170+ self .thumb_label .set_image (qimg )
171+
172+
173+ def open_folder_dialog (self ):
174+ folder = QFileDialog .getExistingDirectory (self , "Select Folder" )
175+ if folder :
176+ self .current_folder = folder
177+ self .file_list .clear ()
178+ exts = ('.cr2' , '.nef' , '.arw' , '.dng' )
179+ files = sorted ([f for f in os .listdir (folder ) if f .lower ().endswith (exts )])
180+ self .file_list .addItems (files )
181+
182+ def on_file_selected (self , item ):
183+ if not item : return
184+ path = os .path .join (self .current_folder , item .text ())
185+ self .current_file_path = path
186+
187+ try :
188+ # Load metadata
189+ iso = self .controller .load_rh (path )
190+ self .iso_spin .setValue (iso )
191+
192+ # Show thumbnail
193+ thumb_rgb = self .controller .generate_thumbnail ()
194+ qimg = numpy_to_qimage_rgb (thumb_rgb , exposure = self .exposure_slider .get_natural_value ())
195+ self .thumb_label .set_image (qimg )
196+
197+ self .dims = None
198+ self .preview_label .setText ("Click thumbnail to preview region" )
199+
200+ except Exception as e :
201+ self .show_error (f"Failed to open file: { e } " )
202+
203+ def on_thumbnail_click (self , x_ratio , y_ratio ):
204+ # Calculate dims based on click ratio and raw size
205+ rh = self .controller .rh
206+ H_full , W_full = rh .raw .shape
207+
208+ # Center of click in raw coords
209+ c_x = int (x_ratio * W_full )
210+ c_y = int (y_ratio * H_full )
211+
212+ w_preview , h_preview = 512 , 512
213+
214+ # Calculate crops
215+ h_start = max (0 , c_y - h_preview // 2 )
216+ h_end = min (H_full , h_start + h_preview )
217+ w_start = max (0 , c_x - w_preview // 2 )
218+ w_end = min (W_full , w_start + w_preview )
219+
220+ self .dims = (h_start , h_end , w_start , w_end )
221+ self .trigger_preview ()
222+
223+ def disable_ui (self ):
224+ self .btn_preview .setEnabled (False )
225+ self .btn_save_cfa .setEnabled (False )
226+ self .thumb_label .setEnabled (False )
227+
228+ def enable_ui (self ):
229+ self .btn_preview .setEnabled (True )
230+ self .btn_save_cfa .setEnabled (True )
231+ self .thumb_label .setEnabled (True )
232+
233+ def trigger_preview (self ):
234+ if not self .dims :
235+ self .status_label .setText ("Select a region on thumbnail first." )
236+ return
237+
238+ conditioning = [self .iso_spin .value (), self .grain_spin .value ()]
239+ # Disable button to prevent spamming
240+ self .disable_ui ()
241+ self .status_label .setText ("Processing..." )
242+ # Fire off the worker
243+ self .controller .run_inference (conditioning , self .dims )
244+
245+ def trigger_save (self ):
246+ if not self .current_file_path :
247+ self .status_label .setText ("Select and image first." )
248+ return
249+
250+ output_filename , _ = QFileDialog .getSaveFileName (
251+ self , "Save Denoised Image" ,
252+ os .path .splitext (self .current_file_path )[0 ] + "_denoised.dng" ,
253+ "DNG Image (*.dng)"
254+ )
255+
256+ conditioning = [self .iso_spin .value (), self .grain_spin .value ()]
257+ self .disable_ui ()
258+ self .status_label .setText ("Processing..." )
259+ self .controller .save_image (output_filename , conditioning , save_cfa = True )
260+
261+ def trigger_save_test_patch (self ):
262+ if not self .current_file_path :
263+ self .status_label .setText ("Select and image first." )
264+ return
265+ try :
266+ output_filename , _ = QFileDialog .getSaveFileName (
267+ self , "Save Denoised Image" ,
268+ os .path .splitext (self .current_file_path )[0 ] + "_test_patch.dng" ,
269+ "DNG Image (*.dng)"
270+ )
271+ cfa = self .controller .rh ._input_handler (dims = self .dims )
272+ cfa = np .squeeze (cfa , axis = 0 )
273+ ccm1 = convert_ccm_to_rational (self .controller .rh .core_metadata .rgb_xyz_matrix [:3 , :])
274+ to_dng (cfa , self .controller .rh , output_filename , ccm1 , save_cfa = True , convert_to_cfa = False , use_orig_wb_points = True )
275+ except Exception as e :
276+ self .show_error (f"Failed to save patch: { e } " )
277+
278+ # Slots
279+ @Slot (float )
280+ def update_progress (self , val ):
281+ self .progress_bar .setValue (int (val * 100 ))
282+
283+ @Slot (object , object )
284+ def display_result (self , img_rgb , denoised ):
285+ self .enable_ui ()
286+ self .status_label .setText ("Done." )
287+ self .progress_bar .setValue (100 )
288+
289+ # Convert output to QImage
290+ qimg = numpy_to_qimage_rgb (denoised , exposure = self .exposure_slider .get_natural_value ())
291+ pix = QPixmap .fromImage (qimg )
292+
293+ # Scale for display
294+ scaled = pix .scaled (self .preview_label .size (), Qt .KeepAspectRatio , Qt .SmoothTransformation )
295+ self .preview_label .setPixmap (scaled )
296+
297+ @Slot (float )
298+ def reset_after_save (self , str ):
299+ self .enable_ui ()
300+ self .status_label .setText (str )
301+ self .progress_bar .setValue (100. )
302+
303+ @Slot (str )
304+ def show_error (self , msg ):
305+ self .enable_ui ()
306+ self .status_label .setText ("Error." )
307+ QMessageBox .critical (self , "Error" , msg )
308+
309+ @Slot (str )
310+ def display_device (self , device ):
311+ print ("device used" )
312+ self .device_label .setText (device )
313+
314+ import platform
315+ if __name__ == '__main__' :
316+ app = QApplication (sys .argv )
317+ if platform .system () == "Darwin" : # Darwin is the kernel for macOS
318+ app .setApplicationDisplayName ("Raw Refinery" )
319+ app .setOrganizationName ("Ryan Mueller" )
320+ app .setDesktopFileName ("com.rawrefinery.app" )
321+
322+ window = RawRefineryApp ()
323+ window .show ()
324+ window .loading_popup ()
325+ sys .exit (app .exec ())
0 commit comments