Skip to content

Add medical image denoising demo for anisotropic diffusion#3997

Closed
m-wisell wants to merge 3 commits intoFEniCS:mainfrom
m-wisell:demo/medical-image-denoising
Closed

Add medical image denoising demo for anisotropic diffusion#3997
m-wisell wants to merge 3 commits intoFEniCS:mainfrom
m-wisell:demo/medical-image-denoising

Conversation

@m-wisell
Copy link
Copy Markdown

@m-wisell m-wisell commented Nov 20, 2025

Summary

This PR adds medical image denoising using anisotropic diffusion.

Addresses: Issue #3973

Implementation

Mathematical Model:
Perona-Malik anisotropic diffusion for edge-preserving denoising

Numerical Methods:

  • P1 Lagrange finite elements for spatial discretization
  • Backward Euler time-stepping
  • Conjugate Gradient solver with Jacobi preconditioner

Features:

  • Image I/O with automatic normalization
  • Image-to-mesh and mesh-to-image conversion
  • Quality metrics: PSNR, SSIM, Edge Preservation Index
  • Auto-generated synthetic test images (no dataset download required)

What Changed (Latest Update)

  • Fixed CI failures: removed deprecated basix.ufl imports
  • Consolidated to single standalone demo file
  • Added test suite (all passing)
  • Simplified dependencies and improved documentation

Quality Metrics (Real Mammogram 512x512)

  • PSNR: 35.52 dB
  • SSIM: 0.8739
  • Edge Preservation: 0.8042

Usage

python demo/demo_anisotropic_diffusion_medical.py
python demo/demo_anisotropic_diffusion_medical.py --image path/to/image.jpg
python demo/demo_anisotropic_diffusion_medical.py --kappa 25 --iterations 40

Ready for review. CI check should now pass.

@m-wisell m-wisell changed the title [WIP]: Add medical image denoising demo for anisotropic diffusion Add medical image denoising demo for anisotropic diffusion Dec 11, 2025
@m-wisell
Copy link
Copy Markdown
Author

Updated to fix CI failures. All tests now passing locally. Ready for review.

Comment on lines +72 to +94
def function_to_image(u, image_shape):
height, width = image_shape

mesh_obj = u.function_space.mesh
coords = mesh_obj.geometry.x[:, :2]
values = u.x.array

x_pixels = np.linspace(0, width - 1, width)
y_pixels = np.linspace(0, height - 1, height)
X, Y = np.meshgrid(x_pixels, y_pixels)

Y_flipped = height - 1 - Y

image = griddata(
coords,
values,
(X, Y_flipped),
method='linear',
fill_value=0.0
)

image = np.nan_to_num(image, nan=0.0)
return image
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function will only work in serial, no?

Comment on lines +50 to +69
def image_to_function(image, V):
u = fem.Function(V)
height, width = image.shape

def image_values(x):
values = np.zeros(x.shape[1])

for i in range(x.shape[1]):
x_coord = x[0, i]
y_coord = x[1, i]

px = int(np.clip(np.round(x_coord), 0, width - 1))
py = int(np.clip(np.round(height - 1 - y_coord), 0, height - 1))

values[i] = image[py, px]

return values

u.interpolate(image_values)
return u
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you vectorize this? Something along the lines of:

from mpi4py import MPI
import dolfinx
import numpy as np

width = 3
height = 2

mesh = dolfinx.mesh.create_rectangle(MPI.COMM_WORLD, [[0,0],[width, height]], [int(width), int(height)])
V = dolfinx.fem.functionspace(mesh, ("DG", 0))
u = dolfinx.fem.Function(V)


a = np.linspace(0.5, width-0.5, width, endpoint=True)
b = np.linspace(0.5, height-0.5, height, endpoint=True)
mg = np.meshgrid(a, b)

def f(x, y):
    return x**2 + y**2
z = f(*mg)

    
def image_values(x):
    values = np.zeros(x.shape[1])
    
    for i in range(x.shape[1]):
        x_coord = x[0, i]
        y_coord = x[1, i]
        
        px = int(np.clip(np.round(x_coord), 0, width - 1))
        py = int(np.clip(np.round(height - y_coord), 0, height - 1))
        values[i] = z[py,px]
    
    return values

def images_values_vectorized(x):
    px = np.clip(np.round(x[0]), 0, width - 1).astype(np.int64)
    py = np.clip(np.round(height - x[1]), 0, height-1).astype(np.int64)
    return z[py, px]

u.interpolate(image_values)

u2 = dolfinx.fem.Function(V)
u2.interpolate(images_values_vectorized)
np.testing.assert_allclose(u.x.array, u2.x.array)

return u_new

def denoise(self, image, n_iterations=30, verbose=True):
self.u_n = image_to_function(image, self.V)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As your mesh aligns with the image grid, using interpolation to move data from a voxel grid (DG-0) to a P1 Lagrange space is not well defined. I would suggest that you for the first time step use u_n in DG-0, and then after solving, replace this function with a P1 function.


return a, L

def solve_timestep(self, a, L):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function needs to be fully rewritten.

  1. you call fem.form repeatedly, even if the form does not change over time. (with my suggestion above, it would change once, but it wouldn't modify the sparsity pattern of point 2, thus those structures can be created once)
  2. The same holds for the matrix and vector. They should only be created once.
  3. As you are not using any advanced PETSc features here, why not use dolfinx.fem.petsc.LinearProblem. (even if one changes the form once, by overwriting LinearProblem._a and LinearProblem._b with the form switching from DG-0 to P-1 for u_n)

@jhale
Copy link
Copy Markdown
Member

jhale commented Dec 16, 2025

I'm cautiously positive about the underlying idea for this demo (showing the use of a PDE solve on a problem from outside mechanics), but the execution is totally different to our existing demos:

  • README.md
  • No inline documentation using jupytext markdown.
  • Very heavy dependency set which is going to be hard to maintain.
  • We do not accept contributions that don't work in parallel.

@garth-wells
Copy link
Copy Markdown
Member

This is a nice example, but a bit heavyweight for our demo suite. And the more dependencies we have the more often we have test system problems. Happy to discuss how we can highlight this (and other) interesting applications examples.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants