From 428b9a3e7f150734c1be6f489156e45edaacdfd7 Mon Sep 17 00:00:00 2001 From: Amirhossein Khalili Date: Thu, 9 Jul 2026 19:55:26 +0800 Subject: [PATCH] fix(v5): average noisy copies from clean source --- backend/processing/services.py | 36 ++++++++++++++++++++++++++++ backend/processing/tests/test_api.py | 23 ++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/backend/processing/services.py b/backend/processing/services.py index da90ccc..fac456e 100644 --- a/backend/processing/services.py +++ b/backend/processing/services.py @@ -165,6 +165,8 @@ def image_state_apply_operation(*, state, operation, params): return image_state_fft_spectrum_create(state=state, params=params or {}) if operation == "inverse_fft_reconstruction": return image_state_inverse_fft_create(state=state, params=params or {}) + if operation == "average_noisy_copies": + return image_state_average_noisy_copies_create(state=state, params=params or {}) source = load_image_array(state.image) result = apply_registered_operation(source, operation, params or {}) @@ -180,6 +182,40 @@ def image_state_apply_operation(*, state, operation, params): return image_state_payload(state=new_state, include_image=True) +def image_state_average_noisy_copies_create(*, state, params): + """Create averaged noisy copies from the clean source behind a Gaussian-noise state. + + If the active state is already a Gaussian-noisy image, the averaging must regenerate independent + noisy copies from its parent image, not from the already-noisy pixels. + """ + + source_state = state + if ( + state.operation == "noise_filter" + and isinstance(state.params, dict) + and state.params.get("kind", "gaussian") == "gaussian" + and state.parent is not None + ): + source_state = state.parent + + operation_params = dict(params or {}) + if source_state.id != state.id: + operation_params["source_state_id"] = str(source_state.id) + + source = load_image_array(source_state.image) + result = apply_registered_operation(source, "average_noisy_copies", operation_params) + new_state = image_state_create( + session=state.session, + parent=state, + image=result, + operation="average_noisy_copies", + params=operation_params, + label=None, + prefix="state-average_noisy_copies", + ) + return image_state_payload(state=new_state, include_image=True) + + def image_state_fft_spectrum_create(*, state, params): """Create an FFT visualization state and persist the actual complex spectrum. diff --git a/backend/processing/tests/test_api.py b/backend/processing/tests/test_api.py index cf67d1c..2a9348f 100644 --- a/backend/processing/tests/test_api.py +++ b/backend/processing/tests/test_api.py @@ -233,6 +233,29 @@ class ApiTests(TestCase): self.assertEqual(result.ndim, 2) self.assertEqual(int(result[0, 0]), 96) + def test_average_noisy_copies_uses_clean_parent_after_gaussian_noise_state(self): + upload = self.client.post("/api/images/", {"image": grayscale_png_upload(value=96)}, format="multipart") + s0_id = upload.data["states"][0]["state_id"] + noisy = self.client.post( + f"/api/states/{s0_id}/operations/", + {"operation": "noise_filter", "params": {"kind": "gaussian", "mean": 0.2, "variance": 0}}, + format="json", + ) + noisy_image = load_image_array(ImageState.objects.get(id=noisy.data["state_id"]).image) + self.assertGreater(int(noisy_image[0, 0]), 96) + + averaged = self.client.post( + f"/api/states/{noisy.data['state_id']}/operations/", + {"operation": "average_noisy_copies", "params": {"N": 10, "kind": "gaussian", "mean": 0, "variance": 0}}, + format="json", + ) + + self.assertEqual(averaged.status_code, 201) + self.assertEqual(averaged.data["parent_state_id"], noisy.data["state_id"]) + self.assertEqual(averaged.data["params"]["source_state_id"], s0_id) + result = load_image_array(ImageState.objects.get(id=averaged.data["state_id"]).image) + self.assertEqual(int(result[0, 0]), 96) + def test_periodic_noise_preserves_grayscale_and_zero_amplitude(self): upload = self.client.post("/api/images/", {"image": grayscale_png_upload(value=96)}, format="multipart") s0_id = upload.data["states"][0]["state_id"]