Coverage for supercat/data.py: 100.00%
143 statements
« prev ^ index » next coverage.py v7.10.6, created at 2026-09-11 13:26 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2026-09-11 13:26 +0000
1from pathlib import Path
2import numpy as np
3import torch
4from torch.utils.data import Dataset
5from skimage.transform import rescale, resize
6import hdf5storage
7from PIL import Image
10def read_image(path: str | Path, size: tuple[int, int, int] | None = None):
11 path = Path(path)
12 suffix = path.suffix.lower()
13 if suffix in [".png", ".jpg", ".jpeg"]:
14 img = Image.open(path).convert("L")
15 if size is not None:
16 size = size[:2]
17 img = img.resize(size[::-1], Image.BICUBIC)
18 elif suffix in [".mat"]:
19 img = read_mat(path)
20 assert img.ndim == 3, f"Expected 3D array from {path}, got shape {img.shape}"
21 if size is not None:
22 img = resize(
23 img,
24 output_shape=size,
25 order=3, # cubic
26 mode="reflect",
27 anti_aliasing=False, # no AA on upscaling
28 preserve_range=True,
29 )
30 else:
31 raise ValueError(f"Unsupported image format: {suffix}")
33 array = np.array(img, dtype=np.float32)
34 return np.expand_dims(array, axis=0)
37def transform_scale(data):
38 return 2.0 * data / 255.0 - 1
41def read_image_as_tensor(path, **kwargs) -> torch.Tensor:
42 image = read_image(path, **kwargs)
43 image = transform_scale(image)
45 image = torch.from_numpy(image.copy()) # ensure contiguous
47 return image
50TRANSFORMATIONS_3D = [
51 lambda tensor: tensor, # Identity
52 lambda tensor: tensor.flip(-2).transpose(-1, -2), # 90° rotation
53 lambda tensor: tensor.flip(-2).flip(-1), # 180° rotation
54 lambda tensor: tensor.flip(-2)
55 .transpose(-1, -2)
56 .flip(-1), # TR-BL diagonal reflection
57 lambda tensor: tensor.flip(-1), # Vertical reflection
58 lambda tensor: tensor.flip(-2), # Horizontal reflection
59 lambda tensor: tensor.flip(-1).transpose(-1, -2), # TL-BR diagonal reflection
60 lambda tensor: tensor.flip(-1)
61 .flip(-2)
62 .transpose(-1, -2), # TR-BL diagonal reflection with both flips
63 lambda tensor: tensor.flip(-3),
64 lambda tensor: tensor.flip(-1).flip(-3),
65 lambda tensor: tensor.flip(-2).flip(-3),
66 lambda tensor: tensor.flip(-1).flip(-2).flip(-3),
67 lambda tensor: tensor.transpose(-1, -2).flip(-3),
68 lambda tensor: tensor.transpose(-1, -2).flip(-1).flip(-3),
69 lambda tensor: tensor.transpose(-1, -2).flip(-2).flip(-3),
70 lambda tensor: tensor.transpose(-1, -2).flip(-1).flip(-2).flip(-3),
71 lambda tensor: tensor.transpose(-1, -3),
72 lambda tensor: tensor.transpose(-1, -3).flip(-1),
73 lambda tensor: tensor.transpose(-1, -3).flip(-2),
74 lambda tensor: tensor.transpose(-1, -3).flip(-3),
75 lambda tensor: tensor.transpose(-1, -3).flip(-1).flip(-2),
76 lambda tensor: tensor.transpose(-1, -3).flip(-1).flip(-3),
77 lambda tensor: tensor.transpose(-1, -3).flip(-2).flip(-3),
78 lambda tensor: tensor.transpose(-1, -3).flip(-1).flip(-2).flip(-3),
79 lambda tensor: tensor.transpose(-2, -3),
80 lambda tensor: tensor.transpose(-2, -3).flip(-1),
81 lambda tensor: tensor.transpose(-2, -3).flip(-2),
82 lambda tensor: tensor.transpose(-2, -3).flip(-3),
83 lambda tensor: tensor.transpose(-2, -3).flip(-1).flip(-2),
84 lambda tensor: tensor.transpose(-2, -3).flip(-1).flip(-3),
85 lambda tensor: tensor.transpose(-2, -3).flip(-2).flip(-3),
86 lambda tensor: tensor.transpose(-2, -3).flip(-1).flip(-2).flip(-3),
87 lambda tensor: tensor.transpose(-2, -3).transpose(-1, -2),
88 lambda tensor: tensor.transpose(-2, -3).transpose(-1, -2).flip(-1),
89 lambda tensor: tensor.transpose(-2, -3).transpose(-1, -2).flip(-2),
90 lambda tensor: tensor.transpose(-2, -3).transpose(-1, -2).flip(-3),
91 lambda tensor: tensor.transpose(-2, -3).transpose(-1, -2).flip(-1).flip(-2),
92 lambda tensor: tensor.transpose(-2, -3).transpose(-1, -2).flip(-1).flip(-3),
93 lambda tensor: tensor.transpose(-2, -3).transpose(-1, -2).flip(-2).flip(-3),
94 lambda tensor: tensor.transpose(-2, -3)
95 .transpose(-1, -2)
96 .flip(-1)
97 .flip(-2)
98 .flip(-3),
99 lambda tensor: tensor.transpose(-1, -2).transpose(-2, -3),
100 lambda tensor: tensor.transpose(-1, -2).transpose(-2, -3).flip(-1),
101 lambda tensor: tensor.transpose(-1, -2).transpose(-2, -3).flip(-2),
102 lambda tensor: tensor.transpose(-1, -2).transpose(-2, -3).flip(-3),
103 lambda tensor: tensor.transpose(-1, -2).transpose(-2, -3).flip(-1).flip(-2),
104 lambda tensor: tensor.transpose(-1, -2).transpose(-2, -3).flip(-1).flip(-3),
105 lambda tensor: tensor.transpose(-1, -2).transpose(-2, -3).flip(-2).flip(-3),
106 lambda tensor: tensor.transpose(-1, -2)
107 .transpose(-2, -3)
108 .flip(-1)
109 .flip(-2)
110 .flip(-3),
111]
112TRANSFORMATIONS_2D = TRANSFORMATIONS_3D[:8]
114# def flip_and_rotate(
115# batch:tuple[torch.Tensor,torch.Tensor,torch.Tensor],
116# sym_id:int|None=None
117# ) -> tuple[torch.Tensor,torch.Tensor,torch.Tensor]:
118# """
119# Applies a random or specified flip and/or rotation transformation to a batch of 2D or 3D tensors.
120# This function transforms the input, target, and residual tensors by flipping, rotating, and
121# transposing them in a consistent manner.
123# Args:
124# batch (tuple[torch.Tensor, torch.Tensor, torch.Tensor]): A tuple containing the input, target,
125# and residual tensors to be transformed.
126# sym_id (int or None, optional): The transformation identifier. If None, a random transformation
127# is chosen. For 2D tensors, sym_id ranges from 0 to 7; for 3D tensors, it ranges from 0 to 47.
129# Returns:
130# tuple[torch.Tensor, torch.Tensor, torch.Tensor]: A tuple containing the transformed input,
131# target, and residual tensors after applying the flip and/or rotation.
133# Raises:
134# AssertionError: If the sym_id is out of the valid range for the tensor dimensions.
135# """
136# input, target, residual = batch
138# # Determine if 2D (4D) or 3D (5D)
139# is_2d = (input.ndim == 4)
140# number_of_transforms = 8 if is_2d else len(TRANSFORMATIONS)
142# if sym_id is None:
143# # Randomly choose one of the transformations
144# sym_id = random.randint(0, number_of_transforms - 1)
146# # Shortcut for no transformation
147# if sym_id == 0:
148# return batch
150# assert 0 <= sym_id < number_of_transforms
151# transformation = TRANSFORMATIONS[sym_id]
153# input = transformation(input)
154# target = transformation(target)
155# residual = transformation(residual)
157# return input, target, residual
160def downscale_tricubic_rescale(vol: np.ndarray, factor: int = 4) -> np.ndarray:
161 """
162 Downscale a 3D volume by `factor` using tricubic interpolation + anti-aliasing.
163 """
164 low = rescale(
165 vol,
166 scale=1.0 / factor,
167 order=3, # cubic
168 mode="reflect",
169 anti_aliasing=True,
170 preserve_range=True,
171 channel_axis=None, # ensures (D,H,W) or (C,D,H,W) handled correctly if needed
172 )
173 return low.astype(vol.dtype, copy=False)
176def upscale_tricubic_rescale(vol: np.ndarray, factor: int = 4) -> np.ndarray:
177 """
178 Upscale a 3D volume by `factor` using tricubic interpolation.
179 """
180 up = rescale(
181 vol,
182 scale=factor,
183 order=3, # cubic
184 mode="reflect",
185 anti_aliasing=False, # no AA on upscaling
186 preserve_range=True,
187 channel_axis=None,
188 )
189 return up.astype(vol.dtype, copy=False)
192def read_mat(path: Path):
193 DEEPROCK_HDF5_KEY = "temp"
194 try:
195 data_dict = hdf5storage.loadmat(str(path))
196 except Exception as err:
197 raise IOError(f"Error reading 3D file '{path}':\n{err}")
198 if DEEPROCK_HDF5_KEY not in data_dict:
199 keys_found = ",".join(data_dict.keys())
200 raise Exception(
201 f"expected key {DEEPROCK_HDF5_KEY} not found in '{path}'.\nCheck the following keys: {keys_found}"
202 )
204 return data_dict[DEEPROCK_HDF5_KEY]
207class Deeprock3D(Dataset):
208 def __init__(
209 self,
210 deeprock: Path,
211 scale: int = 4,
212 channel_first: bool = True,
213 partition: str = "train",
214 augment: bool = False,
215 ):
216 self.deeprock = Path(deeprock)
217 self.scale = int(scale)
218 self.channel_first = channel_first
219 categories = ["sandstone", "carbonate", "coal", "sand"]
220 self.augment = augment
222 hr_items: list[Path] = []
223 for cat in categories:
224 hr_dir = self.deeprock / f"{cat}3D" / f"{cat}3D_{partition}_HR"
225 hr_items.extend(sorted(hr_dir.glob("*.mat")))
226 self.hr_items = hr_items
228 if len(self.hr_items) == 0:
229 raise FileNotFoundError(
230 f"No .mat files found under {self.deeprock}/*3D/*3D_{partition}_HR"
231 )
233 def _hr_to_lr_path(self, hr_path: Path) -> Path:
234 """
235 Map .../<cat>3D/<cat>3D_train_HR/<file>.mat -->
236 .../<cat>3D/<cat>3D_train_TRI_unknown_X{scale}/<file>.mat
237 """
238 rel = hr_path.relative_to(self.deeprock)
239 parts = list(rel.parts)
240 # parts[-2] expected: "<cat>3D_train_HR"
241 parent_name = parts[-2]
242 if not parent_name.endswith("_HR"):
243 raise ValueError(f"Unexpected parent '{parent_name}' for {hr_path}")
244 parts[-2] = parent_name.replace("_HR", f"_LR_default_X{self.scale}").replace(
245 ".mat", f"x{self.scale}.mat"
246 )
247 parts[-1] = parts[-1].replace(".mat", f"x{self.scale}.mat")
248 return self.deeprock.joinpath(*parts)
250 def __len__(self) -> int:
251 return len(self.hr_items)
253 def __getitem__(self, idx: int) -> tuple[torch.Tensor, torch.Tensor]:
254 hr_path = self.hr_items[idx]
256 lr_path = self._hr_to_lr_path(hr_path)
258 # if not lr_path.exists():
259 # raise FileNotFoundError(f"LR file missing for {hr_path.name}: {lr_path}")
261 hr = transform_scale(read_mat(hr_path))
262 lr_orig = transform_scale(read_mat(lr_path))
263 # lr_orig = downscale_tricubic_rescale(lr_orig, factor=self.scale)
264 lr = upscale_tricubic_rescale(lr_orig, factor=self.scale)
266 # lr = transform_scale(read_mat(lr_path))
268 # Optional: enforce channel-first tensors
269 # If (D,H,W), add channel dim; if already (C,D,H,W), leave it.
270 if hr.ndim == 3:
271 hr = hr[None, ...] # (1,D,H,W)
272 if lr.ndim == 3:
273 lr = lr[None, ...]
274 if not self.channel_first and hr.ndim == 4:
275 # Convert to (D,H,W,C)
276 hr = np.moveaxis(hr, 0, -1)
277 lr = np.moveaxis(lr, 0, -1)
279 hr_t = torch.from_numpy(hr.copy()) # ensure contiguous
280 lr_t = torch.from_numpy(lr.copy())
282 if self.augment:
283 transformation = TRANSFORMATIONS_3D[
284 np.random.randint(0, len(TRANSFORMATIONS_3D))
285 ]
286 hr_t = transformation(hr_t)
287 lr_t = transformation(lr_t)
289 assert (
290 lr_t.shape == hr_t.shape
291 ), f"LR shape {lr_t.shape} != HR shape {hr_t.shape} for {hr_path.name} - {lr_path.name}, original LR shape {lr_orig.shape}. scale={self.scale}"
292 assert (
293 lr_t.max() < 1.01
294 ), f"Low resolution {lr_path} gives range {lr_t.min()}-{lr_t.max()}"
295 assert (
296 lr_t.min() > -1.01
297 ), f"Low resolution {lr_path} gives range {lr_t.min()}-{lr_t.max()}"
298 assert (
299 hr_t.max() < 1.01
300 ), f"High resolution {hr_path} gives range {hr_t.min()}-{hr_t.max()}"
301 assert (
302 hr_t.min() > -1.01
303 ), f"High resolution {hr_path} gives range {hr_t.min()}-{hr_t.max()}"
305 return lr_t, hr_t
308class Deeprock2D(Dataset):
309 def __init__(
310 self,
311 deeprock: Path,
312 scale: int = 4,
313 channel_first: bool = True,
314 partition: str = "train",
315 augment: bool = False,
316 ):
317 self.deeprock = Path(deeprock)
318 self.scale = int(scale)
319 self.channel_first = channel_first
320 categories = ["sandstone", "carbonate", "coal", "sand"]
321 self.augment = augment
323 hr_items: list[Path] = []
324 for cat in categories:
325 hr_dir = self.deeprock / f"{cat}2D" / f"{cat}2D_{partition}_HR"
326 hr_items.extend(sorted(hr_dir.glob("*.png")))
327 self.hr_items = hr_items
329 if len(self.hr_items) == 0:
330 raise FileNotFoundError(
331 f"No .png files found under {self.deeprock}/*2D/*2D_{partition}_HR"
332 )
334 def _hr_to_lr_path(self, hr_path: Path) -> Path:
335 """
336 Map .../<cat>2D/<cat>2D_train_HR/<file>.png -->
337 .../<cat>2D/<cat>2D_train_BI_unknown_X{scale}/<file>.png
338 """
339 rel = hr_path.relative_to(self.deeprock)
340 parts = list(rel.parts)
341 # parts[-2] expected: "<cat>2D_train_HR"
342 parent_name = parts[-2]
343 if not parent_name.endswith("_HR"):
344 raise ValueError(f"Unexpected parent '{parent_name}' for {hr_path}")
345 parts[-2] = parent_name.replace("_HR", f"_BI_unknown_X{self.scale}")
346 return self.deeprock.joinpath(*parts)
348 def __len__(self) -> int:
349 return len(self.hr_items)
351 def __getitem__(self, idx: int) -> tuple[torch.Tensor, torch.Tensor]:
352 hr_path = self.hr_items[idx]
353 lr_path = self._hr_to_lr_path(hr_path)
355 if not lr_path.exists():
356 raise FileNotFoundError(f"LR file missing for {hr_path.name}: {lr_path}")
358 hr_t = read_image_as_tensor(hr_path)
359 lr_t = read_image_as_tensor(lr_path)
361 assert hr_t.shape[0] == 1
363 if self.augment:
364 transformation = TRANSFORMATIONS_2D[
365 np.random.randint(0, len(TRANSFORMATIONS_2D))
366 ]
367 hr_t = transformation(hr_t)
368 lr_t = transformation(lr_t)
370 return lr_t, hr_t
373def build_datasets3D(deeprock: Path, scale: int = 4, train_augment: bool = True):
374 training_dataset = Deeprock3D(
375 deeprock=deeprock, scale=scale, partition="train", augment=train_augment
376 )
377 validation_dataset = Deeprock3D(
378 deeprock=deeprock, scale=scale, partition="valid", augment=False
379 )
380 return training_dataset, validation_dataset
383def build_datasets2D(deeprock: Path, scale: int = 4, train_augment: bool = True):
384 training_dataset = Deeprock2D(
385 deeprock=deeprock, scale=scale, partition="train", augment=train_augment
386 )
387 validation_dataset = Deeprock2D(
388 deeprock=deeprock, scale=scale, partition="valid", augment=False
389 )
390 return training_dataset, validation_dataset