fix(v5): reconstruct inverse fft from saved spectrum
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user