From e364d062174bdeb14b1a87eead8d3e5ba525380b Mon Sep 17 00:00:00 2001 From: Martin Laasmaa Date: Tue, 16 Dec 2025 23:02:45 +0200 Subject: [PATCH] Implementing uint16 reading with tifffile --- config/app_config.yaml | 57 ------------------------------------ src/gui/tabs/training_tab.py | 8 +++++ src/model/yolo_wrapper.py | 29 +++++++++++++++--- src/utils/image.py | 3 +- 4 files changed, 35 insertions(+), 62 deletions(-) delete mode 100644 config/app_config.yaml diff --git a/config/app_config.yaml b/config/app_config.yaml deleted file mode 100644 index 9d15cbf..0000000 --- a/config/app_config.yaml +++ /dev/null @@ -1,57 +0,0 @@ -database: - path: data/detections.db -image_repository: - base_path: '' - allowed_extensions: - - .jpg - - .jpeg - - .png - - .tif - - .tiff - - .bmp -models: - default_base_model: yolov8s-seg.pt - models_directory: data/models - base_model_choices: - - yolov8s-seg.pt - - yolo11s-seg.pt -training: - default_epochs: 100 - default_batch_size: 16 - default_imgsz: 1024 - default_patience: 50 - default_lr0: 0.01 - two_stage: - enabled: false - stage1: - epochs: 20 - lr0: 0.0005 - patience: 10 - freeze: 10 - stage2: - epochs: 150 - lr0: 0.0003 - patience: 30 - last_dataset_yaml: /home/martin/code/object_detection/data/datasets/data.yaml - last_dataset_dir: /home/martin/code/object_detection/data/datasets -detection: - default_confidence: 0.25 - default_iou: 0.45 - max_batch_size: 100 -visualization: - bbox_colors: - organelle: '#FF6B6B' - membrane_branch: '#4ECDC4' - default: '#00FF00' - bbox_thickness: 2 - font_size: 12 -export: - formats: - - csv - - json - - excel - default_format: csv -logging: - level: INFO - file: logs/app.log - format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s' diff --git a/src/gui/tabs/training_tab.py b/src/gui/tabs/training_tab.py index 5200eb9..058b5ae 100644 --- a/src/gui/tabs/training_tab.py +++ b/src/gui/tabs/training_tab.py @@ -1303,6 +1303,14 @@ class TrainingTab(QWidget): sample_image = self._find_first_image(images_dir) if not sample_image: return False + + # Do not force an RGB cache for TIFF datasets. + # We handle grayscale/16-bit TIFFs via runtime Ultralytics patches that: + # - load TIFFs with `tifffile` + # - replicate grayscale to 3 channels without quantization + # - normalize uint16 correctly during training + if sample_image.suffix.lower() in {".tif", ".tiff"}: + return False try: img = Image(sample_image) return img.pil_image.mode.upper() != "RGB" diff --git a/src/model/yolo_wrapper.py b/src/model/yolo_wrapper.py index f08ef69..21b1931 100644 --- a/src/model/yolo_wrapper.py +++ b/src/model/yolo_wrapper.py @@ -1,9 +1,13 @@ -""" -YOLO model wrapper for the microscopy object detection application. -Provides a clean interface to YOLOv8 for training, validation, and inference. +"""YOLO model wrapper for the microscopy object detection application. + +Notes on 16-bit TIFF support: +- Ultralytics training defaults assume 8-bit images and normalize by dividing by 255. +- This project can patch Ultralytics at runtime to decode TIFFs via `tifffile` and + normalize `uint16` correctly. + +See [`apply_ultralytics_16bit_tiff_patches()`](src/utils/ultralytics_16bit_patch.py:1). """ -from ultralytics import YOLO from pathlib import Path from typing import Optional, List, Dict, Callable, Any import torch @@ -11,6 +15,7 @@ import tempfile import os from src.utils.image import Image from src.utils.logger import get_logger +from src.utils.ultralytics_16bit_patch import apply_ultralytics_16bit_tiff_patches logger = get_logger(__name__) @@ -31,6 +36,9 @@ class YOLOWrapper: self.device = "cuda" if torch.cuda.is_available() else "cpu" logger.info(f"YOLOWrapper initialized with device: {self.device}") + # Apply Ultralytics runtime patches early (before first import/instantiation of YOLO datasets/trainers). + apply_ultralytics_16bit_tiff_patches() + def load_model(self) -> bool: """ Load YOLO model from path. @@ -40,6 +48,9 @@ class YOLOWrapper: """ try: logger.info(f"Loading YOLO model from {self.model_path}") + # Import YOLO lazily to ensure runtime patches are applied first. + from ultralytics import YOLO + self.model = YOLO(self.model_path) self.model.to(self.device) logger.info("Model loaded successfully") @@ -89,6 +100,16 @@ class YOLOWrapper: f"Data: {data_yaml}, Epochs: {epochs}, Batch: {batch}, ImgSz: {imgsz}" ) + # Defaults for 16-bit safety: disable augmentations that force uint8 and HSV ops that assume 0..255. + # Users can override by passing explicit kwargs. + kwargs.setdefault("mosaic", 0.0) + kwargs.setdefault("mixup", 0.0) + kwargs.setdefault("cutmix", 0.0) + kwargs.setdefault("copy_paste", 0.0) + kwargs.setdefault("hsv_h", 0.0) + kwargs.setdefault("hsv_s", 0.0) + kwargs.setdefault("hsv_v", 0.0) + # Train the model results = self.model.train( data=data_yaml, diff --git a/src/utils/image.py b/src/utils/image.py index acc5f11..578ccde 100644 --- a/src/utils/image.py +++ b/src/utils/image.py @@ -313,7 +313,8 @@ class Image: """String representation of the Image object.""" return ( f"Image(path='{self.path.name}', " - f"shape=({self._width}x{self._height}x{self._channels}), " + # Display as HxWxC to match the conventional NumPy shape semantics. + f"shape=({self._height}x{self._width}x{self._channels}), " f"format={self._format}, " f"size={self.size_mb:.2f}MB)" )