Spaces:
Sleeping
Sleeping
| """Single source of truth for image preprocessing. | |
| Both train.py and app.py import from this module instead of keeping their | |
| own copies. That shared-module structure is the point: it makes | |
| training/serving skew structurally impossible rather than merely unlikely. | |
| """ | |
| import cv2 | |
| import numpy as np | |
| from tensorflow.keras.applications.xception import preprocess_input | |
| IMAGE_SIZE = 299 | |
| SIGMA_X = 10 | |
| def crop_image_from_gray(img, tol=7): | |
| """Crop the black border surrounding the circular retinal fundus.""" | |
| if img.ndim == 2: | |
| mask = img > tol | |
| return img[np.ix_(mask.any(1), mask.any(0))] | |
| gray_img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) | |
| mask = gray_img > tol | |
| check_shape = img[:, :, 0][np.ix_(mask.any(1), mask.any(0))].shape[0] | |
| if check_shape == 0: | |
| # Degenerate all-dark image: cropping would leave nothing, so | |
| # return the input unchanged rather than an empty array. | |
| return img | |
| img1 = img[:, :, 0][np.ix_(mask.any(1), mask.any(0))] | |
| img2 = img[:, :, 1][np.ix_(mask.any(1), mask.any(0))] | |
| img3 = img[:, :, 2][np.ix_(mask.any(1), mask.any(0))] | |
| return np.stack([img1, img2, img3], axis=-1) | |
| def _ben_graham(image_rgb, sigma_x): | |
| """Crop, resize, and apply the Ben Graham high-pass transform. | |
| Shared by preprocess_image and preprocess_array so the two entry | |
| points can never drift apart. | |
| """ | |
| image = crop_image_from_gray(image_rgb) | |
| image = cv2.resize(image, (IMAGE_SIZE, IMAGE_SIZE)) | |
| # 4*I - 4*blur(I) + 128: the heavy Gaussian blur captures only the | |
| # low-frequency content of the image (illumination, colour cast, | |
| # retinal pigmentation), all of which vary by camera and patient. | |
| # Subtracting that blur is a high-pass filter, leaving behind the | |
| # fine structure that actually matters: vessels, microaneurysms, | |
| # exudates and haemorrhages. The +128 re-centers the result into a | |
| # displayable range instead of clipping around zero. | |
| image = cv2.addWeighted(image, 4, cv2.GaussianBlur(image, (0, 0), sigma_x), -4, 128) | |
| return image | |
| def preprocess_image(image_path, sigma_x=SIGMA_X): | |
| """Load an image from disk and preprocess it for the model. | |
| Returns a float32 array scaled to [-1, 1] by Xception's | |
| preprocess_input. | |
| """ | |
| image = cv2.imread(image_path) | |
| image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) | |
| image = _ben_graham(image, sigma_x) | |
| return preprocess_input(image.astype(np.float32)) | |
| def preprocess_array(image_rgb, sigma_x=SIGMA_X): | |
| """Preprocess an in-memory RGB array (e.g. from the Gradio app). | |
| Must produce output identical to preprocess_image for the same | |
| underlying image, since it shares the crop/resize/high-pass helper. | |
| """ | |
| image = _ben_graham(image_rgb, sigma_x) | |
| return preprocess_input(image.astype(np.float32)) | |
| def denormalize_for_display(image): | |
| """Map a [-1, 1]-scaled image back to [0, 1] for matplotlib.""" | |
| return (image + 1.0) / 2.0 | |