import math import cv2 import numpy as np from .algorithms import ( ProcessingError, bit_plane, box_filter, contrast_stretch, gamma, gray_slice, gray_to_color_sinusoidal, gray_to_rgb, histogram_equalization, histogram_matching, hsi_intensity_filter, hsi_to_rgb, logarithmic, local_equalization, negative, normalize_to_uint8, pseudo_color_slices, rgb_to_hsi, roberts, sobel, to_gray, weighted_average, median_filter, high_boost, ensure_uint8, ) CH_BASIC = "Basic" CH3 = "Image Enhancement in the Spatial Domain" CH4 = "Image Enhancement in the Frequency Domain" CH6 = "Color Image Processing" def odd_param(default=3, max_value=35): return {"type": "int", "default": default, "min": 3, "max": max_value, "step": 2, "odd": True} def float_param(default, min_value, max_value, step=0.1): return {"type": "float", "default": default, "min": min_value, "max": max_value, "step": step} def int_param(default, min_value, max_value, step=1): return {"type": "int", "default": default, "min": min_value, "max": max_value, "step": step} def select_param(default, choices): return {"type": "select", "default": default, "choices": choices} def bool_param(default=False): return {"type": "bool", "default": default} def crop(image, params): x = max(0, int(params.get("x", 0))) y = max(0, int(params.get("y", 0))) width = int(params.get("width", image.shape[1] - x)) height = int(params.get("height", image.shape[0] - y)) if width <= 0 or height <= 0: raise ProcessingError("Crop width and height must be positive.") x2 = min(image.shape[1], x + width) y2 = min(image.shape[0], y + height) if x >= x2 or y >= y2: raise ProcessingError("Crop rectangle is outside the image.") return image[y:y2, x:x2] def identity(image, params): return image.copy() def inverse_log(image, params): c = float(params.get("c", 1.0)) normalized = image.astype(np.float32) / 255.0 transformed = np.expm1(normalized / max(c, 1e-8)) transformed /= max(float(np.max(transformed)), 1e-8) return ensure_uint8(np.round(transformed * 255.0)) def threshold(image, params): level = int(params.get("level", 128)) high = int(params.get("high", 255)) low = int(params.get("low", 0)) gray = to_gray(image) return gray_to_rgb(np.where(gray >= level, high, low).astype(np.uint8)) def histeq(image, params): mode = params.get("mode", "intensity") if image.ndim == 2 or mode == "grayscale": return histogram_equalization(image, params) if mode == "rgb": channels = [histogram_equalization(image[:, :, idx], params)[:, :, 0] for idx in range(3)] return np.stack(channels, axis=2).astype(np.uint8) hsi = rgb_to_hsi(image) intensity = np.round(hsi[..., 2] * 255).astype(np.uint8) hsi[..., 2] = histogram_equalization(intensity, params)[:, :, 0].astype(np.float32) / 255.0 return hsi_to_rgb(hsi) def gaussian_noise(image, params): mean = float(params.get("mean", 0)) variance = float(params.get("variance", 0.01)) sigma = math.sqrt(max(variance, 0.0)) * 255.0 noise = np.random.default_rng().normal(mean * 255.0, sigma, size=image.shape) return ensure_uint8(np.round(image.astype(np.float32) + noise)) def salt_pepper_noise(image, params): amount = float(params.get("amount", 0.03)) salt_ratio = float(params.get("salt_ratio", 0.5)) output = image.copy() rng = np.random.default_rng() mask = rng.random(image.shape[:2]) salt = mask < amount * salt_ratio pepper = (mask >= amount * salt_ratio) & (mask < amount) output[salt] = 255 output[pepper] = 0 return output def speckle_noise(image, params): variance = float(params.get("variance", 0.04)) noise = np.random.default_rng().normal(0, math.sqrt(max(variance, 0.0)), size=image.shape) return ensure_uint8(np.round(image.astype(np.float32) + image.astype(np.float32) * noise)) def gaussian_filter(image, params): size = int(params.get("size", 3)) variance = float(params.get("variance", 1.0)) if size < 3 or size % 2 == 0: raise ProcessingError("size must be an odd integer >= 3.") sigma = math.sqrt(max(variance, 1e-8)) return cv2.GaussianBlur(image, (size, size), sigmaX=sigma, sigmaY=sigma, borderType=cv2.BORDER_REFLECT) def max_filter(image, params): size = int(params.get("size", 3)) if size < 3 or size % 2 == 0: raise ProcessingError("size must be an odd integer >= 3.") return cv2.dilate(image, np.ones((size, size), np.uint8)) def min_filter(image, params): size = int(params.get("size", 3)) if size < 3 or size % 2 == 0: raise ProcessingError("size must be an odd integer >= 3.") return cv2.erode(image, np.ones((size, size), np.uint8)) def laplacian_slide(image, params): mask_name = params.get("mask", "cross") kernels = { "cross": np.array([[0, 1, 0], [1, -5, 1], [0, 1, 0]], dtype=np.float32), "diagonal": np.array([[1, 1, 1], [1, -9, 1], [1, 1, 1]], dtype=np.float32), "zero_sum_cross": np.array([[0, 1, 0], [1, -4, 1], [0, 1, 0]], dtype=np.float32), "zero_sum_diagonal": np.array([[1, 1, 1], [1, -8, 1], [1, 1, 1]], dtype=np.float32), } kernel = kernels.get(mask_name) if kernel is None: raise ProcessingError("Unknown Laplacian mask.") channels = [cv2.filter2D(image[:, :, idx], cv2.CV_32F, kernel, borderType=cv2.BORDER_REFLECT) for idx in range(image.shape[2])] result = np.stack(channels, axis=2) return normalize_to_uint8(result) if params.get("mode", "sharpen") == "detail" else ensure_uint8(result) def gradient_abs_sum(image, params): operator = params.get("operator", "sobel") gray = to_gray(image).astype(np.float32) if operator == "roberts": gx = np.array([[-1, 0], [0, 1]], dtype=np.float32) gy = np.array([[0, -1], [1, 0]], dtype=np.float32) else: gx = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], dtype=np.float32) gy = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=np.float32) fx = cv2.filter2D(gray, cv2.CV_32F, gx, borderType=cv2.BORDER_REFLECT) fy = cv2.filter2D(gray, cv2.CV_32F, gy, borderType=cv2.BORDER_REFLECT) return gray_to_rgb(normalize_to_uint8(np.abs(fx) + np.abs(fy))) def rgb_channel(image, params): channel = params.get("channel", "r") index = {"r": 0, "g": 1, "b": 2}.get(channel, 0) return gray_to_rgb(image[:, :, index]) def hsi_view(image, params): component = params.get("component", "i") hsi = rgb_to_hsi(image) index = {"h": 0, "s": 1, "i": 2}.get(component, 2) return gray_to_rgb(np.round(hsi[:, :, index] * 255.0).astype(np.uint8)) def fft_spectrum(image, params): gray = to_gray(image).astype(np.float32) spectrum = np.fft.fftshift(np.fft.fft2(gray)) mode = params.get("mode", "log_magnitude") if mode == "phase": return gray_to_rgb(normalize_to_uint8(np.angle(spectrum))) magnitude = np.abs(spectrum) if mode == "log_magnitude": magnitude = np.log1p(magnitude) return gray_to_rgb(normalize_to_uint8(magnitude)) def distance_grid(shape): rows, cols = shape u = np.arange(rows) - rows / 2 v = np.arange(cols) - cols / 2 vv, uu = np.meshgrid(v, u) return np.sqrt(uu**2 + vv**2) def frequency_filter(image, params): gray = to_gray(image).astype(np.float32) d0 = float(params.get("cutoff", 40)) order = int(params.get("order", 2)) family = params.get("family", "gaussian") kind = params.get("kind", "lowpass") d = distance_grid(gray.shape) if family == "ideal": mask = (d <= d0).astype(np.float32) elif family == "butterworth": mask = 1.0 / (1.0 + (d / max(d0, 1e-8)) ** (2 * max(order, 1))) else: mask = np.exp(-(d**2) / (2.0 * max(d0, 1e-8) ** 2)) if kind == "highpass": mask = 1.0 - mask if params.get("output", "image") == "mask": return gray_to_rgb(normalize_to_uint8(mask)) f = np.fft.fftshift(np.fft.fft2(gray)) result = np.real(np.fft.ifft2(np.fft.ifftshift(f * mask))) return gray_to_rgb(normalize_to_uint8(result)) def frequency_laplacian(image, params): gray = to_gray(image).astype(np.float32) rows, cols = gray.shape u = np.arange(rows) - rows / 2 v = np.arange(cols) - cols / 2 vv, uu = np.meshgrid(v, u) h = -4.0 * (np.pi**2) * (uu**2 + vv**2) f = np.fft.fftshift(np.fft.fft2(gray)) result = np.real(np.fft.ifft2(np.fft.ifftshift(f * h))) return gray_to_rgb(normalize_to_uint8(result)) def correlation(image, params): kernel = np.array(params.get("kernel", [[1, 1, 1], [1, 1, 1], [1, 1, 1]]), dtype=np.float32) kernel /= max(float(np.sum(np.abs(kernel))), 1e-8) gray = to_gray(image).astype(np.float32) return gray_to_rgb(normalize_to_uint8(cv2.filter2D(gray, cv2.CV_32F, kernel, borderType=cv2.BORDER_REFLECT))) def convolution(image, params): kernel = np.array(params.get("kernel", [[1, 1, 1], [1, 1, 1], [1, 1, 1]]), dtype=np.float32) return correlation(image, {"kernel": np.flipud(np.fliplr(kernel)).tolist()}) def bone_scan_workflow(image, params): gray_rgb = gray_to_rgb(to_gray(image)) lap_detail = laplacian_slide(gray_rgb, {"mask": "zero_sum_diagonal", "mode": "detail"}) sharpened = ensure_uint8(gray_rgb.astype(np.float32) + lap_detail.astype(np.float32)) sobel_img = gradient_abs_sum(gray_rgb, {"operator": "sobel"}) smooth_sobel = box_filter(sobel_img, {"size": 5}) mask = normalize_to_uint8((sharpened.astype(np.float32) * smooth_sobel.astype(np.float32)) / 255.0) summed = ensure_uint8(gray_rgb.astype(np.float32) + mask.astype(np.float32)) return gamma(summed, {"gamma": float(params.get("gamma", 0.5)), "c": 1.0}) def operation(id, label, chapter, slide_group, func, params=None, supports="both"): return { "id": id, "label": label, "chapter": chapter, "slide_group": slide_group, "params": params or {}, "supports": supports, "func": func, } OPERATIONS = [ operation("crop", "Crop", CH_BASIC, "Workspace", crop, {"x": int_param(0, 0, 4000), "y": int_param(0, 0, 4000), "width": int_param(256, 1, 8000), "height": int_param(256, 1, 8000)}), operation("identity", "Identity", CH3, "Point Processing", identity), operation("negative", "Negative", CH3, "Point Processing", negative), operation("log", "Log", CH3, "Point Processing", logarithmic, {"c": float_param(1.44, 0.1, 5, 0.05)}), operation("inverse_log", "Inverse Log", CH3, "Point Processing", inverse_log, {"c": float_param(1.0, 0.1, 5, 0.05)}), operation("gamma", "Power-Law / Gamma", CH3, "Point Processing", gamma, {"gamma": float_param(1.0, 0.1, 5, 0.05), "c": float_param(1.0, 0.1, 3, 0.05)}), operation("threshold", "Thresholding", CH3, "Point Processing", threshold, {"level": int_param(128, 0, 255), "low": int_param(0, 0, 255), "high": int_param(255, 0, 255)}), operation("contrast_stretch", "Contrast Stretching", CH3, "Piecewise Linear", contrast_stretch, {"low": int_param(30, 0, 254), "high": int_param(220, 1, 255)}), operation("gray_slice", "Gray-Level Slicing", CH3, "Piecewise Linear", gray_slice, {"start": int_param(96, 0, 255), "end": int_param(160, 0, 255), "preserve_background": bool_param(True)}), operation("bit_plane", "Bit-Plane Slicing", CH3, "Piecewise Linear", bit_plane, {"bit": int_param(7, 0, 7)}), operation("histeq", "histeq()", CH3, "Histogram Processing", histeq, {"mode": select_param("intensity", ["intensity", "rgb", "grayscale"])}), operation("hist_match", "Histogram Specification", CH3, "Histogram Processing", histogram_matching, {"target": select_param("uniform", ["uniform", "dark", "bright", "bimodal"])}), operation("local_equalization", "Local Enhancement", CH3, "Histogram Processing", local_equalization, {"size": odd_param(7, 31)}), operation("gaussian_noise", "Add Gaussian Noise", CH3, "Noise and Denoising", gaussian_noise, {"mean": float_param(0, -1, 1, 0.01), "variance": float_param(0.01, 0, 0.2, 0.005)}), operation("salt_pepper_noise", "Add Salt & Pepper Noise", CH3, "Noise and Denoising", salt_pepper_noise, {"amount": float_param(0.03, 0, 0.5, 0.01), "salt_ratio": float_param(0.5, 0, 1, 0.05)}), operation("speckle_noise", "Add Speckle Noise", CH3, "Noise and Denoising", speckle_noise, {"variance": float_param(0.04, 0, 0.3, 0.01)}), operation("box_filter", "Box / Average Filter", CH3, "Smoothing Linear Filters", box_filter, {"size": odd_param(3, 35)}), operation("weighted_average", "Weighted Average Filter", CH3, "Smoothing Linear Filters", weighted_average, {"size": odd_param(3, 35)}), operation("gaussian_filter", "Gaussian fspecial Filter", CH3, "Smoothing Linear Filters", gaussian_filter, {"size": odd_param(3, 35), "variance": float_param(1.0, 0.01, 25, 0.1)}), operation("median_filter", "Median Filter", CH3, "Order-Statistics Filters", median_filter, {"size": odd_param(3, 25)}), operation("max_filter", "Max Filter", CH3, "Order-Statistics Filters", max_filter, {"size": odd_param(3, 25)}), operation("min_filter", "Min Filter", CH3, "Order-Statistics Filters", min_filter, {"size": odd_param(3, 25)}), operation("laplacian_slide", "Laplacian Masks", CH3, "Sharpening Spatial Filters", laplacian_slide, {"mask": select_param("cross", ["cross", "diagonal", "zero_sum_cross", "zero_sum_diagonal"]), "mode": select_param("sharpen", ["sharpen", "detail"])}), operation("gradient_abs_sum", "Gradient abs(imfilter Gx)+abs(imfilter Gy)", CH3, "Gradient Operator", gradient_abs_sum, {"operator": select_param("sobel", ["sobel", "roberts"])}), operation("sobel", "Sobel Magnitude", CH3, "Gradient Operator", sobel), operation("roberts", "Roberts Magnitude", CH3, "Gradient Operator", roberts), operation("high_boost", "High-Boost Filtering", CH3, "High-Boost Filtering", high_boost, {"amplification": float_param(1.5, 1, 6, 0.1), "size": odd_param(3, 35)}), operation("bone_scan_workflow", "Bone Scan Workflow Preset", CH3, "Combining Spatial Enhancement Methods", bone_scan_workflow, {"gamma": float_param(0.5, 0.1, 2, 0.05)}), operation("fft_spectrum", "FFT/DFT Spectrum View", CH4, "DFT and FFT", fft_spectrum, {"mode": select_param("log_magnitude", ["magnitude", "log_magnitude", "phase"])}), operation("frequency_filter", "Ideal/Butterworth/Gaussian Frequency Filter", CH4, "Frequency Domain Filtering", frequency_filter, {"family": select_param("gaussian", ["ideal", "butterworth", "gaussian"]), "kind": select_param("lowpass", ["lowpass", "highpass"]), "cutoff": float_param(40, 1, 512, 1), "order": int_param(2, 1, 10), "output": select_param("image", ["image", "mask"])}), operation("frequency_laplacian", "Laplacian in Frequency Domain", CH4, "Sharpening Highpass Filtering", frequency_laplacian), operation("convolution", "Convolution Utility", CH4, "Convolution", convolution), operation("correlation", "Correlation Utility", CH4, "Correlation", correlation), operation("rgb_channel", "RGB Channel View", CH6, "RGB color model", rgb_channel, {"channel": select_param("r", ["r", "g", "b"])}), operation("hsi_view", "HSI Component View", CH6, "HSI color model", hsi_view, {"component": select_param("i", ["h", "s", "i"])}), operation("hsi_intensity_filter", "HSI Intensity Processing", CH6, "HSI color model", hsi_intensity_filter, {"method": select_param("smooth", ["smooth", "sharpen"]), "size": odd_param(3, 25)}), operation("pseudo_color_slices", "Pseudocolor Intensity Slicing", CH6, "Pseudocolor Image Processing", pseudo_color_slices), operation("gray_to_color_sinusoidal", "Gray-Level to Color Transform", CH6, "Gray level to color transformation", gray_to_color_sinusoidal, {"hue_frequency": float_param(1, 0.2, 4, 0.1), "saturation_frequency": float_param(0.5, 0.1, 4, 0.1), "intensity_frequency": float_param(0.25, 0.1, 4, 0.1)}), ] OPERATION_MAP = {item["id"]: item for item in OPERATIONS} def operation_metadata(): return [{key: value for key, value in item.items() if key != "func"} for item in OPERATIONS] def apply_registered_operation(image, operation_id, params=None): item = OPERATION_MAP.get(operation_id) if item is None: raise ProcessingError(f"Unsupported operation '{operation_id}'.") return ensure_uint8(item["func"](ensure_uint8(image), params or {}))