vigra icon indicating copy to clipboard operation
vigra copied to clipboard

make vigra capable of reading tiled TIFF images

Open hmeine opened this issue 13 years ago • 9 comments

I would like to use the vigra library on images saved from MeVisLab.

Background: MeVisLab uses a DCM/TIFF format, in which a .dcm file contains the medical meta information and a corresponding .tif file contains the image data. However, this uses tiled TIFF format (as MeVisLab uses a tiled image representation internally to be able to handle arbitrarily large images). This is rather esoteric in the sense that really not many programs can read tiled TIFFs.

I have had a glance at the tiled image loading documentation of libtiff, but it is quite terse. The existing TIFFDecoderImpl in tiff.cxx is already quite complex, so I hope I won't mess things up.

What particularly struck me, was that Andrew Mihal has explicitly changed back from the strip-based, to a scanline-based loading method in 2004 (commenting that the former can be a memory hog), see the tiff.?xx changes in 86fda5d3b631488490961372dfcdcb4d2f8f4554. However, I think going to a tile-based loader would revert this change.

hmeine avatar Jul 19 '11 14:07 hmeine

Ulli, the impex API seems to be based on nextScanline() calls; does this pose restrictions that you can see which are relevant to a tile-based loader?

hmeine avatar Jul 19 '11 14:07 hmeine

Tiled image I/O only makes sense if the import function is able to read only part of the image. At present, this is not supported by importImage(). We could add a function 'ImageImportInfo.setInputROI(Rect2D)' to restrict reading to part of the data. The corresponding destination image is then assumed to be of the appropriate size. The internal implementation of the reading process (tiled or scanline-wise) would be completly hidden from the user.

As a side note: The class HDF5File already supports similar functionality. Maybe you should implement a TIFFFile class along the same lines? It seems that TIFF has a somewhat special role in scientific imaging (for example, see OME-TIFF (http://www.ome-xml.org/wiki/OmeTiff) for microscopy, and multi-page TIFF in issue #5) that may warrant special treatment.

ukoethe avatar Jul 19 '11 15:07 ukoethe

If I recall correctly, the nextScanline() API is part of the required interface (abstract base class) that all codecs must implement. As such, the impex library is pretty much build around per-scanline reading. I'm not sure whether there is a simple way to relax this design decision.

ukoethe avatar Jul 19 '11 15:07 ukoethe

You wrote:

Tiled image I/O only makes sense if the import function is able to read only part of the image. At present, this is not supported by importImage().

Why? Maybe there's a misunderstanding - I plan to load the full volume ("stack"/sequence of images within a TIFF file) into memory; reading only part would be a possible future extension, related but not necessary.

Our default tiles are quite small (128 or 256 squared I think), such that even "normal-sized" volumes are not readable by Vigra currently.

For instance, here's some tiffinfo output:

TIFF Directory at offset 0x389a1ca (59351498)
  Image Width: 512 Image Length: 512 Image Depth: 173
  Tile Width: 128 Tile Length: 128 Tile Depth: 1      <=== NOTE THIS
  Bits/Sample: 16
  Sample Format: unsigned integer
  Compression Scheme: LZW
  Photometric Interpretation: min-is-black
  Orientation: row 0 top, col 0 lhs
  Samples/Pixel: 1
  Min Sample Value: 0
  Max Sample Value: 4095
  SMin Sample Value: 0
  SMax Sample Value: 4095
  Planar Configuration: single image plane
  Software: MFL MeVis File Format Library, TIFF Module

I think I will add a precondition right now that will throw an exception if tile size < image size; as it stands now, you'll get an error message "Can not read scanlines from a tiled image." on stderr for every call to TIFFReadScanline(), but Vigra itself will not notice (but deliver an empty image).

Concerning nextScanline(), you wrote:

the impex library is pretty much build around per-scanline reading. I'm not sure whether there is a simple way to relax this design decision.

That's what I was worried about.

Maybe it is simpler for me to convert the image files. That'd be a shame though, given that we have an image library that makes it very easy to work on parts of images, so I hoped that I could simply go through the list of tiles, and perform loading of the respective ROI within the target image.

Of course, that does not work if the order is dictated by the surrounding API, yet I am still not sure if that's the case. Maybe nextScanline() is just a speaking name for "loadNextChunk", I need to check that.

hmeine avatar Jul 20 '11 08:07 hmeine

OK, I had a look at read_bands() in impex.hxx, and it is indeed fundamentally based on the assumption that nextScanline() will provide the next scanline in buffers accessible via currentScanlineOfBand(). Changing this is made more difficult by the fact that there are multiple "optimized" variants of read_bands().

I start to question the whole design.. ;-) For instance, I wonder if this is really not premature optimization, and other places would have higher potential for optimization than the access of the target image of read_bands.

Anyhow, I will not touch this right now, since IIRC it is already not the first implementation anymore, and I would be afraid to re-introduce problems solved with the current design. What would be nice would be some design documentation for us developers; that would not need to be complete, nor nicely formatted, but any hints on why the API looks like this would be helpful.

hmeine avatar Jul 20 '11 13:07 hmeine

OK, some more interesting findings about our DCM/TIFF format:

  • TIFF poses some restrictions on tile sizes, i.e. they need to be multiples of 16. (Incomplete tiles increase the file size and are cropped after loading.)
  • 3D TIFFs can only be read using the tile-based libtiff API.
  • It is very hard to save non-tiled TIFFs from MeVisLab with the current implementation, i.e. one can adjust the tile size within the "ImageSave" module panel, but not above the image size. Thus, it is only possible if the image size is a multiple of 16.

tiffdump gives us a nice idea of what these files look like:

Magic: 0x4949 <little-endian> Version: 0x2a
Directory 0: offset 213000 (0x34008) next 0 (0)
ImageWidth (256) SHORT (3) 1<53>
ImageLength (257) SHORT (3) 1<53>
BitsPerSample (258) SHORT (3) 1<16>
Compression (259) SHORT (3) 1<1>
Photometric (262) SHORT (3) 1<1>
ImageDescription (270) ASCII (2) 9<MeVisLab\0>
Orientation (274) SHORT (3) 1<1>
SamplesPerPixel (277) SHORT (3) 1<1>
MinSampleValue (280) SHORT (3) 1<0>
MaxSampleValue (281) SHORT (3) 1<4095>
XResolution (282) RATIONAL (5) 1<13.4737>
YResolution (283) RATIONAL (5) 1<13.4737>
PlanarConfig (284) SHORT (3) 1<1>
ResolutionUnit (296) SHORT (3) 1<3>
Software (305) ASCII (2) 43<MFL MeVis File Format Li ...>
TileWidth (322) SHORT (3) 1<32>
TileLength (323) SHORT (3) 1<32>
TileOffsets (324) LONG (4) 104<8 2056 4104 6152 8200 10248 12296 14344 16392 18440 20488 22536 24584 26632 28680 30728 32776 34824 36872 38920 40968 43016 45064 47112 ...>
TileByteCounts (325) LONG (4) 104<2048 2048 2048 2048 2048 2048 2048 2048 2048 2048 2048 2048 2048 2048 2048 2048 2048 2048 2048 2048 2048 2048 2048 2048 ...>
SampleFormat (339) SHORT (3) 1<1>
SMinSampleValue (340) SHORT (3) 1<0>
SMaxSampleValue (341) SHORT (3) 1<4095>
ImageDepth (Silicon Graphics) (32997) LONG (4) 1<26>
TileDepth (Silicon Graphics) (32998) LONG (4) 1<1>

What I deduced from this:

  • It is not a multi-page TIFF, but there is just one TIFF directory and one TIFF image data block.
  • The 3D extents of the image and tiles are given by the TileDepth and ImageDepth fields, which are obviously not from the original TIFF standard, but from an SGI extension.
  • The tiles are conveniently indexed by the TileOffsets tag; in fact this is probably an implementation detail, since the data is read via TIFFReadTile.
  • These 3D TIFFs are non-standard; it is not easy to find programs which can read them. (Some GIS software seems to use 2D tiled TIFFs, but I could not find any image capable of reading our TIFF files in a quick try.)

Anyhow, back to VIGRA - what do you think, wouldn't it be nice to have a "real" 3D image file format (except HDF5)? Unfortunately, it seems to be more work than I thought.

hmeine avatar Jul 20 '11 14:07 hmeine

Anyhow, back to VIGRA - what do you think, wouldn't it be nice to have a "real" 3D image file format (except HDF5)?

Yes, it would be nice. From what I hear from our collaborators, I'd say that OME-TIFF is a good choice for a start.

Unfortunately, it seems to be more work than I thought.

Indeed. What about that idea of a TIFFFile class?

OK, I had a look at read_bands() in impex.hxx, and it is indeed fundamentally based on the assumption that nextScanline() will provide the next scanline in buffers accessible via currentScanlineOfBand().

If you just want to import an image that happens to be stored in tiles (but don't need to read arbitrary subimages), you can easily implement nextScanline() by means of an additional internal buffer that holds as many scanlines as the height of the tiles. Then simply read a row of horizontally adjacent tiles into the buffer (corresponding to e.g. 128 scanlines), and read from the buffer until it is exhausted. This shouldn't be too much work and will not break the current design.

What would be nice would be some design documentation for us developers; that would not need to be complete, nor nicely formatted, but any hints on why the API looks like this would be helpful.

If you want to ask the original people: the impex library was designed by Gunnar Kedenburg and revised by the Hugin developers (with some minor additions by myself).

ukoethe avatar Jul 20 '11 18:07 ukoethe

Unfortunately, it seems to be more work than I thought.

Indeed. What about that idea of a TIFFFile class?

I had the same idea. That could also help with sharing code between the 2D and 3D impex parts.

One more important conclusion from all my TIFF-related findings: while there are three general reading APIs (ignoring the special cased RGBAImage convenience API) described in http://libtiff.maptools.org/libtiff.html (scanline-based, strip-based, and tile-based), they are not interchangeable. In particular, I wrote above that the scanline-based API cannot read tiled TIFFs, but it is equally important to know that the tile-based API cannot read stripped images!

If you just want to import an image that happens to be stored in tiles (but don't need to read arbitrary subimages), you can easliy implement nextScanline() by means of an additional internal buffer that holds as many scanlines as the height of the tiles.

OK, that would be possible indeed.

However, as I see it now, the path to fastest success is to write a tile-based TIFF loader just for importVolume, totally independent of the existing 2D impex code.

For this, it probably makes sense to wrap libtiff in a TIFFFile class, which would then make future code sharing possible.

hmeine avatar Jul 21 '11 08:07 hmeine

wouldn't it be nice to have a "real" 3D image file format (except HDF5)?

Yes, it would be nice. From what I hear from our collaborators, I'd say that OME-TIFF is a good choice for a start.

Is OME-TIFF really related? If you have any 3D / tiled example files from your collaborators, that would be great. In case that that would be easier, it could be similarly useful to just get the output of "tiffdump foo.tif", which seems to list all tiff tags, and thus gives a good description of the file (see above).

hmeine avatar Jul 21 '11 10:07 hmeine