fix(v5): reconstruct inverse fft from saved spectrum

This commit is contained in:
2026-07-09 15:01:47 +03:30
parent ca447c71cc
commit 30e1d250ef
5 changed files with 157 additions and 18 deletions

View File

@@ -9,7 +9,7 @@ from django.utils import timezone
from .algorithms import ProcessingError, average_images, decode_image, histogram, histogram_payload, normalize_to_uint8, process_image, verify_registration
from .models import ImageSession, ImageState, ProcessingJob
from .registry import apply_registered_operation
from .storage import delete_relative_file, load_image_array, payload_for_image, save_image_array
from .storage import delete_relative_file, load_fft_array, load_image_array, payload_for_image, save_fft_array, save_image_array
from .tasks import run_batch_job
@@ -145,9 +145,11 @@ def image_state_delete(*, state):
if state.sequence == 0 or state.operation == "upload":
raise ProcessingError("The original S0 upload state cannot be deleted.")
image_path = state.image
fft_data_path = state.params.get("fft_data_path") if isinstance(state.params, dict) else None
state.children.update(parent=None)
state.delete()
delete_relative_file(image_path)
delete_relative_file(fft_data_path)
def image_state_apply_operation(*, state, operation, params):
@@ -158,6 +160,12 @@ def image_state_apply_operation(*, state, operation, params):
if state.session.expired:
raise ProcessingError("Image session has expired.")
if operation == "fft_spectrum":
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 {})
source = load_image_array(state.image)
result = apply_registered_operation(source, operation, params or {})
new_state = image_state_create(
@@ -172,6 +180,91 @@ def image_state_apply_operation(*, state, operation, params):
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.
The displayed image is only a spectrum preview; the saved FFT data is what the inverse step uses.
"""
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)
preview = fft_preview_image(spectrum, mode)
fft_data_path = save_fft_array(spectrum, "state-fft-data")
operation_params = {
**params,
"mode": mode,
"fft_data_path": fft_data_path,
"source_state_id": str(state.id),
"source_color_mode": "RGB" if source.ndim == 3 else "L",
}
new_state = image_state_create(
session=state.session,
parent=state,
image=preview,
operation="fft_spectrum",
params=operation_params,
label=None,
prefix="state-fft_spectrum",
)
return image_state_payload(state=new_state, include_image=True)
def image_state_inverse_fft_create(*, state, params):
"""Create an image state by applying inverse FFT to a previously saved FFT state."""
if state.operation != "fft_spectrum" or not isinstance(state.params, dict) or not state.params.get("fft_data_path"):
raise ProcessingError("Inverse FFT Reconstruction must be applied to an FFT/DFT Spectrum View state.")
spectrum = load_fft_array(state.params["fft_data_path"])
result = inverse_fft_image(spectrum)
new_state = image_state_create(
session=state.session,
parent=state,
image=result,
operation="inverse_fft_reconstruction",
params=params or {},
label=None,
prefix="state-inverse_fft_reconstruction",
)
return image_state_payload(state=new_state, include_image=True)
def image_fft(image):
"""Return centered DFT data for grayscale or per-channel RGB images."""
if image.ndim == 2:
return np.fft.fftshift(np.fft.fft2(image.astype(np.float32)))
channels = [np.fft.fftshift(np.fft.fft2(image[:, :, idx].astype(np.float32))) for idx in range(image.shape[2])]
return np.stack(channels, axis=2)
def fft_preview_image(spectrum, mode):
"""Convert complex FFT data to a display-only uint8 preview image."""
if mode == "phase":
preview = np.angle(spectrum)
else:
preview = np.abs(spectrum)
if mode == "log_magnitude":
preview = np.log1p(preview)
return normalize_to_uint8(preview)
def inverse_fft_image(spectrum):
"""Apply ifft2(ifftshift(F)) to stored complex FFT data."""
def reconstruct_channel(channel):
reconstructed = np.real(np.fft.ifft2(np.fft.ifftshift(channel)))
return np.round(np.clip(reconstructed, 0, 255)).astype(np.uint8)
if spectrum.ndim == 2:
return reconstruct_channel(spectrum)
channels = [reconstruct_channel(spectrum[:, :, idx]) for idx in range(spectrum.shape[2])]
return np.stack(channels, axis=2)
def combine_states(*, states, operation, params=None):
"""Combine registered states using add, subtract, dot product, average, and/or.