Skip to content

Commit 3f16b95

Browse files
committed
Updated structure and install instructions
1 parent c8bbce1 commit 3f16b95

File tree

7 files changed

+438
-57
lines changed

7 files changed

+438
-57
lines changed

README.md

Lines changed: 0 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -35,60 +35,6 @@ It uses a deep learning–based denoising model designed to preserve fine image
3535

3636
### Build from source from git (linux):
3737

38-
Clone the repository, enter the RawRefinery directory and install locally with pip. I recommend setting up a python 3.11 virtual environment, conda environment, or the equivalent.
39-
40-
#### Install required libraries
41-
sudo apt install python3 python3.12-venv build-essential python3-dev spice-vdagent qemu-guest-agent, upx-ucl
42-
43-
44-
```bash
45-
sudo apt install python3 python3.12-venv build-essential python3-dev
46-
git clone https://github.com/rymuelle/RawRefinery.git
47-
cd RawRefinery
48-
python3 -m venv .venv
49-
. .venv/bin/activate
50-
python3 -m pip install .
51-
```
52-
After this step, you can run the program as:
53-
54-
```bash
55-
python3 main.py
56-
```
57-
58-
However, if you wish to build an installable applicaiton, you may use pyinstaller. Still in the RawRefinery top level directory:
59-
60-
```bash
61-
python3 -m pip install pyinstaller
62-
# Clean up previous builds
63-
rm -rf build dist __pycache__
64-
pyinstaller linux/main.spec
65-
```
66-
67-
68-
# To build .deb
69-
sudo apt update
70-
sudo apt install ruby ruby-dev build-essential
71-
sudo gem install --no-document fpm
72-
73-
74-
```
75-
mkdir -p staging/opt/RawRefinery
76-
mkdir -p staging/usr/share/applications
77-
cp -r dist/RawRefinery/* staging/opt/RawRefinery/
78-
cp linux/RawRefinery.desktop staging/usr/share/applications/RawRefinery.desktop
79-
fpm -n rawrefinery -v 1.0.0 --description "A PySide6 application for Raw Data Processing." \
80-
-t deb -s dir \
81-
-C staging \
82-
--url "http://example.com/rawrefinery" \
83-
--vendor "Ryan Mueller" \
84-
--license "MIT" \
85-
.
86-
```
87-
88-
```bash
89-
sudo dpkg -i rawrefinery_1.0.0_amd64.deb
90-
```
91-
9238
### Download prebuilt installer (Mac OS Only, Windows and Linux executables coming soon)
9339

9440
Download the macOS build here:
Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
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

Comments
 (0)