From 840b697f037f1a1d75865809816adb43def48415 Mon Sep 17 00:00:00 2001 From: Mary Isabelle Wisell Date: Thu, 20 Nov 2025 02:52:54 -0800 Subject: [PATCH] WIP: Add medical image denoising demo for anisotropic diffusion --- .../data/download_cbis_ddsm.py | 112 ++++++++++ .../medical_imaging/data/sample_mammogram.jpg | Bin 0 -> 21912 bytes .../demo_anisotropic_diffusion_medical.py | 205 ++++++++++++++++++ python/demo/medical_imaging/requirements.txt | 24 ++ python/demo/medical_imaging/utils/__init__.py | 0 .../medical_imaging/utils/image_processing.py | 141 ++++++++++++ 6 files changed, 482 insertions(+) create mode 100644 python/demo/medical_imaging/data/download_cbis_ddsm.py create mode 100644 python/demo/medical_imaging/data/sample_mammogram.jpg create mode 100644 python/demo/medical_imaging/demo/demo_anisotropic_diffusion_medical.py create mode 100644 python/demo/medical_imaging/requirements.txt create mode 100644 python/demo/medical_imaging/utils/__init__.py create mode 100644 python/demo/medical_imaging/utils/image_processing.py diff --git a/python/demo/medical_imaging/data/download_cbis_ddsm.py b/python/demo/medical_imaging/data/download_cbis_ddsm.py new file mode 100644 index 00000000000..bb4f108df28 --- /dev/null +++ b/python/demo/medical_imaging/data/download_cbis_ddsm.py @@ -0,0 +1,112 @@ +import os +import numpy as np +import zipfile +from PIL import Image +from pathlib import Path +import argparse +import shutil + +def download_dataset(output_dir="data/cbis_ddsm"): + output_path = Path(output_dir) + output_path.mkdir(parents=True, exist_ok=True) + + os.system(f"kaggle datasets download -d awsaf49/cbis-ddsm-breast-cancer-image-dataset -p {output_dir}") + + zip_file = output_path / "cbis-ddsm-breast-cancer-image-dataset.zip" + if zip_file.exists(): + print("Extracting dataset...") + with zipfile.ZipFile(zip_file, 'r') as zip_ref: + zip_ref.extractall(output_path) + zip_file.unlink() + return True + else: + print("Error: Dataset download failed.") + return False + +def prepare_sample_images(dataset_dir="data/cbis_ddsm", sample_dir="data/samples", n_samples=5): + sample_path = Path(sample_dir) + sample_path.mkdir(parents=True, exist_ok=True) + + dataset_path = Path(dataset_dir) + image_files = list(dataset_path.rglob("*.png"))[:n_samples] + + if not image_files: + print("No images found. Please download the dataset first.") + return + + print(f"Copying {len(image_files)} sample images...") + + for i, img_file in enumerate(image_files): + img = Image.open(img_file).convert('L') + max_size = 1024 + if max(img.size) > max_size: + img.thumbnail((max_size, max_size), Image.Resampling.LANCZOS) + output_file = sample_path / f"sample_{i:02d}.png" + img.save(output_file) + print(f" Saved: {output_file}") + + print(f"Sample images saved to {sample_dir}/") + +def create_synthetic_test_image(output_file="data/synthetic_test.png", size=(512, 512)): + # Create clean image with geometric shapes + img = np.zeros(size, dtype=np.float64) + + y, x = np.ogrid[:size[0], :size[1]] + + # Circle 1 (top-left) - bright white + cx1, cy1, r1 = size[1]//4, size[0]//4, 60 + mask1 = (x - cx1)**2 + (y - cy1)**2 <= r1**2 + img[mask1] = 1.0 + + # Circle 2 (bottom-right) - slightly dimmer + cx2, cy2, r2 = 3*size[1]//4, 3*size[0]//4, 80 + mask2 = (x - cx2)**2 + (y - cy2)**2 <= r2**2 + img[mask2] = 0.9 + + # Rectangle (center) + img[size[0]//3:2*size[0]//3, size[1]//2-40:size[1]//2+40] = 0.8 + + # Add light Gaussian noise + np.random.seed(42) # Reproducible + noise = np.random.normal(0, 0.03, size) + noisy_img = img + noise + + # Clip to [0, 1] + noisy_img = np.clip(noisy_img, 0, 1) + + # Convert to 8-bit with high quality + img_uint8 = (noisy_img * 255).astype(np.uint8) + + # Save with maximum quality + Image.fromarray(img_uint8).save(output_file, quality=100, optimize=False) + + print(f"Improved synthetic test image saved to {output_file}") + print(f" Image size: {size}") + print(f" Normalized range: [0, 1]") + print(f" Noise level: LIGHT (std=0.03)") + print(f" Features: 2 circles + 1 rectangle") + print(f" Quality: HIGH (no compression)") + + return output_file + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Download and prepare CBIS-DDSM dataset") + parser.add_argument("--download", action="store_true", help="Download full dataset") + parser.add_argument("--samples", action="store_true", help="Create sample subset") + parser.add_argument("--synthetic", action="store_true", help="Create synthetic test image") + parser.add_argument("--all", action="store_true", help="Do all of the above") + + args = parser.parse_args() + + if args.all or args.synthetic: + create_synthetic_test_image() + + if args.all or args.download: + success = download_dataset() + if success and (args.all or args.samples): + prepare_sample_images() + elif args.samples: + prepare_sample_images() + + if not any([args.download, args.samples, args.synthetic, args.all]): + parser.print_help() \ No newline at end of file diff --git a/python/demo/medical_imaging/data/sample_mammogram.jpg b/python/demo/medical_imaging/data/sample_mammogram.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b2fa341e976aafa94eefed71722a6f85e7a36f35 GIT binary patch literal 21912 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 literal 0 HcmV?d00001 diff --git a/python/demo/medical_imaging/demo/demo_anisotropic_diffusion_medical.py b/python/demo/medical_imaging/demo/demo_anisotropic_diffusion_medical.py new file mode 100644 index 00000000000..ba7fb0a68e7 --- /dev/null +++ b/python/demo/medical_imaging/demo/demo_anisotropic_diffusion_medical.py @@ -0,0 +1,205 @@ +import numpy as np +from mpi4py import MPI +from dolfinx import mesh, fem, io +from dolfinx.fem.petsc import assemble_matrix, assemble_vector, LinearProblem +import ufl +from ufl import grad, div, dx, dot, sqrt, inner +from petsc4py import PETSc +import sys +import os +from basix.ufl import element + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +from utils.image_processing import ( + load_image, save_image, create_image_mesh, + image_to_function, function_to_image, + compute_psnr, compute_ssim, edge_preservation_index +) +from data.download_cbis_ddsm import create_synthetic_test_image + +class AnisotropicDiffusion: + def __init__(self, image_shape, kappa=30.0, dt=0.1, comm=MPI.COMM_WORLD): + self.image_shape = image_shape + self.kappa = kappa + self.dt = dt + self.comm = comm + + self.domain = create_image_mesh(image_shape, comm) + + P1 = element("Lagrange", self.domain.basix_cell(), 1) + self.V = fem.functionspace(self.domain, P1) + + self.u = ufl.TrialFunction(self.V) + self.v = ufl.TestFunction(self.V) + + self.u_n = fem.Function(self.V) + + def diffusion_coefficient(self, grad_u): + # Compute gradient magnitude with small epsilon to avoid division by zero + grad_magnitude = sqrt(dot(grad_u, grad_u) + 1e-10) + + # Perona-Malik diffusion coefficient + c = 1.0 / (1.0 + (grad_magnitude / self.kappa)**2) + + return c + + def setup_variational_problem(self): + c = self.diffusion_coefficient(grad(self.u_n)) + + F = (self.u - self.u_n) * self.v * dx + \ + self.dt * c * dot(grad(self.u), grad(self.v)) * dx + + a = ufl.lhs(F) + L = ufl.rhs(F) + + return a, L + + def solve_timestep(self, a, L): + A = assemble_matrix(fem.form(a)) + A.assemble() + b = assemble_vector(fem.form(L)) + b.ghostUpdate(addv=PETSc.InsertMode.ADD, mode=PETSc.ScatterMode.REVERSE) + + solver = PETSc.KSP().create(self.comm) + solver.setOperators(A) + solver.setType(PETSc.KSP.Type.CG) + + pc = solver.getPC() + pc.setType(PETSc.PC.Type.JACOBI) + + u_new = fem.Function(self.V) + solver.solve(b, u_new.x.petsc_vec) + u_new.x.scatter_forward() + + return u_new + + def denoise(self, image, n_iterations=20, verbose=True): + self.u_n = image_to_function(image, self.V) + + a, L = self.setup_variational_problem() + + for iteration in range(n_iterations): + if verbose and self.comm.rank == 0: + print(f"Iteration {iteration + 1}/{n_iterations}") + + u_new = self.solve_timestep(a, L) + + self.u_n.x.array[:] = u_new.x.array[:] + + a, L = self.setup_variational_problem() + + denoised = function_to_image(self.u_n, self.image_shape) + + return denoised + +def demo_synthetic_image(): + print(f"Demo: Anisotropic Diffusion on Synthetic Image\n") + + # Load or create synthetic test image + test_image_path = "data/synthetic_test.png" + + if not os.path.exists(test_image_path): + print("Creating synthetic test image...") + os.makedirs("data", exist_ok=True) + create_synthetic_test_image(test_image_path) + + # Load image + print(f"Loading image: {test_image_path}") + image = load_image(test_image_path, normalize=True) + print(f"Image shape: {image.shape}") + + solver = AnisotropicDiffusion( + image_shape=image.shape, + kappa=20.0, + dt=0.05 + ) + + print("\nApplying anisotropic diffusion...") + denoised = solver.denoise(image, n_iterations=40, verbose=True) + + # Compute quality metrics + print(f"Quality Metrics:\n") + psnr = compute_psnr(image, denoised) + ssim = compute_ssim(image, denoised) + epi = edge_preservation_index(image, denoised) + + print(f"PSNR: {psnr:.2f} dB") + print(f"SSIM: {ssim:.4f}") + print(f"Edge Preservation Index: {epi:.4f}") + + # Save results + os.makedirs("output", exist_ok=True) + save_image(denoised, "output/synthetic_denoised.png") + print(f"\nDenoised image saved to: output/synthetic_denoised.png") + + return image, denoised + + +def demo_medical_image(image_path): + print(f"Demo: Anisotropic Diffusion on Medical Image\n") + + # Load image + print(f"Loading image: {image_path}") + + target_size = (512, 512) + image = load_image(image_path, normalize=True, target_size=target_size) + print(f"Image shape: {image.shape}") + + print("\nInitializing anisotropic diffusion solver...") + solver = AnisotropicDiffusion( + image_shape=image.shape, + kappa=20.0, + dt=0.05 + ) + + print("\nApplying anisotropic diffusion...") + denoised = solver.denoise(image, n_iterations=30, verbose=True) + + print(f"Quality Metrics:\n") + psnr = compute_psnr(image, denoised) + ssim = compute_ssim(image, denoised) + epi = edge_preservation_index(image, denoised) + + print(f"PSNR: {psnr:.2f} dB") + print(f"SSIM: {ssim:.4f}") + print(f"Edge Preservation Index: {epi:.4f}") + + # Save results + os.makedirs("output", exist_ok=True) + output_path = "output/medical_denoised.png" + save_image(denoised, output_path) + print(f"\nDenoised image saved to: {output_path}") + + return image, denoised + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Anisotropic diffusion for medical image denoising" + ) + parser.add_argument( + "--image", + type=str, + help="Path to input image (if not provided, uses synthetic test image)" + ) + parser.add_argument( + "--kappa", + type=float, + default=30.0, + help="Gradient threshold (default: 30.0)" + ) + parser.add_argument( + "--iterations", + type=int, + default=20, + help="Number of iterations (default: 20)" + ) + + args = parser.parse_args() + + if args.image: + demo_medical_image(args.image) + else: + demo_synthetic_image() \ No newline at end of file diff --git a/python/demo/medical_imaging/requirements.txt b/python/demo/medical_imaging/requirements.txt new file mode 100644 index 00000000000..677b01f645c --- /dev/null +++ b/python/demo/medical_imaging/requirements.txt @@ -0,0 +1,24 @@ +# Core Dependencies +# (Based on Last Stable Release: Dolfinx v0.9.0) +fenics-dolfinx==0.9.0 +numpy>=1.24.0 +matplotlib>=3.7.0 +scipy>=1.10.0 + +# Image Processing +pillow>=10.0.0 +scikit-image>=0.21.0 + +# Visualization +pyvista>=0.43.0 + +# Data handling +pandas>=2.0.0 +kaggle>=1.6.0 # Unique to the CBIS DDSM Dataset + +# Testing +pytest>=7.4.0 +pytest-mpi>=0.6 + +# For DICOM +pydicom>=2.4.0 \ No newline at end of file diff --git a/python/demo/medical_imaging/utils/__init__.py b/python/demo/medical_imaging/utils/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/python/demo/medical_imaging/utils/image_processing.py b/python/demo/medical_imaging/utils/image_processing.py new file mode 100644 index 00000000000..8bb6245d0d4 --- /dev/null +++ b/python/demo/medical_imaging/utils/image_processing.py @@ -0,0 +1,141 @@ +import numpy as np +from PIL import Image +from dolfinx import mesh, fem +from mpi4py import MPI +import ufl +from scipy import ndimage +from skimage.metrics import structural_similarity +from scipy.interpolate import griddata +from basix.ufl import element + + +def load_image(filepath, normalize=True, target_size=None): + img = Image.open(filepath).convert('L') # Convert to grayscale + + if target_size is not None: + img = img.resize((target_size[1], target_size[0]), Image.Resampling.LANCZOS) + + image = np.array(img, dtype=np.float64) + + if normalize: + image = image / 255.0 + + return image + + +def save_image(image, filepath, denormalize=True): + if denormalize: + image = np.clip(image * 255.0, 0, 255) + + img = Image.fromarray(image.astype(np.uint8)) + img.save(filepath) + + +def create_image_mesh(image_shape, comm=MPI.COMM_WORLD): + height, width = image_shape + + # Create unit square mesh and scale to image dimensions + domain = mesh.create_rectangle( + comm, + [[0.0, 0.0], [float(width), float(height)]], + [width-1, height-1], + cell_type=mesh.CellType.triangle + ) + + return domain + + +def image_to_function(image, V): + u = fem.Function(V) + + # Get mesh coordinates + height, width = image.shape + + # Interpolate image values onto function space + 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] + + # Convert to pixel coordinates (with bounds checking) + 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 + + +def function_to_image(u, image_shape): + height, width = image_shape + + # Get mesh coordinates and function values + mesh = u.function_space.mesh + coords = mesh.geometry.x[:, :2] # Get x, y coordinates (drop z) + 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 + + # Interpolate function values onto regular pixel grid + # Use 'linear' interpolation for smooth results + image = griddata( + coords, + values, + (X, Y_flipped), + method='linear', + fill_value=0.0 + ) + + image = np.nan_to_num(image, nan=0.0) + + return image + + +def compute_psnr(original, denoised, max_value=1.0): + mse = np.mean((original - denoised) ** 2) + if mse == 0: + return float('inf') + + psnr = 20 * np.log10(max_value / np.sqrt(mse)) + return psnr + + +def compute_ssim(original, denoised): + return structural_similarity(original, denoised, data_range=denoised.max() - denoised.min()) + + +def edge_preservation_index(original, denoised): + sobel_orig = ndimage.sobel(original) + sobel_denoised = ndimage.sobel(denoised) + + # Normalize + sobel_orig = sobel_orig / (np.max(sobel_orig) + 1e-10) + sobel_denoised = sobel_denoised / (np.max(sobel_denoised) + 1e-10) + + # Compute correlation + numerator = np.sum(sobel_orig * sobel_denoised) + denominator = np.sqrt(np.sum(sobel_orig**2) * np.sum(sobel_denoised**2)) + + epi = numerator / (denominator + 1e-10) + + return epi + + +def add_gaussian_noise(image, var=0.01, seed=None): + if seed is not None: + np.random.seed(seed) + + noise = np.random.normal(0, np.sqrt(var), image.shape) + noisy = image + noise + + return np.clip(noisy, 0, 1) \ No newline at end of file