retinopathy-detector / preprocess.py
suhanii23's picture
Fix argmax/softmax bug and normalization mismatch in inference (preprocess.py)
c5c411b verified
Raw
History Blame Contribute Delete
2.96 kB
"""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