fr1ll commited on
Commit
542be45
·
verified ·
1 Parent(s): 834879d

New copypasta

Browse files
Files changed (1) hide show
  1. app.py +54 -68
app.py CHANGED
@@ -2,112 +2,98 @@ import gradio as gr
2
  import numpy as np
3
  from pylops.signalprocessing import Radon2D
4
 
5
- def compute_radon_fn(image, num_angles, angle_min, angle_max):
6
- """
7
- Given an input image and radon parameters, compute the radon transform.
8
- The output sinogram is reshaped and transposed so that the horizontal axis corresponds to p values,
9
- and the vertical axis corresponds to the tau (angle index).
10
- A state dictionary is returned with the original sinogram, image shape, and angles.
11
- """
12
- # Convert image to grayscale if it has multiple channels.
13
  if image.ndim == 3:
14
  image_gray = np.mean(image, axis=2)
15
  else:
16
  image_gray = image
17
 
18
- # Create an array of angles (in degrees).
19
- angles = np.linspace(angle_min, angle_max, int(num_angles), endpoint=False)
20
-
21
- # Create the radon operator for the given image shape and angles.
22
- R = Radon2D(image_gray.shape, angles=angles)
23
- # Compute the radon transform (operator acts on the flattened image).
24
- sinogram_flat = R.dot(image_gray.flatten())
25
-
26
- # The radon operator R has shape (num_angles * L, N) where N is the image size.
27
- # We deduce L (the length of each projection) from R.shape.
28
- L = R.shape[0] // len(angles)
29
- # Reshape to (num_angles, L); note that each row corresponds to one angle.
30
- sinogram = sinogram_flat.reshape((len(angles), L))
31
-
32
- # For display, we want p on the x-axis and tau (angle index) on the y-axis.
33
- # Transpose the sinogram to get shape (L, num_angles).
34
- sinogram_display = sinogram.T
35
- # Normalize the display image to [0,1].
36
- sinogram_display = (sinogram_display - sinogram_display.min()) / (sinogram_display.max() - sinogram_display.min() + 1e-8)
37
-
38
- # Pack the parameters and the original sinogram into a state dict.
 
 
 
 
 
39
  state = {
40
- "sinogram": sinogram, # shape: (num_angles, L)
41
- "image_shape": image_gray.shape, # original image dimensions
42
- "angles": angles.tolist() # store as a list for JSON-serialization
 
43
  }
44
-
45
- return sinogram_display, state
46
 
47
  def apply_mask_and_inverse_fn(state, mask_image):
48
- """
49
- Given the state (which includes the original sinogram, image shape, and angles)
50
- and a drawing (mask) provided on the radon image, this function applies the mask
51
- (setting painted sinogram pixels to zero) and then computes an inverse transform
52
- via the adjoint of the Radon operator.
53
- """
54
  if mask_image is None:
55
  return None
56
 
57
- # Ensure the mask is single-channel.
58
  if mask_image.ndim == 3:
59
  mask_image = mask_image[..., 0]
60
-
61
- # Retrieve stored sinogram and parameters.
62
- sinogram = state["sinogram"] # shape: (num_angles, L)
63
  image_shape = tuple(state["image_shape"])
64
- angles = np.array(state["angles"])
65
 
66
- # The displayed sinogram was transposed (shape (L, num_angles)); we transpose the mask
67
- # to align with the stored sinogram (shape (num_angles, L)).
68
  mask = mask_image.T
69
- # Create a binary mask; here we consider pixels with value > 0.5 as “painted.”
70
  mask_binary = (mask > 0.5).astype(float)
71
 
72
- # Apply the mask to the sinogram (zeroing out painted pixels).
73
- sinogram_masked = sinogram * (1 - mask_binary)
74
 
75
- # Reconstruct the image using the adjoint of the radon operator.
76
- R = Radon2D(image_shape, angles=angles)
77
- # The adjoint (transpose) of the radon operator is used as an approximation for the inverse.
78
- rec_flat = R.T.dot(sinogram_masked.flatten())
79
  rec = rec_flat.reshape(image_shape)
80
-
81
- # Normalize the reconstructed image to [0,1] for display.
82
  rec_norm = (rec - rec.min()) / (rec.max() - rec.min() + 1e-8)
83
  return rec_norm
84
 
85
  with gr.Blocks() as demo:
86
- gr.Markdown("## Radon Transform with Interactive Masking")
87
 
88
  with gr.Row():
89
  image_input = gr.Image(label="Input Image", type="numpy")
90
- with gr.Column():
91
- num_angles_slider = gr.Slider(10, 360, step=1, value=180, label="Number of Angles")
92
- angle_min_slider = gr.Slider(0, 360, step=1, value=0, label="Angle Min (degrees)")
93
- angle_max_slider = gr.Slider(0, 360, step=1, value=180, label="Angle Max (degrees)")
94
 
95
  compute_button = gr.Button("Compute Radon")
96
- radon_output = gr.Image(label="Radon Transform", interactive=False, type="numpy")
 
97
  radon_drawing = gr.ImageEditor(label="Paint on Radon (mask out pixels)", type="numpy")
98
  inverse_output = gr.Image(label="Reconstructed Image", interactive=False, type="numpy")
99
 
100
- # State to store parameters and the computed sinogram.
101
  state = gr.State()
102
 
103
- # When the user clicks the button, compute the radon transform.
104
  compute_button.click(
105
  fn=compute_radon_fn,
106
- inputs=[image_input, num_angles_slider, angle_min_slider, angle_max_slider],
107
  outputs=[radon_output, state]
108
  )
109
 
110
- # When the user paints on the radon image, apply the mask and compute the inverse.
111
  radon_drawing.change(
112
  fn=apply_mask_and_inverse_fn,
113
  inputs=[state, radon_drawing],
@@ -115,7 +101,7 @@ with gr.Blocks() as demo:
115
  )
116
 
117
  gr.Markdown(
118
- "**Instructions:** Upload an image and adjust the radon parameters. Click 'Compute Radon' to see the sinogram (displayed with p values on the x-axis and τ on the y-axis). Then use the paintbrush tool to mask out regions of the sinogram and see how the inverse transform (reconstruction) changes."
119
  )
120
 
121
- demo.launch()
 
2
  import numpy as np
3
  from pylops.signalprocessing import Radon2D
4
 
5
+ def compute_radon_fn(image):
6
+ # Convert image to grayscale if needed.
 
 
 
 
 
 
7
  if image.ndim == 3:
8
  image_gray = np.mean(image, axis=2)
9
  else:
10
  image_gray = image
11
 
12
+ M, N = image_gray.shape
13
+ # Define axes centered around zero.
14
+ taxis = np.linspace(-M/2, M/2, M) # time axis (rows)
15
+ haxis = np.linspace(-N/2, N/2, N) # spatial axis (columns)
16
+ # Set detector axis (pxaxis) to cover the full image diagonal.
17
+ L = int(np.ceil(np.sqrt(M**2 + N**2)))
18
+ pxaxis = np.linspace(-L/2, L/2, L)
19
+
20
+ # Create the Radon2D operator with engine 'numba' and centeredh=True.
21
+ R = Radon2D(taxis, haxis, pxaxis,
22
+ kind='linear', centeredh=True, interp=True,
23
+ onthefly=False, engine='numba', dtype='float64', name='R')
24
+
25
+ # Compute the forward radon transform.
26
+ radon_data_flat = R.dot(image_gray.flatten())
27
+ # Deduce the number of projections from the operator shape.
28
+ nproj = R.shape[0] // L
29
+ # Reshape to (nproj, L) so each row corresponds to one projection.
30
+ radon_data = radon_data_flat.reshape((nproj, L))
31
+
32
+ # Transpose for display: p (detector coordinate) on x-axis, τ on y-axis.
33
+ radon_display = radon_data.T
34
+ # Normalize for display.
35
+ radon_display = (radon_display - radon_display.min()) / (radon_display.max() - radon_display.min() + 1e-8)
36
+
37
+ # Save the state including the radon data and operator for inverse computation.
38
  state = {
39
+ "radon_data": radon_data, # shape: (nproj, L)
40
+ "image_shape": image_gray.shape,
41
+ "R": R, # the Radon2D operator
42
+ "L": L # detector length
43
  }
44
+ return radon_display, state
 
45
 
46
  def apply_mask_and_inverse_fn(state, mask_image):
 
 
 
 
 
 
47
  if mask_image is None:
48
  return None
49
 
50
+ # Ensure mask is single-channel.
51
  if mask_image.ndim == 3:
52
  mask_image = mask_image[..., 0]
53
+
54
+ radon_data = state["radon_data"]
 
55
  image_shape = tuple(state["image_shape"])
56
+ L = state["L"]
57
 
58
+ # The displayed radon image was transposed, so transpose mask back.
 
59
  mask = mask_image.T
60
+ # Create binary mask: painted pixels (value > 0.5) become 1.
61
  mask_binary = (mask > 0.5).astype(float)
62
 
63
+ # Apply the mask: zero-out masked pixels in the radon data.
64
+ radon_masked = radon_data * (1 - mask_binary)
65
 
66
+ # Reconstruct the image using the adjoint (transpose) of the operator.
67
+ R = state["R"]
68
+ rec_flat = R.T.dot(radon_masked.flatten())
 
69
  rec = rec_flat.reshape(image_shape)
70
+ # Normalize reconstruction for display.
 
71
  rec_norm = (rec - rec.min()) / (rec.max() - rec.min() + 1e-8)
72
  return rec_norm
73
 
74
  with gr.Blocks() as demo:
75
+ gr.Markdown("## Radon Transform with Interactive Masking (Using PyLops Radon2D)")
76
 
77
  with gr.Row():
78
  image_input = gr.Image(label="Input Image", type="numpy")
 
 
 
 
79
 
80
  compute_button = gr.Button("Compute Radon")
81
+ radon_output = gr.Image(label="Radon Transform Image", interactive=False, type="numpy")
82
+ # Use the updated ImageEditor component for interactive masking.
83
  radon_drawing = gr.ImageEditor(label="Paint on Radon (mask out pixels)", type="numpy")
84
  inverse_output = gr.Image(label="Reconstructed Image", interactive=False, type="numpy")
85
 
86
+ # State to hold radon data and operator.
87
  state = gr.State()
88
 
89
+ # When "Compute Radon" is clicked, compute the radon transform.
90
  compute_button.click(
91
  fn=compute_radon_fn,
92
+ inputs=[image_input],
93
  outputs=[radon_output, state]
94
  )
95
 
96
+ # When the user edits (paints) the radon image, apply the mask and compute the inverse.
97
  radon_drawing.change(
98
  fn=apply_mask_and_inverse_fn,
99
  inputs=[state, radon_drawing],
 
101
  )
102
 
103
  gr.Markdown(
104
+ "**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."
105
  )
106
 
107
+ demo.launch()