"""Dataset utilities for seaweed binary segmentation.""" from __future__ import annotations from pathlib import Path import albumentations as A import numpy as np import rasterio import torch from PIL import Image from torch.utils.data import Dataset from torchvision import transforms IMAGE_EXTS = {".tif", ".tiff", ".png", ".jpg", ".jpeg"} MASK_EXTS = (".png", ".tif", ".tiff", ".jpg", ".jpeg") class SeaweedSegmentationDataset(Dataset): def __init__(self, image_dir, mask_dir, transform=None, target_size=256, use_4channel=True): self.image_dir = Path(image_dir) self.mask_dir = Path(mask_dir) self.transform = transform self.target_size = int(target_size) self.use_4channel = bool(use_4channel) if not self.image_dir.exists(): raise FileNotFoundError(f"Image directory not found: {self.image_dir}") if not self.mask_dir.exists(): raise FileNotFoundError(f"Mask directory not found: {self.mask_dir}") self.images = sorted(p.name for p in self.image_dir.iterdir() if p.is_file() and p.suffix.lower() in IMAGE_EXTS) self.normalize_3ch = transforms.Normalize(mean=(0.430, 0.411, 0.296), std=(0.213, 0.156, 0.143)) self.normalize_4ch = transforms.Normalize(mean=(0.430, 0.411, 0.296, 0.350), std=(0.213, 0.156, 0.143, 0.180)) def __len__(self): return len(self.images) def find_mask_path(self, image_name: str) -> Path: stem = Path(image_name).stem for ext in MASK_EXTS: for suffix in (ext, ext.upper()): candidate = self.mask_dir / f"{stem}{suffix}" if candidate.exists(): return candidate return self.mask_dir / f"{stem}.png" @staticmethod def read_raster(path: Path) -> np.ndarray: if path.suffix.lower() in {".tif", ".tiff"}: with rasterio.open(path) as src: return np.transpose(src.read(), (1, 2, 0)) image = Image.open(path) return np.asarray(image) @staticmethod def extract_432_bands(image: np.ndarray) -> np.ndarray: if image.ndim == 2: image = image[:, :, None] if image.shape[2] >= 4: return image[:, :, [3, 2, 1]] output = image[:, :, : min(3, image.shape[2])] while output.shape[2] < 3: output = np.concatenate([output, output[:, :, -1:]], axis=2) return output @staticmethod def read_mask(path: Path, fallback_shape: tuple[int, int]) -> np.ndarray: if not path.exists(): return np.zeros(fallback_shape, dtype=np.uint8) if path.suffix.lower() in {".tif", ".tiff"}: with rasterio.open(path) as src: mask = src.read(1) else: mask = np.asarray(Image.open(path).convert("L")) return (mask > 127).astype(np.uint8) def __getitem__(self, idx): img_name = self.images[idx] img_path = self.image_dir / img_name mask_path = self.find_mask_path(img_name) try: image = self.read_raster(img_path) if image.ndim == 2: image = image[:, :, None] mask = self.read_mask(mask_path, image.shape[:2]) if self.use_4channel and image.shape[2] >= 4: processed = image[:, :, :4] processed = torch.from_numpy(processed.astype(np.float32)) if processed.max() > 1.0: processed = processed / 65535.0 processed = self.normalize_4ch(processed.permute(2, 0, 1)) else: processed = self.extract_432_bands(image) processed = torch.from_numpy(processed.astype(np.float32)) if processed.max() > 1.0: processed = processed / 65535.0 processed = self.normalize_3ch(processed.permute(2, 0, 1)) mask_tensor = torch.from_numpy(mask).long() if self.target_size != processed.shape[1] or self.target_size != processed.shape[2]: processed = transforms.Resize((self.target_size, self.target_size), antialias=True)(processed) mask_tensor = transforms.Resize( (self.target_size, self.target_size), interpolation=transforms.InterpolationMode.NEAREST, )(mask_tensor.unsqueeze(0)).squeeze(0) if self.transform: augmented = self.transform(image=processed.permute(1, 2, 0).numpy(), mask=mask_tensor.numpy()) processed = torch.from_numpy(augmented["image"]).permute(2, 0, 1).float() mask_tensor = torch.from_numpy(augmented["mask"]).long() return {"image": processed, "mask": mask_tensor, "filename": img_name} except Exception as exc: print(f"Error loading {img_name}: {exc}") return None def get_train_transforms(target_size=256, use_4channel=True): return A.Compose( [ A.Resize(target_size, target_size), A.HorizontalFlip(p=0.5), A.VerticalFlip(p=0.3), A.RandomRotate90(p=0.3), A.ShiftScaleRotate(shift_limit=0.1, scale_limit=0.1, rotate_limit=15, p=0.5), A.RandomBrightnessContrast(p=0.3), A.GaussNoise(p=0.2), ] ) def get_val_transforms(target_size=256, use_4channel=True): return None