From 05385fce2ac749dd7a88246a1ac81fcdac475fe3 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 22 Apr 2026 07:31:45 +0100 Subject: [PATCH] Constrain phase reranking to nearby sizes --- src/repixelizer/params.py | 1 + src/repixelizer/pipeline.py | 14 +++++++++++ tests/test_pipeline.py | 46 ++++++++++++++++++++++++++++++++----- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/src/repixelizer/params.py b/src/repixelizer/params.py index b1bd69a..c2861e1 100644 --- a/src/repixelizer/params.py +++ b/src/repixelizer/params.py @@ -39,6 +39,7 @@ class SolverHyperParams: phase_rerank_edge_concentration_weight: float = 0.10 phase_rerank_inference_penalty_weight: float = 0.05 phase_rerank_confidence_threshold: float = 0.12 + phase_rerank_max_size_delta_ratio: float = 0.15 phase_rerank_margin: float = 0.004 def to_dict(self) -> dict[str, float]: diff --git a/src/repixelizer/pipeline.py b/src/repixelizer/pipeline.py index 0a3ebd9..773b83d 100644 --- a/src/repixelizer/pipeline.py +++ b/src/repixelizer/pipeline.py @@ -167,6 +167,7 @@ def _select_phase_candidate( "stroke_wobble_error": foreground_stroke_wobble_error(preview, source), "edge_concentration": foreground_edge_concentration(candidate_artifacts.target_rgba), "inference_penalty": top_score - float(candidate.score), + "size_delta_ratio": _size_delta_ratio(inference, candidate_inference), } ) @@ -186,6 +187,13 @@ def _select_phase_candidate( best_rank = float("inf") best_candidate = inference for index, record in enumerate(candidate_records): + candidate_inference = record["inference"] + size_delta_ratio = float(record["size_delta_ratio"]) + if ( + candidate_inference.target_width != inference.target_width + or candidate_inference.target_height != inference.target_height + ) and size_delta_ratio > solver_params.phase_rerank_max_size_delta_ratio: + continue rank = ( solver_params.phase_rerank_support_weight * support_penalty[index] + solver_params.phase_rerank_edge_position_weight * edge_position_penalty[index] @@ -215,3 +223,9 @@ def _normalize_penalty(values, *, higher_is_better: bool = False) -> list[float] if higher_is_better: normalized = 1.0 - normalized return normalized.astype(np.float32).tolist() + + +def _size_delta_ratio(a: InferenceResult, b: InferenceResult) -> float: + width_ratio = abs(b.target_width - a.target_width) / max(1.0, float(a.target_width)) + height_ratio = abs(b.target_height - a.target_height) / max(1.0, float(a.target_height)) + return max(width_ratio, height_ratio) diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py index 017c4e4..ed7e91b 100644 --- a/tests/test_pipeline.py +++ b/tests/test_pipeline.py @@ -147,12 +147,12 @@ def test_phase_rerank_can_override_low_confidence_inference_pick(monkeypatch) -> def test_phase_rerank_can_override_to_better_size_candidate(monkeypatch) -> None: - source = np.zeros((4, 4, 4), dtype=np.float32) - candidate_a = InferenceCandidate(target_width=2, target_height=2, phase_x=0.0, phase_y=0.0, score=0.91, breakdown={}) - candidate_b = InferenceCandidate(target_width=3, target_height=3, phase_x=0.0, phase_y=0.0, score=0.88, breakdown={}) + source = np.zeros((16, 16, 4), dtype=np.float32) + candidate_a = InferenceCandidate(target_width=16, target_height=16, phase_x=0.0, phase_y=0.0, score=0.91, breakdown={}) + candidate_b = InferenceCandidate(target_width=18, target_height=18, phase_x=0.0, phase_y=0.0, score=0.88, breakdown={}) inference = InferenceResult( - target_width=2, - target_height=2, + target_width=16, + target_height=16, phase_x=0.0, phase_y=0.0, confidence=0.0, @@ -168,7 +168,7 @@ def test_phase_rerank_can_override_to_better_size_candidate(monkeypatch) -> None return DummyArtifacts(rgba) def fake_support(source_rgba, output_rgba, *, target_width, target_height, phase_x, phase_y): - if target_width == 2: + if target_width == 16: return {"score": 0.10} return {"score": 0.01} @@ -180,6 +180,40 @@ def test_phase_rerank_can_override_to_better_size_candidate(monkeypatch) -> None assert selected.target_height == candidate_b.target_height +def test_phase_rerank_rejects_large_size_jump(monkeypatch) -> None: + source = np.zeros((16, 16, 4), dtype=np.float32) + candidate_a = InferenceCandidate(target_width=16, target_height=16, phase_x=0.0, phase_y=0.0, score=0.91, breakdown={}) + candidate_b = InferenceCandidate(target_width=24, target_height=24, phase_x=0.0, phase_y=0.0, score=0.88, breakdown={}) + inference = InferenceResult( + target_width=16, + target_height=16, + phase_x=0.0, + phase_y=0.0, + confidence=0.0, + top_candidates=[candidate_a, candidate_b], + ) + + class DummyArtifacts: + def __init__(self, rgba: np.ndarray) -> None: + self.target_rgba = rgba + + def fake_optimize_uv_field(source_rgba, inference, analysis, steps, seed, device, solver_params=None): + rgba = np.zeros((inference.target_height, inference.target_width, 4), dtype=np.float32) + return DummyArtifacts(rgba) + + def fake_support(source_rgba, output_rgba, *, target_width, target_height, phase_x, phase_y): + if target_width == 16: + return {"score": 0.10} + return {"score": 0.01} + + monkeypatch.setattr("repixelizer.pipeline.optimize_uv_field", fake_optimize_uv_field) + monkeypatch.setattr("repixelizer.pipeline.source_lattice_consistency_breakdown", fake_support) + + selected = _select_phase_candidate(source, inference, analysis=object(), seed=7, device="cpu") + assert selected.target_width == candidate_a.target_width + assert selected.target_height == candidate_a.target_height + + def test_phase_rerank_can_prefer_better_line_metrics(monkeypatch) -> None: source = np.zeros((4, 4, 4), dtype=np.float32) candidate_a = InferenceCandidate(target_width=2, target_height=2, phase_x=-0.2, phase_y=0.0, score=0.91, breakdown={})