CodeCamp #140 [New] [Feature] Add synapse dataset and data augmentation in dev-1.x. (#2432)

## Motivation

Add Synapse dataset in MMSegmentation.
Old PR: https://github.com/open-mmlab/mmsegmentation/pull/2372.
This commit is contained in:
王永韬 2023-01-06 16:14:54 +08:00 committed by GitHub
parent bd29c20778
commit 2d67e51db3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 486 additions and 12 deletions

View File

@ -0,0 +1,41 @@
dataset_type = 'SynapseDataset'
data_root = 'data/synapse/'
img_scale = (224, 224)
train_pipeline = [
dict(type='LoadImageFromFile'),
dict(type='LoadAnnotations'),
dict(type='Resize', scale=img_scale, keep_ratio=True),
dict(type='RandomRotFlip', rotate_prob=0.5, flip_prob=0.5, degree=20),
dict(type='PackSegInputs')
]
test_pipeline = [
dict(type='LoadImageFromFile'),
dict(type='Resize', scale=img_scale, keep_ratio=True),
dict(type='LoadAnnotations'),
dict(type='PackSegInputs')
]
train_dataloader = dict(
batch_size=6,
num_workers=2,
persistent_workers=True,
sampler=dict(type='InfiniteSampler', shuffle=True),
dataset=dict(
type=dataset_type,
data_root=data_root,
data_prefix=dict(
img_path='img_dir/train', seg_map_path='ann_dir/train'),
pipeline=train_pipeline))
val_dataloader = dict(
batch_size=1,
num_workers=4,
persistent_workers=True,
sampler=dict(type='DefaultSampler', shuffle=False),
dataset=dict(
type=dataset_type,
data_root=data_root,
data_prefix=dict(img_path='img_dir/val', seg_map_path='ann_dir/val'),
pipeline=test_pipeline))
test_dataloader = val_dataloader
val_evaluator = dict(type='IoUMetric', iou_metrics=['mDice'])
test_evaluator = val_evaluator

View File

@ -414,3 +414,84 @@ The contents of LIP datasets include:
│   │ │ ├── 100034_483681.png
│   │ │ ├── ...
```
## Synapse dataset
This dataset could be download from [this page](https://www.synapse.org/#!Synapse:syn3193805/wiki/)
To follow the data preparation setting of [TransUNet](https://arxiv.org/abs/2102.04306), which splits original training set (30 scans)
into new training (18 scans) and validation set (12 scans). Please run the following command to prepare the dataset.
```shell
unzip RawData.zip
cd ./RawData/Training
```
Then create `train.txt` and `val.txt` to split dataset.
According to TransUnet, the following is the data set division.
train.txt
```none
img0005.nii.gz
img0006.nii.gz
img0007.nii.gz
img0009.nii.gz
img0010.nii.gz
img0021.nii.gz
img0023.nii.gz
img0024.nii.gz
img0026.nii.gz
img0027.nii.gz
img0028.nii.gz
img0030.nii.gz
img0031.nii.gz
img0033.nii.gz
img0034.nii.gz
img0037.nii.gz
img0039.nii.gz
img0040.nii.gz
```
val.txt
```none
img0008.nii.gz
img0022.nii.gz
img0038.nii.gz
img0036.nii.gz
img0032.nii.gz
img0002.nii.gz
img0029.nii.gz
img0003.nii.gz
img0001.nii.gz
img0004.nii.gz
img0025.nii.gz
img0035.nii.gz
```
The contents of synapse datasets include:
```none
├── Training
│ ├── img
│ │ ├── img0001.nii.gz
│ │ ├── img0002.nii.gz
│ │ ├── ...
│ ├── label
│ │ ├── label0001.nii.gz
│ │ ├── label0002.nii.gz
│ │ ├── ...
│ ├── train.txt
│ ├── val.txt
```
Then, use this command to convert synapse dataset.
```shell
python tools/dataset_converters/synapse.py --dataset-path /path/to/synapse
```
Noted that MMSegmentation default evaluation metric (such as mean dice value) is calculated on 2D slice image,
which is not comparable to results of 3D scan in some paper such as [TransUNet](https://arxiv.org/abs/2102.04306).

View File

@ -18,6 +18,7 @@ from .night_driving import NightDrivingDataset
from .pascal_context import PascalContextDataset, PascalContextDataset59
from .potsdam import PotsdamDataset
from .stare import STAREDataset
from .synapse import SynapseDataset
# yapf: disable
from .transforms import (CLAHE, AdjustGamma, BioMedical3DPad,
BioMedical3DRandomCrop, BioMedicalGaussianBlur,
@ -26,9 +27,9 @@ from .transforms import (CLAHE, AdjustGamma, BioMedical3DPad,
LoadBiomedicalAnnotation, LoadBiomedicalData,
LoadBiomedicalImageFromFile, LoadImageFromNDArray,
PackSegInputs, PhotoMetricDistortion, RandomCrop,
RandomCutOut, RandomMosaic, RandomRotate, Rerange,
ResizeShortestEdge, ResizeToMultiple, RGB2Gray,
SegRescale)
RandomCutOut, RandomMosaic, RandomRotate,
RandomRotFlip, Rerange, ResizeShortestEdge,
ResizeToMultiple, RGB2Gray, SegRescale)
from .voc import PascalVOCDataset
# yapf: enable
@ -46,5 +47,6 @@ __all__ = [
'LoadBiomedicalAnnotation', 'LoadBiomedicalData', 'GenerateEdge',
'DecathlonDataset', 'LIPDataset', 'ResizeShortestEdge',
'BioMedicalGaussianNoise', 'BioMedicalGaussianBlur',
'BioMedicalRandomGamma', 'BioMedical3DPad'
'BioMedicalRandomGamma', 'BioMedical3DPad', 'RandomRotFlip',
'SynapseDataset'
]

28
mmseg/datasets/synapse.py Normal file
View File

@ -0,0 +1,28 @@
# Copyright (c) OpenMMLab. All rights reserved.
from mmseg.registry import DATASETS
from .basesegdataset import BaseSegDataset
@DATASETS.register_module()
class SynapseDataset(BaseSegDataset):
"""Synapse dataset.
Before dataset preprocess of Synapse, there are total 13 categories of
foreground which does not include background. After preprocessing, 8
foreground categories are kept while the other 5 foreground categories are
handled as background. The ``img_suffix`` is fixed to '.jpg' and
``seg_map_suffix`` is fixed to '.png'.
"""
METAINFO = dict(
classes=('background', 'aorta', 'gallbladder', 'left_kidney',
'right_kidney', 'liver', 'pancreas', 'spleen', 'stomach'),
palette=[[0, 0, 0], [0, 0, 255], [0, 255, 0], [255, 0, 0],
[0, 255, 255], [255, 0, 255], [255, 255, 0], [60, 255, 255],
[240, 240, 240]])
def __init__(self,
img_suffix='.jpg',
seg_map_suffix='.png',
**kwargs) -> None:
super().__init__(
img_suffix=img_suffix, seg_map_suffix=seg_map_suffix, **kwargs)

View File

@ -8,9 +8,9 @@ from .transforms import (CLAHE, AdjustGamma, BioMedical3DPad,
BioMedical3DRandomCrop, BioMedicalGaussianBlur,
BioMedicalGaussianNoise, BioMedicalRandomGamma,
GenerateEdge, PhotoMetricDistortion, RandomCrop,
RandomCutOut, RandomMosaic, RandomRotate, Rerange,
ResizeShortestEdge, ResizeToMultiple, RGB2Gray,
SegRescale)
RandomCutOut, RandomMosaic, RandomRotate,
RandomRotFlip, Rerange, ResizeShortestEdge,
ResizeToMultiple, RGB2Gray, SegRescale)
# yapf: enable
__all__ = [
@ -20,5 +20,5 @@ __all__ = [
'ResizeToMultiple', 'LoadImageFromNDArray', 'LoadBiomedicalImageFromFile',
'LoadBiomedicalAnnotation', 'LoadBiomedicalData', 'GenerateEdge',
'ResizeShortestEdge', 'BioMedicalGaussianNoise', 'BioMedicalGaussianBlur',
'BioMedicalRandomGamma', 'BioMedical3DPad'
'BioMedicalRandomGamma', 'BioMedical3DPad', 'RandomRotFlip'
]

View File

@ -861,6 +861,84 @@ class RandomCutOut(BaseTransform):
return repr_str
@TRANSFORMS.register_module()
class RandomRotFlip(BaseTransform):
"""Rotate and flip the image & seg or just rotate the image & seg.
Required Keys:
- img
- gt_seg_map
Modified Keys:
- img
- gt_seg_map
Args:
rotate_prob (float): The probability of rotate image.
flip_prob (float): The probability of rotate&flip image.
degree (float, tuple[float]): Range of degrees to select from. If
degree is a number instead of tuple like (min, max),
the range of degree will be (``-degree``, ``+degree``)
"""
def __init__(self, rotate_prob=0.5, flip_prob=0.5, degree=(-20, 20)):
self.rotate_prob = rotate_prob
self.flip_prob = flip_prob
assert 0 <= rotate_prob <= 1 and 0 <= flip_prob <= 1
if isinstance(degree, (float, int)):
assert degree > 0, f'degree {degree} should be positive'
self.degree = (-degree, degree)
else:
self.degree = degree
assert len(self.degree) == 2, f'degree {self.degree} should be a ' \
f'tuple of (min, max)'
def random_rot_flip(self, results: dict) -> dict:
k = np.random.randint(0, 4)
results['img'] = np.rot90(results['img'], k)
for key in results.get('seg_fields', []):
results[key] = np.rot90(results[key], k)
axis = np.random.randint(0, 2)
results['img'] = np.flip(results['img'], axis=axis).copy()
for key in results.get('seg_fields', []):
results[key] = np.flip(results[key], axis=axis).copy()
return results
def random_rotate(self, results: dict) -> dict:
angle = np.random.uniform(min(*self.degree), max(*self.degree))
results['img'] = mmcv.imrotate(results['img'], angle=angle)
for key in results.get('seg_fields', []):
results[key] = mmcv.imrotate(results[key], angle=angle)
return results
def transform(self, results: dict) -> dict:
"""Call function to rotate or rotate & flip image, semantic
segmentation maps.
Args:
results (dict): Result dict from loading pipeline.
Returns:
dict: Rotated or rotated & flipped results.
"""
rotate_flag = 0
if random.random() < self.rotate_prob:
results = self.random_rotate(results)
rotate_flag = 1
if random.random() < self.flip_prob and rotate_flag == 0:
results = self.random_rot_flip(results)
return results
def __repr__(self):
repr_str = self.__class__.__name__
repr_str += f'(rotate_prob={self.rotate_prob}, ' \
f'flip_prob={self.flip_prob}, ' \
f'degree={self.degree})'
return repr_str
@TRANSFORMS.register_module()
class RandomMosaic(BaseTransform):
"""Mosaic augmentation. Given 4 images, mosaic transform combines them into

View File

@ -6,8 +6,8 @@ from .class_names import (ade_classes, ade_palette, cityscapes_classes,
get_palette, isaid_classes, isaid_palette,
loveda_classes, loveda_palette, potsdam_classes,
potsdam_palette, stare_classes, stare_palette,
vaihingen_classes, vaihingen_palette, voc_classes,
voc_palette)
synapse_classes, synapse_palette, vaihingen_classes,
vaihingen_palette, voc_classes, voc_palette)
# yapf: enable
from .collect_env import collect_env
from .io import datafrombytes
@ -27,5 +27,5 @@ __all__ = [
'cityscapes_palette', 'ade_palette', 'voc_palette', 'cocostuff_palette',
'loveda_palette', 'potsdam_palette', 'vaihingen_palette', 'isaid_palette',
'stare_palette', 'dataset_aliases', 'get_classes', 'get_palette',
'datafrombytes'
'datafrombytes', 'synapse_palette', 'synapse_classes'
]

View File

@ -265,6 +265,20 @@ def stare_palette():
return [[120, 120, 120], [6, 230, 230]]
def synapse_palette():
"""Synapse palette for external use."""
return [[0, 0, 0], [0, 0, 255], [0, 255, 0], [255, 0, 0], [0, 255, 255],
[255, 0, 255], [255, 255, 0], [60, 255, 255], [240, 240, 240]]
def synapse_classes():
"""Synapse class names for external use."""
return [
'background', 'aorta', 'gallbladder', 'left_kidney', 'right_kidney',
'liver', 'pancreas', 'spleen', 'stomach'
]
def lip_classes():
"""LIP class names for external use."""
return [

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -9,7 +9,7 @@ import pytest
from mmseg.datasets import (ADE20KDataset, BaseSegDataset, CityscapesDataset,
COCOStuffDataset, DecathlonDataset, ISPRSDataset,
LIPDataset, LoveDADataset, PascalVOCDataset,
PotsdamDataset, iSAIDDataset)
PotsdamDataset, SynapseDataset, iSAIDDataset)
from mmseg.registry import DATASETS
from mmseg.utils import get_classes, get_palette
@ -220,6 +220,19 @@ def test_vaihingen():
assert len(test_dataset) == 1
def test_synapse():
test_dataset = SynapseDataset(
pipeline=[],
data_prefix=dict(
img_path=osp.join(
osp.dirname(__file__),
'../data/pseudo_synapse_dataset/img_dir'),
seg_map_path=osp.join(
osp.dirname(__file__),
'../data/pseudo_synapse_dataset/ann_dir')))
assert len(test_dataset) == 2
def test_isaid():
test_dataset = iSAIDDataset(
pipeline=[],

View File

@ -184,6 +184,68 @@ def test_flip():
assert np.equal(original_seg, results['gt_semantic_seg']).all()
def test_random_rotate_flip():
with pytest.raises(AssertionError):
transform = dict(type='RandomRotFlip', flip_prob=1.5)
TRANSFORMS.build(transform)
with pytest.raises(AssertionError):
transform = dict(type='RandomRotFlip', rotate_prob=1.5)
TRANSFORMS.build(transform)
with pytest.raises(AssertionError):
transform = dict(type='RandomRotFlip', degree=[20, 20, 20])
TRANSFORMS.build(transform)
with pytest.raises(AssertionError):
transform = dict(type='RandomRotFlip', degree=-20)
TRANSFORMS.build(transform)
transform = dict(
type='RandomRotFlip', flip_prob=1.0, rotate_prob=0, degree=20)
rot_flip_module = TRANSFORMS.build(transform)
results = dict()
img = mmcv.imread(
osp.join(
osp.dirname(__file__),
'../data/pseudo_synapse_dataset/img_dir/case0005_slice000.jpg'),
'color')
original_img = copy.deepcopy(img)
seg = np.array(
Image.open(
osp.join(
osp.dirname(__file__),
'../data/pseudo_synapse_dataset/ann_dir/case0005_slice000.png')
))
original_seg = copy.deepcopy(seg)
results['img'] = img
results['gt_semantic_seg'] = seg
results['seg_fields'] = ['gt_semantic_seg']
results['img_shape'] = img.shape
results['ori_shape'] = img.shape
# Set initial values for default meta_keys
results['pad_shape'] = img.shape
results['scale_factor'] = 1.0
result_flip = rot_flip_module(results)
assert original_img.shape == result_flip['img'].shape
assert original_seg.shape == result_flip['gt_semantic_seg'].shape
transform = dict(
type='RandomRotFlip', flip_prob=0, rotate_prob=1.0, degree=20)
rot_flip_module = TRANSFORMS.build(transform)
result_rotate = rot_flip_module(results)
assert original_img.shape == result_rotate['img'].shape
assert original_seg.shape == result_rotate['gt_semantic_seg'].shape
assert str(transform) == "{'type': 'RandomRotFlip'," \
" 'flip_prob': 0," \
" 'rotate_prob': 1.0," \
" 'degree': 20}"
def test_pad():
# test assertion if both size_divisor and size is None
with pytest.raises(AssertionError):

View File

@ -0,0 +1,155 @@
# Copyright (c) OpenMMLab. All rights reserved.
import argparse
import os.path as osp
import nibabel as nib
import numpy as np
from mmengine.utils import mkdir_or_exist
from PIL import Image
def read_files_from_txt(txt_path):
with open(txt_path) as f:
files = f.readlines()
files = [file.strip() for file in files]
return files
def read_nii_file(nii_path):
img = nib.load(nii_path).get_fdata()
return img
def split_3d_image(img):
c, _, _ = img.shape
res = []
for i in range(c):
res.append(img[i, :, :])
return res
def label_mapping(label):
"""Label mapping from TransUNet paper setting. It only has 9 classes, which
are 'background', 'aorta', 'gallbladder', 'left_kidney', 'right_kidney',
'liver', 'pancreas', 'spleen', 'stomach', respectively. Other foreground
classes in original dataset are all set to background.
More details could be found here: https://arxiv.org/abs/2102.04306
"""
maped_label = np.zeros_like(label)
maped_label[label == 8] = 1
maped_label[label == 4] = 2
maped_label[label == 3] = 3
maped_label[label == 2] = 4
maped_label[label == 6] = 5
maped_label[label == 11] = 6
maped_label[label == 1] = 7
maped_label[label == 7] = 8
return maped_label
def pares_args():
parser = argparse.ArgumentParser(
description='Convert synapse dataset to mmsegmentation format')
parser.add_argument(
'--dataset-path', type=str, help='synapse dataset path.')
parser.add_argument(
'--save-path',
default='data/synapse',
type=str,
help='save path of the dataset.')
args = parser.parse_args()
return args
def main():
args = pares_args()
dataset_path = args.dataset_path
save_path = args.save_path
if not osp.exists(dataset_path):
raise ValueError('The dataset path does not exist. '
'Please enter a correct dataset path.')
if not osp.exists(osp.join(dataset_path, 'img')) \
or not osp.exists(osp.join(dataset_path, 'label')):
raise FileNotFoundError('The dataset structure is incorrect. '
'Please check your dataset.')
train_id = read_files_from_txt(osp.join(dataset_path, 'train.txt'))
train_id = [idx[3:7] for idx in train_id]
test_id = read_files_from_txt(osp.join(dataset_path, 'val.txt'))
test_id = [idx[3:7] for idx in test_id]
mkdir_or_exist(osp.join(save_path, 'img_dir/train'))
mkdir_or_exist(osp.join(save_path, 'img_dir/val'))
mkdir_or_exist(osp.join(save_path, 'ann_dir/train'))
mkdir_or_exist(osp.join(save_path, 'ann_dir/val'))
# It follows data preparation pipeline from here:
# https://github.com/Beckschen/TransUNet/tree/main/datasets
for i, idx in enumerate(train_id):
img_3d = read_nii_file(
osp.join(dataset_path, 'img', 'img' + idx + '.nii.gz'))
label_3d = read_nii_file(
osp.join(dataset_path, 'label', 'label' + idx + '.nii.gz'))
img_3d = np.clip(img_3d, -125, 275)
img_3d = (img_3d + 125) / 400
img_3d *= 255
img_3d = np.transpose(img_3d, [2, 0, 1])
img_3d = np.flip(img_3d, 2)
label_3d = np.transpose(label_3d, [2, 0, 1])
label_3d = np.flip(label_3d, 2)
label_3d = label_mapping(label_3d)
for c in range(img_3d.shape[0]):
img = img_3d[c]
label = label_3d[c]
img = Image.fromarray(img).convert('RGB')
label = Image.fromarray(label).convert('L')
img.save(
osp.join(
save_path, 'img_dir/train', 'case' + idx.zfill(4) +
'_slice' + str(c).zfill(3) + '.jpg'))
label.save(
osp.join(
save_path, 'ann_dir/train', 'case' + idx.zfill(4) +
'_slice' + str(c).zfill(3) + '.png'))
for i, idx in enumerate(test_id):
img_3d = read_nii_file(
osp.join(dataset_path, 'img', 'img' + idx + '.nii.gz'))
label_3d = read_nii_file(
osp.join(dataset_path, 'label', 'label' + idx + '.nii.gz'))
img_3d = np.clip(img_3d, -125, 275)
img_3d = (img_3d + 125) / 400
img_3d *= 255
img_3d = np.transpose(img_3d, [2, 0, 1])
img_3d = np.flip(img_3d, 2)
label_3d = np.transpose(label_3d, [2, 0, 1])
label_3d = np.flip(label_3d, 2)
label_3d = label_mapping(label_3d)
for c in range(img_3d.shape[0]):
img = img_3d[c]
label = label_3d[c]
img = Image.fromarray(img).convert('RGB')
label = Image.fromarray(label).convert('L')
img.save(
osp.join(
save_path, 'img_dir/val', 'case' + idx.zfill(4) +
'_slice' + str(c).zfill(3) + '.jpg'))
label.save(
osp.join(
save_path, 'ann_dir/val', 'case' + idx.zfill(4) +
'_slice' + str(c).zfill(3) + '.png'))
if __name__ == '__main__':
main()