fix(v5): default fft spectrum to log magnitude

This commit is contained in:
2026-07-09 20:01:04 +08:00
parent 428b9a3e7f
commit 38faff0ed0
4 changed files with 14 additions and 26 deletions

View File

@@ -86,7 +86,7 @@ The app is organized as a small MATLAB-like image workspace. Each operation crea
### Chapter 4: Frequency Domain ### Chapter 4: Frequency Domain
- **FFT/DFT spectrum view** shows magnitude, log magnitude, or phase of the image in the frequency domain. Formula: `F(u,v) = DFT{f(x,y)}`. Apply it to a periodic-noisy state with `log_magnitude` to see the noise peaks. - **FFT/DFT spectrum view** shows the log magnitude of the image in the frequency domain. Formula: `log(1 + |fftshift(fft2(f))|)`. Apply it to a periodic-noisy state to see the noise peaks.
- **Inverse FFT reconstruction** reconstructs from a previously saved FFT state. Formula: `f = real(ifft2(ifftshift(F)))`. - **Inverse FFT reconstruction** reconstructs from a previously saved FFT state. Formula: `f = real(ifft2(ifftshift(F)))`.
### Chapter 6: RGB Color Processing ### Chapter 6: RGB Color Processing

View File

@@ -354,19 +354,14 @@ def rgb_channel(image, params):
def fft_spectrum(image, params): def fft_spectrum(image, params):
"""Display the DFT magnitude, log magnitude, or phase spectrum of an image. """Display the DFT log-magnitude spectrum of an image.
Use it to understand whether image information is concentrated in low or high frequencies. Use it to understand whether image information is concentrated in low or high frequencies.
""" """
gray = to_gray(image).astype(np.float32) gray = to_gray(image).astype(np.float32)
spectrum = np.fft.fftshift(np.fft.fft2(gray)) spectrum = np.fft.fftshift(np.fft.fft2(gray))
mode = params.get("mode", "log_magnitude") magnitude = np.log1p(np.abs(spectrum))
if mode == "phase":
return gray_to_rgb(normalize_to_uint8(np.angle(spectrum)))
magnitude = np.abs(spectrum)
if mode == "log_magnitude":
magnitude = np.log1p(magnitude)
return gray_to_rgb(normalize_to_uint8(magnitude)) return gray_to_rgb(normalize_to_uint8(magnitude))
@@ -436,7 +431,7 @@ OPERATIONS = [
kernel_pair_preview("Sobel", [[-1, -2, -1], [0, 0, 0], [1, 2, 1]], [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]), kernel_pair_preview("Sobel", [[-1, -2, -1], [0, 0, 0], [1, 2, 1]], [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]),
], formula="Gradient image = abs(imfilter(f,Gx)) + abs(imfilter(f,Gy))."), ], formula="Gradient image = abs(imfilter(f,Gx)) + abs(imfilter(f,Gy))."),
operation("high_boost", "High-Boost / Edge Emphasis", CH3, "High-Boost Filtering", high_boost_slide, {"A": float_param(1.5, 1, 6, 0.1, description="Boost factor A, where A >= 1."), "K": odd_param(3, 35, description="Odd averaging mask size used for the blurred image.")}, formula="f_hb = A f - blurred(f)."), operation("high_boost", "High-Boost / Edge Emphasis", CH3, "High-Boost Filtering", high_boost_slide, {"A": float_param(1.5, 1, 6, 0.1, description="Boost factor A, where A >= 1."), "K": odd_param(3, 35, description="Odd averaging mask size used for the blurred image.")}, formula="f_hb = A f - blurred(f)."),
operation("fft_spectrum", "FFT/DFT Spectrum View", CH4, "DFT and FFT", fft_spectrum, {"mode": select_param("log_magnitude", ["magnitude", "log_magnitude", "phase"], description="Choose magnitude, log magnitude, or phase display.")}, formula="F(u,v) = DFT{f(x,y)}", repeatable=False), operation("fft_spectrum", "FFT/DFT Spectrum View", CH4, "DFT and FFT", fft_spectrum, formula="Display log(1 + |fftshift(fft2(f))|).", repeatable=False),
operation("inverse_fft_reconstruction", "Inverse FFT Reconstruction", CH4, "DFT and FFT", inverse_fft_reconstruction, formula="f = real(ifft2(ifftshift(F))). Apply this to an FFT/DFT Spectrum View state.", repeatable=False), operation("inverse_fft_reconstruction", "Inverse FFT Reconstruction", CH4, "DFT and FFT", inverse_fft_reconstruction, formula="f = real(ifft2(ifftshift(F))). Apply this to an FFT/DFT Spectrum View state.", repeatable=False),
operation("rgb_to_gray", "Convert to Grayscale", CH6, "Color Conversion", rgb_to_gray_matlab, {"red_weight": float_param(0.299, 0, 1, 0.001, description="R coefficient in gray = aR + bG + cB."), "green_weight": float_param(0.587, 0, 1, 0.001, description="G coefficient in gray = aR + bG + cB."), "blue_weight": float_param(0.114, 0, 1, 0.001, description="B coefficient in gray = aR + bG + cB.")}, formula="gray = 0.299R + 0.587G + 0.114B by default.", repeatable=False), operation("rgb_to_gray", "Convert to Grayscale", CH6, "Color Conversion", rgb_to_gray_matlab, {"red_weight": float_param(0.299, 0, 1, 0.001, description="R coefficient in gray = aR + bG + cB."), "green_weight": float_param(0.587, 0, 1, 0.001, description="G coefficient in gray = aR + bG + cB."), "blue_weight": float_param(0.114, 0, 1, 0.001, description="B coefficient in gray = aR + bG + cB.")}, formula="gray = 0.299R + 0.587G + 0.114B by default.", repeatable=False),
operation("rgb_channel", "RGB Channel View", CH6, "RGB color model", rgb_channel, {"channel": select_param("r", ["r", "g", "b"], description="Select the RGB channel to view.")}, formula="Show one RGB channel as grayscale.", repeatable=False), operation("rgb_channel", "RGB Channel View", CH6, "RGB color model", rgb_channel, {"channel": select_param("r", ["r", "g", "b"], description="Select the RGB channel to view.")}, formula="Show one RGB channel as grayscale.", repeatable=False),

View File

@@ -223,15 +223,12 @@ def image_state_fft_spectrum_create(*, state, params):
""" """
source = load_image_array(state.image) source = load_image_array(state.image)
mode = params.get("mode", "log_magnitude")
if mode not in {"magnitude", "log_magnitude", "phase"}:
raise ProcessingError("FFT mode must be magnitude, log_magnitude, or phase.")
spectrum = image_fft(source) spectrum = image_fft(source)
preview = fft_preview_image(spectrum, mode) preview = fft_preview_image(spectrum)
fft_data_path = save_fft_array(spectrum, "state-fft-data") fft_data_path = save_fft_array(spectrum, "state-fft-data")
operation_params = { operation_params = {
**params, **(params or {}),
"mode": mode, "mode": "log_magnitude",
"fft_data_path": fft_data_path, "fft_data_path": fft_data_path,
"source_state_id": str(state.id), "source_state_id": str(state.id),
"source_color_mode": "RGB" if source.ndim == 3 else "L", "source_color_mode": "RGB" if source.ndim == 3 else "L",
@@ -276,15 +273,10 @@ def image_fft(image):
return np.stack(channels, axis=2) return np.stack(channels, axis=2)
def fft_preview_image(spectrum, mode): def fft_preview_image(spectrum):
"""Convert complex FFT data to a display-only uint8 preview image.""" """Convert complex FFT data to a display-only log-magnitude uint8 preview image."""
if mode == "phase": preview = np.log1p(np.abs(spectrum))
preview = np.angle(spectrum)
else:
preview = np.abs(spectrum)
if mode == "log_magnitude":
preview = np.log1p(preview)
return normalize_to_uint8(preview) return normalize_to_uint8(preview)

View File

@@ -83,6 +83,7 @@ class ApiTests(TestCase):
self.assertEqual(operations["periodic_noise"]["params"]["T"]["default"], 100) self.assertEqual(operations["periodic_noise"]["params"]["T"]["default"], 100)
self.assertEqual(operations["inverse_fft_reconstruction"]["label"], "Inverse FFT Reconstruction") self.assertEqual(operations["inverse_fft_reconstruction"]["label"], "Inverse FFT Reconstruction")
self.assertFalse(operations["inverse_fft_reconstruction"]["repeatable"]) self.assertFalse(operations["inverse_fft_reconstruction"]["repeatable"])
self.assertEqual(operations["fft_spectrum"]["params"], {})
self.assertEqual(operations["box_filter"]["label"], "Average / Box Filter") self.assertEqual(operations["box_filter"]["label"], "Average / Box Filter")
self.assertEqual(operations["gaussian_filter"]["label"], "Gaussian Filter") self.assertEqual(operations["gaussian_filter"]["label"], "Gaussian Filter")
self.assertFalse(operations["negative"]["repeatable"]) self.assertFalse(operations["negative"]["repeatable"])
@@ -312,7 +313,7 @@ class ApiTests(TestCase):
s0_id = upload.data["states"][0]["state_id"] s0_id = upload.data["states"][0]["state_id"]
spectrum = self.client.post( spectrum = self.client.post(
f"/api/states/{s0_id}/operations/", f"/api/states/{s0_id}/operations/",
{"operation": "fft_spectrum", "params": {"mode": "log_magnitude"}}, {"operation": "fft_spectrum", "params": {}},
format="json", format="json",
) )
@@ -327,7 +328,7 @@ class ApiTests(TestCase):
s0_id = upload.data["states"][0]["state_id"] s0_id = upload.data["states"][0]["state_id"]
spectrum = self.client.post( spectrum = self.client.post(
f"/api/states/{s0_id}/operations/", f"/api/states/{s0_id}/operations/",
{"operation": "fft_spectrum", "params": {"mode": "log_magnitude"}}, {"operation": "fft_spectrum", "params": {}},
format="json", format="json",
) )
response = self.client.post( response = self.client.post(
@@ -347,7 +348,7 @@ class ApiTests(TestCase):
s0_id = upload.data["states"][0]["state_id"] s0_id = upload.data["states"][0]["state_id"]
spectrum = self.client.post( spectrum = self.client.post(
f"/api/states/{s0_id}/operations/", f"/api/states/{s0_id}/operations/",
{"operation": "fft_spectrum", "params": {"mode": "log_magnitude"}}, {"operation": "fft_spectrum", "params": {}},
format="json", format="json",
) )
response = self.client.post( response = self.client.post(