2828# such a type:
2929#
3030# .. math::
31- # t(r, \theta; x) = \tan(90°- \theta)x + \frac{r}{\sin(\theta)}
31+ # t\sin( \theta) + x\cos( \theta) = r,
3232#
3333# where :math:`\theta` is the angle between the x-axis (:math:`x`) and
3434# the perpendicular to the summation line and :math:`r` is the distance
35- # from the origin of the summation line.
35+ # from the origin of the summation line. Radon transform in CT
36+ # corresponds to the integral of the input image along the straight line above.
37+ # To implement the integration in PyLops we simply need to express
38+ # :math:`t(r,\theta;x)` which is given by:
39+ #
40+ # .. math::
41+ # t(r,\theta; x) = \tan\left(\frac{\pi}{2}-\theta\right)x + \frac{r}{\sin(\theta)}.
3642
3743
3844@jit (nopython = True )
@@ -44,6 +50,13 @@ def radoncurve(x, r, theta):
4450 )
4551
4652
53+ ###############################################################################
54+ # Note that in the above implementation we added centering :math:`t \mapsto t - n_y/2` and
55+ # :math:`r \mapsto r - n_y/2` so that the origin of the integration lines is exactly in the
56+ # center of the image (centering for :math:`x` is not needed because we will use
57+ # ``centeredh=True`` in the constructor of ``Radon2D``).
58+
59+
4760x = np .load ("../testdata/optimization/shepp_logan_phantom.npy" ).T
4861x = x / x .max ()
4962nx , ny = x .shape
@@ -81,14 +94,36 @@ def radoncurve(x, r, theta):
8194axs [1 ].set_title ("Data" )
8295axs [1 ].axis ("tight" )
8396axs [2 ].imshow (xrec .T , cmap = "gray" )
84- axs [2 ].set_title ("Adjoint model" )
97+ axs [2 ].set_title ("Adjoint model in PyLops " )
8598axs [2 ].axis ("tight" )
8699fig .tight_layout ()
87100
101+ ###############################################################################
102+ # Note that our raw data ``y`` *does not represent exactly* classical sinograms
103+ # in medical imaging. Integration along curves in the adjoint form of
104+ # :func:`pylops.signalprocessing.Radon2D` is performed with respect to
105+ # :math:`dx`, whereas canonically it is assumed to be with respect to the natural
106+ # parametrization :math:`dl = \sqrt{(dx)^2 + (dt)^2}`. To retrieve back the
107+ # classical sinogram we have to divide data by the jacobian
108+ # :math:`j(x,l) = \left\vert dx/dl \right\vert = |\sin(\theta)|`.
109+
110+ sinogram = np .divide (
111+ y .T , np .abs (np .sin (theta ) + 1e-15 )
112+ ) # small shift to avoid zero-division
113+ fig , axs = plt .subplots (1 , 2 , figsize = (10 , 4 ))
114+ axs [0 ].imshow (y .T , cmap = "gray" )
115+ axs [0 ].set_title ("Data" )
116+ axs [0 ].axis ("tight" )
117+ axs [1 ].imshow (sinogram , cmap = "gray" )
118+ axs [1 ].set_title ("Sinogram in medical imaging" )
119+ axs [1 ].axis ("tight" )
120+ fig .tight_layout ()
88121
89122###############################################################################
90- # Finally we take advantage of our different solvers and try to invert the
91- # modelling operator both in a least-squares sense and using TV-reg.
123+ # From now on, we will not pursue further working with the "true sinogram", instead
124+ # we will reconstruct the original phantom directly from ``y``. For this we take advantage
125+ # of our different solvers and try to invert the modelling operator both in a
126+ # least-squares sense and using TV-reg.
92127Dop = [
93128 pylops .FirstDerivative (
94129 (nx , ny ), axis = 0 , edge = True , kind = "backward" , dtype = np .float64
0 commit comments