Skip to content

Commit 21cb2b3

Browse files
committed
Split image settings dialog into image settings and lens calibration dialogs
- Extract lens calibration (distortion coefficients, wizard) into a separate LensCalibrationDialog - Add alignment_date and calibration_date timestamps to Camera model for tracking when each was last performed - Add status icons (checkmark/warning) to Lens Calibration and Image Alignment rows in camera properties, with tooltips explaining state - Show warning on alignment row when calibration was updated after alignment (points preserved but marked stale) - Backward compatible: existing configs with alignment points or non-zero distortion coefficients get timestamps on load
1 parent f8afae8 commit 21cb2b3

5 files changed

Lines changed: 331 additions & 94 deletions

File tree

rayforge/camera/models/camera.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ def __init__(self, name: str, device_id: str):
5454

5555
# Properties for camera alignment points
5656
self._image_to_world: Optional[Tuple[PointList, PointList]] = None
57+
self._alignment_date: Optional[datetime] = None
5758

5859
# Signals
5960
self.changed = Signal()
@@ -264,6 +265,7 @@ def distortion_k1(self) -> float:
264265
@distortion_k1.setter
265266
def distortion_k1(self, value: float):
266267
self._distortion_k1 = float(value)
268+
self._calibration_date = datetime.now()
267269
self.changed.send(self)
268270
self.settings_changed.send(self)
269271

@@ -274,6 +276,7 @@ def distortion_k2(self) -> float:
274276
@distortion_k2.setter
275277
def distortion_k2(self, value: float):
276278
self._distortion_k2 = float(value)
279+
self._calibration_date = datetime.now()
277280
self.changed.send(self)
278281
self.settings_changed.send(self)
279282

@@ -284,6 +287,7 @@ def distortion_p1(self) -> float:
284287
@distortion_p1.setter
285288
def distortion_p1(self, value: float):
286289
self._distortion_p1 = float(value)
290+
self._calibration_date = datetime.now()
287291
self.changed.send(self)
288292
self.settings_changed.send(self)
289293

@@ -294,6 +298,7 @@ def distortion_p2(self) -> float:
294298
@distortion_p2.setter
295299
def distortion_p2(self, value: float):
296300
self._distortion_p2 = float(value)
301+
self._calibration_date = datetime.now()
297302
self.changed.send(self)
298303
self.settings_changed.send(self)
299304

@@ -304,6 +309,7 @@ def distortion_k3(self) -> float:
304309
@distortion_k3.setter
305310
def distortion_k3(self, value: float):
306311
self._distortion_k3 = float(value)
312+
self._calibration_date = datetime.now()
307313
self.changed.send(self)
308314
self.settings_changed.send(self)
309315

@@ -432,6 +438,10 @@ def image_to_world(self, value: Optional[Tuple[PointList, PointList]]):
432438
f"{self._image_to_world} to {value}"
433439
)
434440
self._image_to_world = value
441+
if value is not None:
442+
self._alignment_date = datetime.now()
443+
else:
444+
self._alignment_date = None
435445
self.changed.send(self)
436446
self.settings_changed.send(self)
437447

@@ -489,6 +499,32 @@ def calibration_image_size(self) -> Optional[Tuple[int, int]]:
489499
def calibration_frames_used(self) -> Optional[int]:
490500
return self._calibration_frames_used
491501

502+
@property
503+
def alignment_date(self) -> Optional[datetime]:
504+
return self._alignment_date
505+
506+
@alignment_date.setter
507+
def alignment_date(self, value: Optional[datetime]):
508+
if self._alignment_date == value:
509+
return
510+
self._alignment_date = value
511+
self.changed.send(self)
512+
self.settings_changed.send(self)
513+
514+
@property
515+
def has_alignment(self) -> bool:
516+
return self._image_to_world is not None
517+
518+
@property
519+
def alignment_valid(self) -> bool:
520+
if not self.has_alignment:
521+
return False
522+
if self._calibration_date is None:
523+
return True
524+
if self._alignment_date is None:
525+
return False
526+
return self._alignment_date >= self._calibration_date
527+
492528
def to_dict(self) -> Dict[str, Any]:
493529
data = {
494530
"name": self.name,
@@ -521,6 +557,14 @@ def to_dict(self) -> Dict[str, Any]:
521557
else:
522558
data["image_to_world"] = None
523559

560+
if self._alignment_date is not None:
561+
data["alignment_date"] = self._alignment_date.isoformat()
562+
563+
if self._calibration_date is not None:
564+
data["calibration_date"] = (
565+
self._calibration_date.isoformat()
566+
)
567+
524568
if self.has_calibration:
525569
data["camera_matrix_fx"] = self._camera_matrix_fx
526570
data["camera_matrix_fy"] = self._camera_matrix_fy
@@ -569,6 +613,7 @@ def from_dict(cls, data: Dict[str, Any]) -> "Camera":
569613
"calibration_date",
570614
"calibration_image_size",
571615
"calibration_frames_used",
616+
"alignment_date",
572617
}
573618
extra = {k: v for k, v in data.items() if k not in known_keys}
574619

@@ -608,6 +653,13 @@ def from_dict(cls, data: Dict[str, Any]) -> "Camera":
608653
else:
609654
camera.image_to_world = None
610655

656+
if data.get("alignment_date"):
657+
camera._alignment_date = datetime.fromisoformat(
658+
data["alignment_date"]
659+
)
660+
elif camera._image_to_world is not None:
661+
camera._alignment_date = datetime.now()
662+
611663
camera._camera_matrix_fx = data.get("camera_matrix_fx")
612664
camera._camera_matrix_fy = data.get("camera_matrix_fy")
613665
camera._camera_matrix_cx = data.get("camera_matrix_cx")
@@ -617,6 +669,14 @@ def from_dict(cls, data: Dict[str, Any]) -> "Camera":
617669
camera._calibration_date = datetime.fromisoformat(
618670
data["calibration_date"]
619671
)
672+
elif any(v != 0.0 for v in [
673+
camera._distortion_k1,
674+
camera._distortion_k2,
675+
camera._distortion_k3,
676+
camera._distortion_p1,
677+
camera._distortion_p2,
678+
]):
679+
camera._calibration_date = datetime.now()
620680
if data.get("calibration_image_size"):
621681
camera._calibration_image_size = tuple(
622682
data["calibration_image_size"]

rayforge/ui_gtk/camera/alignment_dialog.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22
import math
3+
from datetime import datetime
34
from gettext import gettext as _
45
from typing import List, Optional, Tuple
56

@@ -641,6 +642,7 @@ def on_apply_clicked(self, _):
641642
raise ValueError("Less than 4 points for alignment.")
642643

643644
self.camera.image_to_world = (image_points, world_points)
645+
self.camera.alignment_date = datetime.now()
644646
logger.info("Camera alignment applied.")
645647
self.close()
646648

rayforge/ui_gtk/camera/image_settings_dialog.py

Lines changed: 1 addition & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -248,63 +248,11 @@ def _setup_ui(self):
248248
)
249249
image_group.add(row)
250250

251-
calibration_group = Adw.PreferencesGroup(
252-
title=_("Lens Calibration"),
253-
description=_(
254-
"Correct lens distortion for straighter lines. "
255-
"Use the wizard for automatic calibration or adjust manually."
256-
),
257-
margin_top=12,
258-
)
259-
260-
self.wizard_button = Gtk.Button(
261-
label=_("Wizard"), valign=Gtk.Align.CENTER, margin_start=8
262-
)
263-
self.wizard_button.connect("clicked", self._on_wizard_clicked)
264-
calibration_group.set_header_suffix(self.wizard_button)
265-
266-
settings_box.append(calibration_group)
267-
268-
self._distortion_rows = {}
269-
for key, title, subtitle in [
270-
(
271-
"distortion_k1",
272-
_("Radial 1 (k1)"),
273-
_("First order radial distortion"),
274-
),
275-
(
276-
"distortion_k2",
277-
_("Radial 2 (k2)"),
278-
_("Second order radial distortion"),
279-
),
280-
(
281-
"distortion_k3",
282-
_("Radial 3 (k3)"),
283-
_("Third order radial distortion"),
284-
),
285-
(
286-
"distortion_p1",
287-
_("Tangential 1 (p1)"),
288-
_("First order tangential distortion"),
289-
),
290-
(
291-
"distortion_p2",
292-
_("Tangential 2 (p2)"),
293-
_("Second order tangential distortion"),
294-
),
295-
]:
296-
row = self._create_spin_row(
297-
title, subtitle, getattr(self.camera, key), key
298-
)
299-
self._distortion_rows[key] = row
300-
calibration_group.add(row)
301-
302251
self.camera.settings_changed.connect(self._on_camera_settings_changed)
303252
self.controller.resolutions_probed.connect(self._on_resolutions_probed)
304253

305254
def _on_camera_settings_changed(self, camera):
306-
for key, row in self._distortion_rows.items():
307-
row.set_value(getattr(camera, key))
255+
pass
308256

309257
def _on_resolution_changed(self, combo_row, pspec):
310258
if self._updating_ui:
@@ -405,27 +353,6 @@ def _create_slider_row(
405353
on_value_changed=callback,
406354
)
407355

408-
def _create_spin_row(
409-
self, title: str, subtitle: str, value: float, config_key: str
410-
) -> Adw.SpinRow:
411-
row = Adw.SpinRow(
412-
title=title,
413-
subtitle=subtitle,
414-
adjustment=Gtk.Adjustment(
415-
value=value,
416-
lower=-10.0,
417-
upper=10.0,
418-
step_increment=0.001,
419-
page_increment=0.01,
420-
),
421-
digits=4,
422-
numeric=True,
423-
)
424-
row.connect(
425-
"notify::value", self._on_distortion_value_changed, config_key
426-
)
427-
return row
428-
429356
def on_white_balance_changed(self, scale):
430357
if not self.auto_white_balance_switch.get_active():
431358
self.camera.white_balance = scale.get_value()
@@ -456,21 +383,6 @@ def on_denoise_changed(self, scale):
456383
def on_transparency_changed(self, scale):
457384
self.camera.transparency = scale.get_value()
458385

459-
def _on_distortion_value_changed(
460-
self, spin_row: Adw.SpinRow, pspec, config_key: str
461-
):
462-
setattr(self.camera, config_key, spin_row.get_value())
463-
464-
def _on_wizard_clicked(self, button):
465-
from .calibration_wizard import CalibrationWizard
466-
467-
window = self.get_ancestor(Gtk.Window)
468-
if not isinstance(window, Gtk.Window):
469-
return
470-
471-
wizard = CalibrationWizard(window, self.controller)
472-
wizard.present()
473-
474386
def do_close_request(self, *args) -> bool:
475387
logger.debug(
476388
f"CameraImageSettingsDialog closing for camera {self.camera.name}"

0 commit comments

Comments
 (0)