Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions negpy/desktop/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@
remap_workspace_config,
remember_split_scans,
saved_crop_rect,
slice_chain,
split_scans,
)
from negpy.services.export.templating import path_safe, render_export_filename
Expand Down Expand Up @@ -3663,9 +3664,16 @@ def auto_skew_frame(self) -> None:
config = self.state.config
geo = config.geometry
# Every detection path reads the flat-fielded source.
source = (
raw if metadata_lens_corrections(config) else apply_flatfield(raw, config.flatfield, orientation=self.state.preview_orientation)
)
if config.stitch.stitch_enabled or metadata_lens_corrections(config):
source = raw
else:
cuts = self._preview_gain_slices(self.active_diptych())
source = apply_flatfield(
raw,
config.flatfield,
(lambda g: slice_chain(g, cuts)) if cuts else None,
orientation=self.state.preview_orientation,
)
QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
try:
fit = squared_geometry(source, geo, config.process.process_mode)
Expand Down
15 changes: 12 additions & 3 deletions negpy/desktop/workers/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -1627,9 +1627,18 @@ def _frame_evidence(
return None, None

config = frame.config
corrected = (
raw if metadata_lens_corrections(config) else apply_flatfield(raw, config.flatfield, orientation=meta.get("orientation", 1))
)
if config.stitch.stitch_enabled or metadata_lens_corrections(config):
corrected = raw
else:
from negpy.services.assets.half_frame import slice_chain

cuts = gain_slices_for_asset(file_info)
corrected = apply_flatfield(
raw,
config.flatfield,
(lambda g: slice_chain(g, cuts)) if cuts else None,
orientation=meta.get("orientation", 1),
)
geometry = config.geometry
keystone = None
if squares_on_arm(geometry):
Expand Down
34 changes: 33 additions & 1 deletion tests/test_batch_autocrop_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,9 @@ def test_batch_autocrop_applies_flatfield_and_crop_free_geometry(qapp, monkeypat
worker = BatchAutoCropWorker(preview)
captured: dict = {}

def _apply_flatfield(image, received_config, orientation=1):
def _apply_flatfield(image, received_config, gain_crop=None, orientation=1):
captured["flatfield_config"] = received_config
captured["gain_crop"] = gain_crop
captured["orientation"] = orientation
return image + 1.0

Expand Down Expand Up @@ -189,6 +190,7 @@ def _detect(key, transformed, *, target_ratio, rebate_trim=1.0):

assert captured["flatfield_config"] == flatfield
assert captured["orientation"] == 6
assert captured["gain_crop"] is None
assert captured["geometry"].crop_rect is None
assert captured["geometry"].crop_from_auto is False
assert captured["geometry"].autocrop_offset == 0
Expand All @@ -204,6 +206,36 @@ def _detect(key, transformed, *, target_ratio, rebate_trim=1.0):
assert captured["context"].scale_factor == 1.0


def _flatfield_config() -> WorkspaceConfig:
base = WorkspaceConfig()
return replace(base, flatfield=replace(base.flatfield, apply=True, profile_id="rig-1"))


def test_batch_autocrop_cuts_the_flatfield_gain_to_the_half(qapp, monkeypatch) -> None:
_stub_detection(monkeypatch)
crops: list = []
monkeypatch.setattr(render_workers, "apply_flatfield", lambda image, _config, gain_crop=None, **_kw: crops.append(gain_crop) or image)
frame = _input("frame", _flatfield_config())
frame.file_info.update(half=2, split_x=0.5, gutter_thickness=0.0)

BatchAutoCropWorker(_PreviewService()).process(_task(frame))

assert crops[0] is not None
assert crops[0](np.zeros((8, 12, 3), dtype=np.float32)).shape == (8, 6, 3)


def test_batch_autocrop_skips_the_flatfield_on_a_stitch(qapp, monkeypatch) -> None:
_stub_detection(monkeypatch)
calls: list = []
monkeypatch.setattr(render_workers, "apply_flatfield", lambda image, *_args, **_kwargs: calls.append(1) or image)
config = _flatfield_config()
config = replace(config, stitch=replace(config.stitch, stitch_enabled=True))

BatchAutoCropWorker(_PreviewService()).process(_task(_input("frame", config)))

assert calls == []


def test_batch_autocrop_per_file_failure_does_not_abort_roll(qapp, monkeypatch) -> None:
base = WorkspaceConfig()

Expand Down
48 changes: 48 additions & 0 deletions tests/test_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,54 @@ def test_auto_skew_measures_the_flat_fielded_source(self):
bake.assert_called_once()
self.assertEqual(float(fit.call_args.args[0].mean()), 0.5)

def test_auto_skew_cuts_the_flatfield_gain_to_the_active_half(self):
self.controller.state.preview_raw = np.zeros((100, 60, 3), dtype=np.float32)
self.controller.request_render = MagicMock()
self.controller.set_status = MagicMock()
half = (2, 0.5, None, 0.0, "x")

with (
patch.object(self.controller, "active_diptych", return_value=None),
patch.object(self.controller, "_active_half", return_value=half),
patch("negpy.desktop.controller.apply_flatfield", side_effect=lambda image, *_, **__: image) as bake,
patch("negpy.features.geometry.skew.trusted_frame_skew", return_value=None),
):
self.controller.auto_skew_frame()

gain_crop = bake.call_args.args[2]
self.assertEqual(gain_crop(np.zeros((100, 120, 3), dtype=np.float32)).shape, (100, 60, 3))

def test_auto_skew_cuts_the_flatfield_gain_to_the_diptych_crop(self):
self.controller.state.preview_raw = np.zeros((100, 60, 3), dtype=np.float32)
self.controller.request_render = MagicMock()
self.controller.set_status = MagicMock()
dip = ({"split_x": 0.5, "crop_rect": (0.0, 0.0, 0.5, 1.0), "gutter_thickness": 0.0, "split_axis": "x"}, None)

with (
patch.object(self.controller, "active_diptych", return_value=dip),
patch("negpy.desktop.controller.apply_flatfield", side_effect=lambda image, *_, **__: image) as bake,
patch("negpy.features.geometry.skew.trusted_frame_skew", return_value=None),
):
self.controller.auto_skew_frame()

gain_crop = bake.call_args.args[2]
self.assertEqual(gain_crop(np.zeros((100, 120, 3), dtype=np.float32)).shape, (100, 60, 3))

def test_auto_skew_skips_the_flatfield_on_a_stitch(self):
config = self.controller.state.config
self.controller.state.config = replace(config, stitch=replace(config.stitch, stitch_enabled=True))
self.controller.state.preview_raw = np.zeros((100, 120, 3), dtype=np.float32)
self.controller.request_render = MagicMock()
self.controller.set_status = MagicMock()

with (
patch("negpy.desktop.controller.apply_flatfield") as bake,
patch("negpy.features.geometry.skew.trusted_frame_skew", return_value=None),
):
self.controller.auto_skew_frame()

bake.assert_not_called()

def test_auto_skew_failure_reports_and_changes_nothing(self):
self.controller.state.preview_raw = np.zeros((100, 120, 3), dtype=np.float32)
self.controller.request_render = MagicMock()
Expand Down
Loading