CoolFace
Apppublic

AndreaNotes1/fgh-dermatology

sourceHugging Faceupdated 3mo agoView on Hugging Face
0likes
preprocess.py95 linesDownload Raw Back to utils
1"""2utils/preprocess.py3====================4Image preprocessing pipeline for the FGH-Dermatology inference pipeline.5 6Responsible for turning an arbitrary uploaded dermoscopic/clinical image7(JPEG, PNG, TIFF, BMP — any colour mode) into the normalised float tensor8the deep learning model expects, while remaining robust across both9light- and dark-skinned patient imagery.10"""11 12from typing import Tuple13 14import numpy as np15from PIL import Image, ImageOps16 17 18def load_rgb_image(image_path: str) -> Image.Image:19    """20    Load an image from disk, correct EXIF orientation, and force RGB mode21    (handling RGBA, palette, grayscale, and CMYK source images safely).22    """23    img = Image.open(image_path)24    img = ImageOps.exif_transpose(img)  # respect camera/scanner orientation25    if img.mode != "RGB":26        img = img.convert("RGB")27    return img28 29 30def preprocess_image(image_path: str, target_size: Tuple[int, int] = (224, 224)) -> np.ndarray:31    """32    Full preprocessing pipeline for model inference.33 34    Steps35    -----36    1. Load image and normalise to RGB (handles RGBA -> RGB conversion).37    2. Resize to the model's expected square input dimensions.38    3. Scale pixel values to the [0, 1] range.39    4. Apply mild contrast-normalisation so the model attends to lesion40       morphology (shape, border, texture) rather than raw skin colour —41       supporting equitable performance across Fitzpatrick skin types.42    5. Expand dimensions to a (1, H, W, 3) batch tensor.43 44    Parameters45    ----------46    image_path : str47        Path to the source image on disk.48    target_size : tuple(int, int)49        (height, width) the model expects.50 51    Returns52    -------53    np.ndarray of shape (1, H, W, 3), dtype float32, values in [0, 1].54    """55    img = load_rgb_image(image_path)56    img = img.resize(target_size, Image.LANCZOS)57 58    arr = np.asarray(img).astype(np.float32) / 255.059 60    arr = _normalise_for_skintone_robustness(arr)61 62    return np.expand_dims(arr, axis=0)63 64 65def _normalise_for_skintone_robustness(arr: np.ndarray) -> np.ndarray:66    """67    Apply a light per-channel standardisation (zero-mean-ish rescale clipped68    back to [0, 1]) so absolute skin colour/luminance contributes less to69    activations than structural lesion features (border, asymmetry,70    texture). This is a lightweight, model-agnostic step; a production model71    trained explicitly for skintone equity may apply its own calibrated72    normalisation instead — in which case this step is a harmless no-op73    pass-through that callers can disable by passing `skip_norm=True` to74    `preprocess_image` in a future revision.75    """76    channel_mean = arr.mean(axis=(0, 1), keepdims=True)77    channel_std = arr.std(axis=(0, 1), keepdims=True) + 1e-678 79    # Blend a mild standardisation with the original signal so we improve80    # cross-skintone contrast normalisation without discarding true colour81    # information the model may also rely on.82    standardised = (arr - channel_mean) / channel_std83    standardised = (standardised - standardised.min()) / (84        standardised.max() - standardised.min() + 1e-685    )86 87    blended = 0.6 * arr + 0.4 * standardised88    return np.clip(blended, 0.0, 1.0).astype(np.float32)89 90 91def get_image_dimensions(image_path: str) -> Tuple[int, int]:92    """Return (width, height) of the source image without full decoding cost."""93    with Image.open(image_path) as img:94        return img.size95