Dataset information


source

MedDataset

def MedDataset(
    dataframe:NoneType=None, image_col:str=None, mask_col:str='mask_path', path:NoneType=None,
    img_list:NoneType=None, postfix:str='', apply_reorder:bool=True,
    dtype:(<class 'fastMONAI.vision_core.MedImage'>, <class 'fastMONAI.vision_core.MedMask'>)=MedImage,
    max_workers:int=1, use_cache:bool=True, cache_path:NoneType=None
):

A class to extract and present information about the dataset.


source

suggest_patch_size

def suggest_patch_size(
    dataset:MedDataset, target_spacing:list=None, min_patch_size:list=None, max_patch_size:list=None, divisor:int=16
)->list:

Suggest optimal patch size based on dataset dimensions.

Uses median shape as the starting point but clamps to the minimum volume size per axis, ensuring the suggested patch fits ALL volumes without requiring padding during training.

Algorithm: 1. Use min(median, min_volume) per axis for safety 2. Round down to nearest multiple of divisor (16 for UNet compatibility) 3. Clamp to [min_patch_size, max_patch_size] bounds 4. Validate: error if min_patch_size exceeds smallest volume

Args: dataset: MedDataset instance with analyzed images. target_spacing: Target voxel spacing [x, y, z]. If None, uses dataset.get_suggestion()[‘target_spacing’]. min_patch_size: Minimum per dimension. Default [32, 32, 32]. max_patch_size: Maximum per dimension. Default [256, 256, 256]. divisor: Ensure divisibility (default 16 for UNet compatibility).

Returns: list: [patch_dim_0, patch_dim_1, patch_dim_2]

Example: >>> from fastMONAI.dataset_info import MedDataset >>> dataset = MedDataset(dataframe=df, mask_col=‘mask_path’, dtype=MedMask) >>> >>> # Use recommended spacing >>> patch_size = suggest_patch_size(dataset) >>> >>> # Use custom spacing >>> patch_size = suggest_patch_size(dataset, target_spacing=[1.0, 1.0, 2.0])


source

preprocess_dataset

def preprocess_dataset(
    df, img_col, mask_col:NoneType=None, output_dir:str='preprocessed', target_spacing:NoneType=None,
    apply_reorder:bool=True, transforms:NoneType=None, max_workers:int=4, skip_existing:bool=True,
    dataset_version:NoneType=None
):

Preprocess a dataset into a manifest-validated, fingerprinted disk cache.

Cache identity covers the optional mask-based dataset_version, preprocessing contract, and cheap source signatures (resolved path, size, and nanosecond mtime). A complete cache is published only after every case succeeds and the manifest is written. The DataFrame receives output paths only after publication.

Transform pipeline order: CopyAffine (if masks) -> ToCanonical (if apply_reorder) -> Resample (if target_spacing) -> user transforms

Args: df: DataFrame with file paths. img_col: Column name for image paths. mask_col: Optional column name for mask paths. output_dir: Cache root. A versioned child directory is created below it. target_spacing: Target voxel spacing for resampling. apply_reorder: Whether to reorder to RAS+ canonical orientation. transforms: Additional TorchIO or serializable fastMONAI transforms. max_workers: Number of parallel volume workers. skip_existing: Reuse a valid matching cache. False explicitly rebuilds it. dataset_version: Optional existing dataset/annotation fingerprint to bind to this cache. Raw image contents are not read to compute the cache version.

Returns: PreprocessingResult describing the published or reused cache.


source

PreprocessingResult

def PreprocessingResult(
    cache_version:str, manifest_path:Path, output_dir:Path, processed:int, reused:bool
)->None:

Published preprocessing-cache identity and location.

import tempfile, shutil
from fastcore.test import test_eq, test_fail

_tmp = Path(tempfile.mkdtemp())

# Create synthetic NIfTI files
for i in range(3):
    tio.ScalarImage(tensor=torch.randn(1, 10, 10, 10)).save(_tmp / f'img_{i}.nii.gz')
    tio.LabelMap(tensor=torch.randint(0, 2, (1, 10, 10, 10))).save(_tmp / f'mask_{i}.nii.gz')

# Image-only cache: originals are preserved and outputs live below a version directory.
_df1 = pd.DataFrame({'img': [str(_tmp / f'img_{i}.nii.gz') for i in range(3)]})
_orig_paths1 = _df1['img'].tolist()
_cache_root = _tmp / 'cache'
_result1 = preprocess_dataset(
    _df1, img_col='img', output_dir=_cache_root, apply_reorder=False
)
test_eq(_df1['img'].tolist(), _orig_paths1)
test_eq(_result1.processed, 3)
test_eq(_result1.reused, False)
test_eq(_result1.output_dir.parent, _cache_root)
test_eq(all(Path(p).parent == _result1.output_dir / 'images' for p in _df1.img_preprocessed), True)
test_eq(all(Path(p).is_file() for p in _df1.img_preprocessed), True)

_manifest1 = json.loads(_result1.manifest_path.read_text())
test_eq(_manifest1['manifest_schema'], 1)
test_eq(_manifest1['preprocessing_cache_version'], _result1.cache_version)
test_eq(_manifest1['dataset_version'], None)
test_eq(_manifest1['contract']['apply_reorder'], False)

# Identical source signatures and contract reuse the published cache.
_df2 = pd.DataFrame({'img': _orig_paths1})
_result2 = preprocess_dataset(
    _df2, img_col='img', output_dir=_cache_root, apply_reorder=False
)
test_eq(_result2.cache_version, _result1.cache_version)
test_eq(_result2.processed, 0)
test_eq(_result2.reused, True)

# A preprocessing-contract change gets a different cache directory.
_df_spacing = pd.DataFrame({'img': _orig_paths1})
_result_spacing = preprocess_dataset(
    _df_spacing, img_col='img', output_dir=_cache_root,
    target_spacing=[2, 1, 1], apply_reorder=False,
)
assert _result_spacing.cache_version != _result1.cache_version

# A cheap source signature change also invalidates reuse without reading raw contents.
_source0 = Path(_orig_paths1[0])
_stat0 = _source0.stat()
os.utime(_source0, ns=(_stat0.st_atime_ns, _stat0.st_mtime_ns + 1_000_000))
_df_changed = pd.DataFrame({'img': _orig_paths1})
_result_changed = preprocess_dataset(
    _df_changed, img_col='img', output_dir=_cache_root, apply_reorder=False
)
assert _result_changed.cache_version not in {
    _result1.cache_version, _result_spacing.cache_version
}

# A damaged cache fails closed; an explicit rebuild republishes it.
Path(_df_changed.img_preprocessed.iloc[0]).unlink()
_df_damaged = pd.DataFrame({'img': _orig_paths1})
test_fail(
    lambda: preprocess_dataset(
        _df_damaged, img_col='img', output_dir=_cache_root, apply_reorder=False
    ),
    contains='no valid matching manifest',
)
_result_rebuilt = preprocess_dataset(
    _df_damaged, img_col='img', output_dir=_cache_root,
    apply_reorder=False, skip_existing=False,
)
test_eq(_result_rebuilt.cache_version, _result_changed.cache_version)
test_eq(_result_rebuilt.processed, 3)
test_eq(all(Path(p).is_file() for p in _df_damaged.img_preprocessed), True)

# Masks and the existing annotation fingerprint are bound into the manifest.
_df3 = pd.DataFrame({
    'img': _orig_paths1,
    'mask': [str(_tmp / f'mask_{i}.nii.gz') for i in range(3)],
})
_orig_mask3 = _df3['mask'].tolist()
_result3 = preprocess_dataset(
    _df3, img_col='img', mask_col='mask', output_dir=_tmp / 'mask_cache',
    apply_reorder=False, dataset_version='annotation-v1',
)
test_eq(_df3['img'].tolist(), _orig_paths1)
test_eq(_df3['mask'].tolist(), _orig_mask3)
test_eq(all(Path(p).is_file() for p in _df3.img_preprocessed), True)
test_eq(all(Path(p).is_file() for p in _df3.mask_preprocessed), True)
test_eq(json.loads(_result3.manifest_path.read_text())['dataset_version'], 'annotation-v1')

# A worker failure removes staging and publishes no cache directory.
_bad_source = _tmp / 'not_a_nifti.nii.gz'
_bad_source.write_text('not a medical image')
_failure_root = _tmp / 'failed_cache'
test_fail(
    lambda: preprocess_dataset(
        pd.DataFrame({'img': [str(_bad_source)]}),
        img_col='img', output_dir=_failure_root, apply_reorder=False,
    ),
    contains='no cache was published',
)
test_eq(list(_failure_root.iterdir()), [])

# Input validation
test_fail(lambda: preprocess_dataset(pd.DataFrame(), img_col='img'), contains='empty')
test_fail(lambda: preprocess_dataset(pd.DataFrame({'x': [1]}), img_col='img'), contains='not found')
_df_dup = pd.DataFrame({'img': [str(_tmp / 'img_0.nii.gz')] * 2})
test_fail(lambda: preprocess_dataset(_df_dup, img_col='img'), contains='Duplicate')

# Failed metadata files are surfaced (not silently dropped from statistics).
_files = [str(_tmp / 'img_0.nii.gz'), str(_tmp / 'img_1.nii.gz'), str(_tmp / 'does_not_exist.nii.gz')]
with warnings.catch_warnings(record=True) as _w:
    warnings.simplefilter('always')
    _ds = MedDataset(img_list=_files, apply_reorder=False, use_cache=False)
test_eq(_ds.failed_files, [str(_tmp / 'does_not_exist.nii.gz')])
test_eq(len(_ds.df), 2)
test_eq('error' in _ds.df.columns, False)
test_eq(any('failed to load' in str(_x.message) for _x in _w), True)
test_eq(bool(np.isnan(_ds.get_size_statistics()['median']).any()), False)

# All-files-failed metadata analysis constructs and statistics guard cleanly.
_ds_all = MedDataset(
    img_list=[str(_tmp / 'no1.nii.gz'), str(_tmp / 'no2.nii.gz')],
    apply_reorder=False, use_cache=False,
)
test_eq(len(_ds_all.df), 0)
test_eq(len(_ds_all.failed_files), 2)
test_fail(lambda: _ds_all.summary(), contains='empty')
test_fail(lambda: _ds_all.get_suggestion(), contains='empty')

shutil.rmtree(_tmp)
Preprocessing complete: 3 processed, 0 skipped, 0 failed
Preprocessing complete: 0 processed, 3 skipped, 0 failed
Preprocessing complete: 3 processed, 0 skipped, 0 failed
Preprocessing complete: 3 processed, 0 skipped, 0 failed
Preprocessing complete: 3 processed, 0 skipped, 0 failed
Preprocessing complete: 3 processed, 0 skipped, 0 failed
Warning: Failed to process /tmp/tmp728st0kf/does_not_exist.nii.gz: File not found: "/tmp/tmp728st0kf/does_not_exist.nii.gz"
Warning: Failed to process /tmp/tmp728st0kf/no1.nii.gz: File not found: "/tmp/tmp728st0kf/no1.nii.gz"
Warning: Failed to process /tmp/tmp728st0kf/no2.nii.gz: File not found: "/tmp/tmp728st0kf/no2.nii.gz"

Preprocessing:   0%|          | 0/3 [00:00<?, ?it/s]
Preprocessing: 100%|##########| 3/3 [00:00<00:00, 598.02it/s]

Preprocessing:   0%|          | 0/3 [00:00<?, ?it/s]
Preprocessing: 100%|##########| 3/3 [00:00<00:00, 215.84it/s]

Preprocessing:   0%|          | 0/3 [00:00<?, ?it/s]
Preprocessing: 100%|##########| 3/3 [00:00<00:00, 2813.71it/s]

Preprocessing:   0%|          | 0/3 [00:00<?, ?it/s]
Preprocessing: 100%|##########| 3/3 [00:00<00:00, 275.45it/s]

Preprocessing:   0%|          | 0/3 [00:00<?, ?it/s]
Preprocessing: 100%|##########| 3/3 [00:00<00:00, 141.00it/s]

Preprocessing:   0%|          | 0/1 [00:00<?, ?it/s]Error loading image with SimpleITK:
Exception thrown in SimpleITK ImageFileReader_Execute: /work/src/Code/IO/src/sitkImageReaderBase.cxx:99:
sitk::ERROR: Unable to determine ImageIO reader for "/tmp/tmp728st0kf/not_a_nifti.nii.gz"

Trying NiBabel...

Preprocessing: 100%|##########| 1/1 [00:00<00:00, 409.36it/s]
2/2 file(s) failed to load and were excluded from dataset statistics. See self.failed_files.

source

get_class_weights

def get_class_weights(
    labels:(<built-in function array>, <class 'list'>), class_weight:str='balanced'
)->Tensor:

Calculates and returns the class weights.

Args: labels: An array or list of class labels for each instance in the dataset. class_weight: Defaults to ‘balanced’.

Returns: A tensor of class weights.