radon-brush / app.py
fr1ll's picture
Set npx to nh why not
e6bb3be verified
import gradio as gr
import numpy as np
from pylops.signalprocessing import Radon2D
from typing import Literal
def compute_radon_fn(image,
kind: Literal["linear", "hyperbolic", "parabolic"] = "linear"):
# Convert image to grayscale if needed.
if image.ndim == 3:
image_gray = np.mean(image, axis=2)
else:
image_gray = image
nt, nh = image_gray.shape
# Define axes centered around zero.
taxis = np.linspace(-nt/2, nt/2, nt) # time axis (rows)
haxis = np.linspace(-nh/2, nh/2, nh) # spatial axis (columns)
# Set detector axis (pxaxis) to cover the full image diagonal.
# npx = int(np.ceil(np.sqrt(nt**2 + nh**2)))
npx = nh # why?
pxaxis = np.linspace(-npx/2, npx/2, npx)
print(f"Shapes:\n taxis:{taxis.shape}, haxis:{haxis.shape}, pxaxis:{pxaxis.shape}")
# Create the Radon2D operator with engine 'numba' and centeredh=True.
R = Radon2D(taxis, haxis, pxaxis,
kind=kind, centeredh=True, interp=True,
onthefly=False, engine='numpy', dtype='float64', name='R')
# Compute the forward radon transform.
radon_data_flat = R.dot(image_gray.flatten())
# Deduce the number of projections from the operator shape.
nproj = R.shape[0] // npx
# Reshape to (nproj, L) so each row corresponds to one projection.
radon_data = radon_data_flat.reshape((nproj, npx))
# Transpose for display: p (detector coordinate) on x-axis, τ on y-axis.
radon_display = radon_data.T
# Normalize for display.
radon_display = (radon_display - radon_display.min()) / (radon_display.max() - radon_display.min() + 1e-8)
# Save the state including the radon data and operator for inverse computation.
state = {
"radon_data": radon_data, # shape: (nproj, L)
"image_shape": image_gray.shape,
"R": R, # the Radon2D operator
"L": npx # detector length
}
return radon_display, state
def apply_mask_and_inverse_fn(state, mask_image):
if mask_image is None:
return None
# Ensure mask is single-channel.
if mask_image.ndim == 3:
mask_image = mask_image[..., 0]
radon_data = state["radon_data"]
image_shape = tuple(state["image_shape"])
L = state["L"]
# The displayed radon image was transposed, so transpose mask back.
mask = mask_image.T
# Create binary mask: painted pixels (value > 0.5) become 1.
mask_binary = (mask > 0.5).astype(float)
# Apply the mask: zero-out masked pixels in the radon data.
radon_masked = radon_data * (1 - mask_binary)
# Reconstruct the image using the adjoint (transpose) of the operator.
R = state["R"]
rec_flat = R.T.dot(radon_masked.flatten())
rec = rec_flat.reshape(image_shape)
# Normalize reconstruction for display.
rec_norm = (rec - rec.min()) / (rec.max() - rec.min() + 1e-8)
return rec_norm
with gr.Blocks() as demo:
gr.Markdown("## Radon Transform with Interactive Masking (Using PyLops Radon2D)")
with gr.Row():
image_input = gr.Image(label="Input Image", type="numpy")
compute_button = gr.Button("Compute Radon")
radon_output = gr.Image(label="Radon Transform Image", interactive=False, type="numpy")
# Use the updated ImageEditor component for interactive masking.
radon_drawing = gr.ImageEditor(label="Paint on Radon (mask out pixels)", type="numpy")
inverse_output = gr.Image(label="Reconstructed Image", interactive=False, type="numpy")
# State to hold radon data and operator.
state = gr.State()
# When "Compute Radon" is clicked, compute the radon transform.
compute_button.click(
fn=compute_radon_fn,
inputs=[image_input],
outputs=[radon_output, state]
)
# When the user edits (paints) the radon image, apply the mask and compute the inverse.
radon_drawing.change(
fn=apply_mask_and_inverse_fn,
inputs=[state, radon_drawing],
outputs=[inverse_output]
)
gr.Markdown(
"**Instructions:** Upload an image and click 'Compute Radon' to compute the radon transform using PyLops’ Radon2D (with engine 'numba' and centeredh=True). Then use the paintbrush tool to mask parts of the radon image and see the resulting reconstruction."
)
demo.launch()