Skip to content

Commit

Permalink
Support resolution and spacing metadata (#349)
Browse files Browse the repository at this point in the history
Add 'tiff.resolution_unit' and 'tiff.x_resolution', 'tiff.y_resolution' metadata to the CuImage object.
(See https://www.awaresystems.be/imaging/tiff/tifftags/resolutionunit.html)

If `resolution_unit` is not 1 (1 = No absolute unit of measurement), `spacing_units` is `['micrometer', 'micrometer', 'color']` and spacing unit is calculated based on `micrometer`.

**spacing_test.py**
```python
from pprint import pprint
from cucim import CuImage
img = CuImage("TCGA-18-3406-01Z-00-DX1_tissue.tif")
pprint(img.metadata, indent=2, compact=True)

```

```bash
❯ python spacing_test.py
{ 'cucim': { 'associated_images': [],
             'channel_names': ['R', 'G', 'B', 'A'],
             'coord_sys': 'LPS',
             'dims': 'YXC',
             'direction': [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
             'dtype': {'bits': 8, 'code': 1, 'lanes': 1},
             'ndim': 3,
             'origin': [0.0, 0.0, 0.0],
             'path': '/home/gbae/Downloads/TCGA-18-3406-01Z-00-DX1_tissue.tif',
             'resolutions': { 'level_count': 6,
                              'level_dimensions': [ [16034, 10970],
                                                    [8017, 5485], [4008, 2742],
                                                    [2004, 1371], [1002, 685],
                                                    [501, 342]],
                              'level_downsamples': [ 1.0, 2.0,
                                                     4.000614166259766,
                                                     8.001228332519531,
                                                     16.008296966552734,
                                                     32.040008544921875],
                              'level_tile_sizes': [ [512, 512], [512, 512],
                                                    [512, 512], [512, 512],
                                                    [512, 512], [512, 512]]},
             'shape': [10970, 16034, 4],
             'spacing': [2.0159945487976074, 2.0159945487976074, 1.0],
             'spacing_units': ['micrometer', 'micrometer', 'color'],
             'typestr': '|u1'},
  'tiff': { 'model': '',
            'resolution_unit': 'centimeter',
            'software': '',
            'x_resolution': 4960.3310546875,
            'y_resolution': 4960.3310546875}}
```

**Test install**
```bash
python -m pip install --force-reinstall --extra-index-url https://test.pypi.org/simple/ cucim==0.0.333
```

Address #333 

Signed-off-by: Gigon Bae <[email protected]>

Authors:
  - Gigon Bae (https://github.com/gigony)
  - Gregory Lee (https://github.com/grlee77)

Approvers:
  - Gregory Lee (https://github.com/grlee77)

URL: #349
  • Loading branch information
gigony authored Aug 3, 2022
1 parent c834039 commit 39f7c01
Show file tree
Hide file tree
Showing 11 changed files with 235 additions and 32 deletions.
44 changes: 37 additions & 7 deletions cpp/plugins/cucim.kit.cuslide/src/cuslide/cuslide.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -171,16 +171,46 @@ static bool CUCIM_ABI parser_parse(CuCIMFileHandle_ptr handle_ptr, cucim::io::fo
channel_names.emplace_back(std::string_view{ "A" });
}

// TODO: Set correct spacing value
// Spacing units
std::pmr::vector<std::string_view> spacing_units(&resource);
spacing_units.reserve(ndim);

std::pmr::vector<float> spacing(&resource);
spacing.reserve(ndim);
spacing.insert(spacing.end(), ndim, 1.0);
const auto resolution_unit = level0_ifd->resolution_unit();
const auto x_resolution = level0_ifd->x_resolution();
const auto y_resolution = level0_ifd->y_resolution();

switch (resolution_unit)
{
case 1: // no absolute unit of measurement
spacing.emplace_back(y_resolution);
spacing.emplace_back(x_resolution);
spacing.emplace_back(1.0f);

spacing_units.emplace_back(std::string_view{ "" });
spacing_units.emplace_back(std::string_view{ "" });
break;
case 2: // inch
spacing.emplace_back(y_resolution != 0 ? 25400 / y_resolution : 1.0f);
spacing.emplace_back(x_resolution != 0 ? 25400 / x_resolution : 1.0f);
spacing.emplace_back(1.0f);

spacing_units.emplace_back(std::string_view{ "micrometer" });
spacing_units.emplace_back(std::string_view{ "micrometer" });
break;
case 3: // centimeter
spacing.emplace_back(y_resolution != 0 ? 10000 / y_resolution : 1.0f);
spacing.emplace_back(x_resolution != 0 ? 10000 / x_resolution : 1.0f);
spacing.emplace_back(1.0f);

spacing_units.emplace_back(std::string_view{ "micrometer" });
spacing_units.emplace_back(std::string_view{ "micrometer" });
break;
default:
spacing.insert(spacing.end(), ndim, 1.0f);
}

// TODO: Set correct spacing units
std::pmr::vector<std::string_view> spacing_units(&resource);
spacing_units.reserve(ndim);
spacing_units.emplace_back(std::string_view{ "micrometer" });
spacing_units.emplace_back(std::string_view{ "micrometer" });
spacing_units.emplace_back(std::string_view{ "color" });

std::pmr::vector<float> origin({ 0.0, 0.0, 0.0 }, &resource);
Expand Down
15 changes: 15 additions & 0 deletions cpp/plugins/cucim.kit.cuslide/src/cuslide/tiff/ifd.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ IFD::IFD(TIFF* tiff, uint16_t index, ifd_offset_t offset) : tiff_(tiff), ifd_ind
model_ = std::string(model_char_ptr ? model_char_ptr : "");
TIFFGetField(tif, TIFFTAG_IMAGEDESCRIPTION, &model_char_ptr);
image_description_ = std::string(model_char_ptr ? model_char_ptr : "");
TIFFGetField(tif, TIFFTAG_RESOLUTIONUNIT, &resolution_unit_);
TIFFGetField(tif, TIFFTAG_XRESOLUTION, &x_resolution_);
TIFFGetField(tif, TIFFTAG_YRESOLUTION, &y_resolution_);

TIFFDirectory& tif_dir = tif->tif_dir;
flags_ = tif->tif_flags;
Expand Down Expand Up @@ -451,6 +454,18 @@ std::string& IFD::image_description()
{
return image_description_;
}
uint16_t IFD::resolution_unit() const
{
return resolution_unit_;
}
float IFD::x_resolution() const
{
return x_resolution_;
}
float IFD::y_resolution() const
{
return y_resolution_;
}
uint32_t IFD::width() const
{
return width_;
Expand Down
7 changes: 7 additions & 0 deletions cpp/plugins/cucim.kit.cuslide/src/cuslide/tiff/ifd.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ class EXPORT_VISIBLE IFD : public std::enable_shared_from_this<IFD>
std::string& software();
std::string& model();
std::string& image_description();
uint16_t resolution_unit() const;
float x_resolution() const;
float y_resolution() const;
uint32_t width() const;
uint32_t height() const;
uint32_t tile_width() const;
Expand Down Expand Up @@ -109,6 +112,10 @@ class EXPORT_VISIBLE IFD : public std::enable_shared_from_this<IFD>
std::string software_;
std::string model_;
std::string image_description_;
uint16_t resolution_unit_ = 1; // 1 = No absolute unit of measurement, 2 = Inch, 3 = Centimeter
float x_resolution_ = 1.0f;
float y_resolution_ = 1.0f;

uint32_t flags_ = 0;
uint32_t width_ = 0;
uint32_t height_ = 0;
Expand Down
17 changes: 17 additions & 0 deletions cpp/plugins/cucim.kit.cuslide/src/cuslide/tiff/tiff.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,9 @@ void TIFF::resolve_vendor_format()
auto& first_ifd = ifds_[0];
std::string& model = first_ifd->model();
std::string& software = first_ifd->software();
const uint16_t resolution_unit = first_ifd->resolution_unit();
const float x_resolution = first_ifd->x_resolution();
const float y_resolution = first_ifd->y_resolution();

// Detect Aperio SVS format
{
Expand Down Expand Up @@ -416,6 +419,20 @@ void TIFF::resolve_vendor_format()

tiff_metadata.emplace("model", model);
tiff_metadata.emplace("software", software);
switch (resolution_unit)
{
case 2:
tiff_metadata.emplace("resolution_unit", "inch");
break;
case 3:
tiff_metadata.emplace("resolution_unit", "centimeter");
break;
default:
tiff_metadata.emplace("resolution_unit", "");
break;
}
tiff_metadata.emplace("x_resolution", x_resolution);
tiff_metadata.emplace("y_resolution", y_resolution);

(*json_metadata).emplace("tiff", std::move(tiff_metadata));
}
Expand Down
28 changes: 23 additions & 5 deletions cpp/src/cuimage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -680,10 +680,11 @@ CuImage CuImage::read_region(std::vector<int64_t>&& location,
location.emplace_back(0);
location.emplace_back(0);
}

const ResolutionInfo& res_info = resolutions();
// If `size` is not specified, size would be (width, height) of the image at the specified `level`.
if (size.empty())
{
const ResolutionInfo& res_info = resolutions();
const auto level_count = res_info.level_count();
if (level_count == 0)
{
Expand Down Expand Up @@ -853,19 +854,36 @@ CuImage CuImage::read_region(std::vector<int64_t>&& location,
// The first dimension is for 'batch' ('N')
spacing_units.emplace_back(std::string_view{ "batch" });
}
const auto& level_downsample = res_info.level_downsample(level);
for (; index < ndim; ++index)
{
int64_t dim_char = dim_indices_.index(dims[index]);
int64_t dim_index = dim_indices_.index(dims[index]);
if (dim_index < 0)
{
throw std::runtime_error(fmt::format("[Error] Invalid dimension name: {}", dims[index]));
}

const char* str_ptr = image_metadata_->spacing_units[dim_char];
size_t str_len = strlen(image_metadata_->spacing_units[dim_char]);
const char* str_ptr = image_metadata_->spacing_units[dim_index];
size_t str_len = strlen(image_metadata_->spacing_units[dim_index]);

char* spacing_unit = static_cast<char*>(resource.allocate(str_len + 1));
memcpy(spacing_unit, str_ptr, str_len);
spacing_unit[str_len] = '\0';
// std::pmr::string spacing_unit{ image_metadata_->spacing_units[dim_char], &resource };
// std::pmr::string spacing_unit{ image_metadata_->spacing_units[dim_index], &resource };

spacing_units.emplace_back(std::string_view{ spacing_unit });

// Update spacing based on level_downsample
char dim_char = image_metadata_->dims[dim_index];
switch (dim_char)
{
case 'X':
case 'Y':
spacing[index] /= level_downsample;
break;
default:
break;
}
}

std::pmr::vector<float> origin(&resource);
Expand Down
16 changes: 8 additions & 8 deletions python/cucim/requirements-test.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
GPUtil==1.4.0
imagecodecs==2021.6.8
openslide-python==1.1.2
psutil==5.8.0
pytest==6.2.4
pytest-cov==2.12.1
pytest-lazy-fixture==0.6.3
tifffile==2021.7.2
GPUtil>=1.4.0
imagecodecs>=2021.6.8
openslide-python>=1.1.2
psutil>=5.8.0
pytest>=6.2.4
pytest-cov>=2.12.1
pytest-lazy-fixture>=0.6.3
tifffile>=2022.7.28
49 changes: 45 additions & 4 deletions python/cucim/tests/fixtures/testimage.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@
from ..util.gen_image import ImageGenerator


def gen_image(tmpdir_factory, recipe):
def gen_image(tmpdir_factory, recipe, resolution=None):
dataset_path = tmpdir_factory.mktemp('datasets').strpath
dataset_gen = ImageGenerator(dataset_path, [recipe])
dataset_gen = ImageGenerator(dataset_path, [recipe], [resolution])
image_path = dataset_gen.gen()
return (dataset_path, image_path[0])

Expand Down Expand Up @@ -63,9 +63,8 @@ def testimg_tiff_stripe_32x24_16_raw(tmpdir_factory):
def testimg_tiff_stripe_32x24_16(request):
return request.param

# tiff_stripe_4096x4096_256


# tiff_stripe_4096x4096_256
@pytest.fixture(scope='session')
def testimg_tiff_stripe_4096x4096_256_jpeg(tmpdir_factory):
dataset_path, image_path = gen_image(
Expand Down Expand Up @@ -137,3 +136,45 @@ def testimg_tiff_stripe_100000x100000_256_raw(tmpdir_factory):
])
def testimg_tiff_stripe_100000x100000_256(request):
return request.param


# testimg_tiff_stripe_4096_4096_256_jpeg_resolution
@pytest.fixture(scope='session')
def testimg_tiff_stripe_4096_4096_256_jpeg_resolution_3_5_centimeter(
tmpdir_factory):
resolution = (0.3, 0.5, "CENTIMETER")
dataset_path, image_path = gen_image(
tmpdir_factory, 'tiff::stripe:4096x4096:256:jpeg', resolution)
yield image_path, resolution
# Clean up fake dataset folder
shutil.rmtree(dataset_path)


@pytest.fixture(scope='session')
def testimg_tiff_stripe_4096_4096_256_jpeg_resolution_4_7_inch(tmpdir_factory):
resolution = (0.4, 0.7, "INCH")
dataset_path, image_path = gen_image(
tmpdir_factory, 'tiff::stripe:4096x4096:256:jpeg', resolution)
yield image_path, resolution
# Clean up fake dataset folder
shutil.rmtree(dataset_path)


@pytest.fixture(scope='session')
def testimg_tiff_stripe_4096_4096_256_jpeg_resolution_9_1_none(tmpdir_factory):
resolution = (9, 1, "NONE")
dataset_path, image_path = gen_image(
tmpdir_factory, 'tiff::stripe:4096x4096:256:jpeg', resolution)
yield image_path, resolution
# Clean up fake dataset folder
shutil.rmtree(dataset_path)


@pytest.fixture(scope='session', params=[
lazy_fixture(
'testimg_tiff_stripe_4096_4096_256_jpeg_resolution_3_5_centimeter'),
lazy_fixture('testimg_tiff_stripe_4096_4096_256_jpeg_resolution_4_7_inch'),
lazy_fixture('testimg_tiff_stripe_4096_4096_256_jpeg_resolution_9_1_none'),
])
def testimg_tiff_stripe_4096_4096_256_jpeg_resolution(request):
return request.param
51 changes: 50 additions & 1 deletion python/cucim/tests/unit/clara/test_load_image_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#

from ...util.io import open_image_cucim
import math


def test_load_image_metadata(testimg_tiff_stripe_32x24_16):
Expand Down Expand Up @@ -45,7 +46,7 @@ def test_load_image_metadata(testimg_tiff_stripe_32x24_16):
# Returns physical size in tuple.
assert img.spacing() == [1.0, 1.0, 1.0]
# Units for each spacing element (size is same with `ndim`).
assert img.spacing_units() == ['micrometer', 'micrometer', 'color']
assert img.spacing_units() == ['', '', 'color']
# Physical location of (0, 0, 0) (size is always 3).
assert img.origin == [0.0, 0.0, 0.0]
# Direction cosines (size is always 3x3).
Expand All @@ -71,6 +72,54 @@ def test_load_image_metadata(testimg_tiff_stripe_32x24_16):
assert img.raw_metadata == '{"axes": "YXC", "shape": [24, 32, 3]}'


def test_load_image_resolution_metadata(testimg_tiff_stripe_4096_4096_256_jpeg_resolution): # noqa: E501
image, resolution = testimg_tiff_stripe_4096_4096_256_jpeg_resolution
img = open_image_cucim(image)

x_resolution, y_resolution, resolution_unit = resolution

if resolution_unit == "CENTIMETER":
x_spacing = 10000.0 / x_resolution
y_spacing = 10000.0 / y_resolution
spacing_unit = "micrometer"
elif resolution_unit == "INCH":
x_spacing = 25400.0 / x_resolution
y_spacing = 25400.0 / y_resolution
spacing_unit = "micrometer"
else:
x_spacing = x_resolution
y_spacing = y_resolution
spacing_unit = ""

# Returns physical size in tuple.
assert all(map(lambda a, b: math.isclose(a, b, rel_tol=0.1),
img.spacing(), (y_spacing, x_spacing, 1.0)))
# Units for each spacing element (size is same with `ndim`).
assert img.spacing_units() == [spacing_unit, spacing_unit, 'color']

# A metadata object as `dict`
metadata = img.metadata
print(metadata)
assert isinstance(metadata, dict)
assert len(metadata) == 2 # 'cucim' and 'tiff'
assert math.isclose(metadata['tiff']['x_resolution'],
x_resolution, rel_tol=0.00001)
assert math.isclose(metadata['tiff']['y_resolution'],
y_resolution, rel_tol=0.00001)
unit_value = resolution_unit.lower() if resolution_unit != "NONE" else ""
assert metadata['tiff']['resolution_unit'] == unit_value

# Check if lower resolution image's metadata has lower physical spacing.
num_levels = img.resolutions['level_count']
for level in range(num_levels):
lowres_img = img.read_region((0, 0), (100, 100), level=level)
lowres_downsample = img.resolutions["level_downsamples"][level]
assert all(map(lambda a, b: math.isclose(a, b, rel_tol=0.1),
lowres_img.spacing(),
(y_spacing / lowres_downsample,
x_spacing / lowres_downsample, 1.0)))


def test_load_rgba_image_metadata(tmpdir):
"""Test accessing RGBA image's metadata.
Expand Down
Loading

0 comments on commit 39f7c01

Please sign in to comment.