Introduction to TIFF Images

The Tag Image File Format (TIFF or TIF) is a complex format (full spec here). In particular, each TIFF file may contain several related images (frames) and additional information (tags) organized into an Image File Directory (IFD) structure. IFD uses a key-value format, similar to a python dictionnay - all keys are numbers. Many tags are predefined, such at the tags providing the image width, length, compression, ...> > The Class TIFFImage defined below handles commonly needed actions with TIFF such as retrieving tags values, adding new tags, exploring all the frames, extracting all or specific frames into separate image files ('tiff' or 'jpg').

The module PIL.TiffImagePlugin offers a set of classes to easily handle TIFF, among which:> - loading TIFF files and work with frames using TiffImageFile(path). It operates like Image but with specific methods and properties for TIFF. For example, .seek(frame_nbr) selects one of the frames and makes it active. After that, any action on the TiffImageFile object will apply to that particular frame. .n_frames to get the total numer of frames in the file. It will inherit properties such as size, ...> - IFD predefined tags can be accessed through PIL.TiffTags. > - Add new tags using the correct IFD structure with PIL.TiffImagePlugin.ImageFileDirectory_v2.

p2tiff = Path('../test/img/tiff/tif-multi-frame-with-tags.tif').resolve()
tiff = TiffImageFile(p2tiff)

Display n frames from the loaded TIFF image file

n = 3
for frame in range(min(n, tiff.n_frames)):
    print(f"--- Frame {frame} {'-'*80}")
    display(tiff.resize((150,150)))
    tiff.seek(frame)
    print(f"This frame has size {tiff.size}")
    print(f"The image was loaded from {tiff.filename}")
--- Frame 0 --------------------------------------------------------------------------------
This frame has size (512, 512)
The image was loaded from /home/vtec/unpackai/test/img/tiff/tif-multi-frame-with-tags.tif
--- Frame 1 --------------------------------------------------------------------------------
This frame has size (512, 512)
The image was loaded from /home/vtec/unpackai/test/img/tiff/tif-multi-frame-with-tags.tif
--- Frame 2 --------------------------------------------------------------------------------
This frame has size (512, 512)
The image was loaded from /home/vtec/unpackai/test/img/tiff/tif-multi-frame-with-tags.tif

Example of pre-defined tags and their desciption:

predefined_tag_sublist = [256, 257, 259, 269, 270, 33432, 34665, 34675, 34853]
print(f"  Tag Nbr | Tag Name")
for tag_nbr in predefined_tag_sublist:
    print(f"  {tag_nbr:5d}   | {TiffTags.TAGS_V2[tag_nbr].name}")
  Tag Nbr | Tag Name
    256   | ImageWidth
    257   | ImageLength
    259   | Compression
    269   | DocumentName
    270   | ImageDescription
  33432   | Copyright
  34665   | ExifIFD
  34675   | ICCProfile
  34853   | GPSInfoIFD

It is possible to retrieve all existing tags to one frame.

  • .tag_v2.keys(), .tag_v2.values(), .tag_v2.items(), .tag_v2.get(tag_nbr) as in a dictionary
  • .ifd to retrieve the entire Image File Directory. It behaves as a dictionary.
  • TiffTags.TAGS_V2 is a fixed dictionary that gives access to the predefined tags and their meanings

List of predefined tags in the loaded TIFF image:

len(tiff.tag_v2.keys())
14
tiff.seek(0)
tags_predefined = [tag for tag in tiff.tag_v2.keys() if tag in list(TiffTags.TAGS_V2.keys())]
[f"{tag:5d} - {TiffTags.TAGS_V2[tag].name:30s}:  {tiff.tag_v2.get(tag)}" for tag in tags_predefined]
['  256 - ImageWidth                    :  512',
 '  257 - ImageLength                   :  512',
 '  259 - Compression                   :  1',
 '  262 - PhotometricInterpretation     :  1',
 '  270 - ImageDescription              :  {"shape": [512, 512]}',
 '  273 - StripOffsets                  :  (320,)',
 '  277 - SamplesPerPixel               :  1',
 '  278 - RowsPerStrip                  :  512',
 '  279 - StripByteCounts               :  (32768,)',
 '  282 - XResolution                   :  1.0',
 '  283 - YResolution                   :  1.0',
 '  296 - ResolutionUnit                :  1',
 '  305 - Software                      :  tifffile.py']

List of custom tags in the loaded TIFF image:

tags_custom = [tag for tag in tiff.tag_v2.keys() if tag not in list(TiffTags.TAGS_V2.keys())]
[f"{str(tag):38s}:  {tiff.tag_v2.get(tag)}" for tag in tags_custom]
['65001                                 :  Specular reflection']

Class TiffImage

class TiffImage[source]

TiffImage(path:Union[Path, str])

Load and handle TIFF images, such as viewing frames, extracting frames and saving them as single image files.

TiffImage loads a TIFF image from a path (Path or str) and return a TiffImage object that:

  • gives access to the number of frames 'n_frames', all the tags 'tags' and the image size in pixel 'size'.
  • allows to return a specific frame as an object
  • allows to extract and save all or any frame as 'tif' or 'jpg image file(s)'
  • provides a repr including information on each the frames in the image
  • allows to show thumbnails of all frames in a grid.

Abreviations in the code are according to https://docs.fast.ai/dev/abbr.html

Examples

Create a new instance of TiffImage and display its default representation.

tiff = TiffImage(p2tiff)
tiff
<unpackai.cv.data.TiffImage> TIFF image file with 8 frames.
  Loaded from /home/vtec/unpackai/test/img/tiff/tif-multi-frame-with-tags.tif.
  Frame Content Summary:
     Frame |     Size      | Nbr Tags 
       0   |  (512, 512)   |    14    
       1   |  (512, 512)   |    14    
       2   |  (512, 512)   |    14    
       3   |  (512, 512)   |    14    
       4   |  (512, 512)   |    14    
       5   |  (512, 512)   |    14    
       6   |  (512, 512)   |    14    
       7   |  (512, 512)   |    14    
  To show image thumbnails, use method '.show(frame_nbr)' of '.show_all()'

Explore the TIFF file frames

tiff.show(4)
tiff.show_all()
Showing frame 4 out of 8:
Showing all 8 frames:

Access the TiffImage properties:

tiff.n_frames, tiff.size
(8, (512, 512))
tiff.tags
{256: 512,
 257: 512,
 259: 1,
 262: 1,
 270: '{"shape": [512, 512]}',
 273: (231856,),
 277: 1,
 278: 512,
 279: (32768,),
 282: 1.0,
 283: 1.0,
 296: 1,
 65001: 'Oral mucosa',
 305: 'tifffile.py'}
tiff.tags[65001]
'Oral mucosa'

Retrieve a specific frame as a PIL.Image.Image object

tiff.get_frame(4)
type(tiff.get_frame(0)), isinstance(tiff.get_frame(0), Image.Image)
(PIL.TiffImagePlugin.TiffImageFile, True)

Review all the tags for one frame

tiff.summary_tags(0)
| Tag Nbr |      Predefined Tag Name      |                              Tag Value                                |
|================================================================================================================ |
|   256   | ImageWidth                    | 512                                                                   |
|   257   | ImageLength                   | 512                                                                   |
|   259   | Compression                   | 1                                                                     |
|   262   | PhotometricInterpretation     | 1                                                                     |
|   270   | ImageDescription              | {"shape": [512, 512]}                                                 |
|   273   | StripOffsets                  | (320,)                                                                |
|   277   | SamplesPerPixel               | 1                                                                     |
|   278   | RowsPerStrip                  | 512                                                                   |
|   279   | StripByteCounts               | (32768,)                                                              |
|   282   | XResolution                   | 1.0                                                                   |
|   283   | YResolution                   | 1.0                                                                   |
|   296   | ResolutionUnit                | 1                                                                     |
|   305   | Software                      | tifffile.py                                                           |
|  65001  |                               | Specular reflection                                                   |

Extract one or several frames to be saved as 'jpg' or 'tif'

with TemporaryDirectory() as tdir:
    p2dir = Path(tdir)
    print('Files in the temporary directory prior to extraction:')
    print('/n'.join([f" - {f.name}" for f in p2dir.iterdir()]))

    tiff.extract_one_frame(frame_nbr=2, dest=p2dir)
    tiff.extract_one_frame(frame_nbr=3, dest=p2dir, image_format='tif')
    tiff.extract_one_frame(frame_nbr=4, dest=p2dir, fname='specific-name')

    
    print('Files in the temporary directory after extraction:')
    print('\n'.join([f" - {f.name}" for f in p2dir.iterdir()]))
Files in the temporary directory prior to extraction:

Files in the temporary directory after extraction:
 - specific-name.jpg
 - tif-multi-frame-with-tags-0003.tif
 - tif-multi-frame-with-tags-0002.jpg
with TemporaryDirectory() as tdir:
    p2dir = Path(tdir)
    print('Files in the temporary directory prior to extraction:')
    print('/n'.join([f" - {f.name}" for f in p2dir.iterdir()]))
    
    tiff.extract_frames(dest=p2dir, naming_method='counter')

    print('Files in the temporary directory after extraction:')
    print('\n'.join([f" - {f.name}" for f in p2dir.iterdir()]))
Files in the temporary directory prior to extraction:

Files in the temporary directory after extraction:
 - tif-multi-frame-with-tags-0000.tif
 - tif-multi-frame-with-tags-0004.tif
 - tif-multi-frame-with-tags-0006.tif
 - tif-multi-frame-with-tags-0005.tif
 - tif-multi-frame-with-tags-0003.tif
 - tif-multi-frame-with-tags-0001.tif
 - tif-multi-frame-with-tags-0002.tif
 - tif-multi-frame-with-tags-0007.tif
with TemporaryDirectory() as tdir:
    p2dir = Path(tdir)
    print('Files in the temporary directory prior to extraction:')
    print('/n'.join([f" - {f.name}" for f in p2dir.iterdir()]))
    
    tiff.extract_frames(dest=p2dir, naming_method='tag_value', tag=65001)

    print('Files in the temporary directory after extraction:')
    print('\n'.join([f" - {f.name}" for f in p2dir.iterdir()]))
Files in the temporary directory prior to extraction:

Files in the temporary directory after extraction:
 - tif-multi-frame-with-tags-Hair.tif
 - tif-multi-frame-with-tags-Skin.tif
 - tif-multi-frame-with-tags-Specular_reflection.tif
 - tif-multi-frame-with-tags-Attached_gingiva.tif
 - tif-multi-frame-with-tags-Oral_mucosa.tif
 - tif-multi-frame-with-tags-Enamel.tif
 - tif-multi-frame-with-tags-Stain.tif
 - tif-multi-frame-with-tags-Marginal_gingiva.tif

Tests

# For Test Cases (might have duplicate import because it will be in a dedicated file)
from pathlib import Path
from PIL import Image
from tempfile import TemporaryDirectory
from typing import List

import pytest

from test_common.utils_4_tests import DATA_DIR, IMG_DIR
from test_utils import GITHUB_TEST_DATA_URL, check_connection_github
tiff_fname = 'tif-multi-frame-with-tags.tif'
gif_fname = 'animated.gif'
LOCAL_TEST_TIFF = IMG_DIR / "tiff" / tiff_fname
LOCAL_TEST_GIF = IMG_DIR / "tiff" / gif_fname

LOCAL_TEST_SUMMARY_TAGS = DATA_DIR / 'tiff_summary_tags.txt'
LOCAL_TEST_REPR = DATA_DIR / 'tiff_repr.txt'

GITHUB_TEST_IMG_DIR_URL = "https://raw.githubusercontent.com/unpackAI/unpackai/main/test/img"
GITHUB_TEST_TIFF = f"{GITHUB_TEST_IMG_DIR_URL}/tiff/{tiff_fname}"
GITHUB_TEST_GIF = f"{GITHUB_TEST_IMG_DIR_URL}/tiff/{gif_fname}"

def test_exception_on_missing_path():
    with pytest.raises(FileExistsError) as msg:
        TiffImage('blabla.tif')
    expected_msg = f"Cannot find path {'blabla.tif'}"
    assert str(msg.value) == expected_msg
        
def test_exception_on_wrong_suffix():
    with pytest.raises(ValueError) as msg:
        TiffImage(IMG_DIR/'tiff'/gif_fname)
    expected_msg = f"Image file should be .tif or .tiff, not '{'.gif'}'"
    assert str(msg.value) == expected_msg

@pytest.fixture(scope="session")
def test_tiff_image():
    """Fixture to pass the test TIFF image path"""
    return LOCAL_TEST_TIFF

@pytest.fixture(scope="session")
def test_repr():
    return LOCAL_TEST_REPR.read_text()

@pytest.fixture(scope="session")
def test_summary_tag_printout():
    return LOCAL_TEST_SUMMARY_TAGS.read_text()

@pytest.fixture(scope="session")
def local_TiffImage(test_tiff_image):
    """Create instance of TiffImage by loading the test TIFF image"""
    return TiffImage(test_tiff_image)

class Test_TiffImage:
    """Testing class for TiffImage"""
       
    def test_n_frames(self, local_TiffImage):
        """Test property n_frame"""
        expected_value = 8
        assert local_TiffImage.n_frames == expected_value
        
    def test_size(self, local_TiffImage):
        """Test property Size"""
        expected_value = (512, 512)
        assert local_TiffImage.size == expected_value
        
    def test_tags(self, local_TiffImage):
        """Test returned dictionary"""
        expected_value_0 = {256: 512, 257: 512, 259: 1, 262: 1, 270: '{"shape": [512, 512]}', 
                            273: (320,), 277: 1, 278: 512, 279: (32768,), 282: 1.0, 283: 1.0, 296: 1,
                            65001: 'Specular reflection',
                            305: 'tifffile.py'}
        expected_value_1 = {256: 512, 257: 512, 259: 1, 262: 1, 270: '{"shape": [512, 512]}',
                            273: (33408,), 277: 1, 278: 512, 279: (32768,), 282: 1.0, 283: 1.0, 296: 1, 
                            65001: 'Attached gingiva', 
                            305: 'tifffile.py'}
        local_TiffImage.tiff.seek(0)
        assert local_TiffImage.tags == expected_value_0
        local_TiffImage.tiff.seek(1)
        assert local_TiffImage.tags == expected_value_1
    
    def test_repr(self, local_TiffImage, capsys, test_repr):
        """Test the __repr__"""
        expected_value = test_repr
        local_TiffImage.tiff.seek(0)
        print(local_TiffImage)
        captured = capsys.readouterr()
        assert captured.out == expected_value
        
    def test_summary_tags_printout(self, local_TiffImage, capsys, test_summary_tag_printout):
        """Test the tag summary method"""        
        expected_value = test_summary_tag_printout
        local_TiffImage.tiff.seek(0)
        local_TiffImage.summary_tags()
        captured = capsys.readouterr()
        assert captured.out == expected_value
        
    def test_type_returned_by_get_frame(self, local_TiffImage):
        """Test that object is 'PIL.TiffImagePlugin.TiffImageFile'"""
        frame = local_TiffImage.get_frame(0)
        assert isinstance(frame, TiffImageFile)
        
    def test_extract_one_frame(self, local_TiffImage):
        """Test that frames are extracted and saved"""
        with TemporaryDirectory() as tdir:
            p2dir = Path(tdir)        
            tiff.extract_one_frame(frame_nbr=2, dest=p2dir)
            assert (p2dir / 'tif-multi-frame-with-tags-0002.jpg').is_file()
            tiff.extract_one_frame(frame_nbr=3, dest=p2dir, image_format='tif')
            assert (p2dir / 'tif-multi-frame-with-tags-0003.tif').is_file()
            tiff.extract_one_frame(frame_nbr=4, dest=p2dir, fname='specific-name')
            assert (p2dir / 'specific-name.jpg').is_file()

    def test_extract_frames_by_tag(self, local_TiffImage):
        """Test that all frames are saved and naming by tag is correct """
        with TemporaryDirectory() as tdir:
            p2dir = Path(tdir)        
            tiff.extract_frames(dest=p2dir, naming_method='tag_value', tag=65001)
            assert (p2dir / 'tif-multi-frame-with-tags-Hair.tif').is_file()
            assert (p2dir / 'tif-multi-frame-with-tags-Skin.tif').is_file()
            assert (p2dir / 'tif-multi-frame-with-tags-Stain.tif').is_file()
            assert (p2dir / 'tif-multi-frame-with-tags-Marginal_gingiva.tif').is_file()
            assert len(list(p2dir.iterdir())) == local_TiffImage.n_frames

    def test_extract_frames_by_nbr(self, local_TiffImage):
        """Test that all frames are saved and naming by number is correct """
        with TemporaryDirectory() as tdir:
            p2dir = Path(tdir)        
            tiff.extract_frames(dest=p2dir)
            assert (p2dir / 'tif-multi-frame-with-tags-0001.tif').is_file()
            assert (p2dir / 'tif-multi-frame-with-tags-0002.tif').is_file()
            assert (p2dir / 'tif-multi-frame-with-tags-0005.tif').is_file()
            assert (p2dir / 'tif-multi-frame-with-tags-0007.tif').is_file()
            assert len(list(p2dir.iterdir())) == local_TiffImage.n_frames

    def test_is_valid_frame_(self, local_TiffImage):
        nframes = local_TiffImage.n_frames
        assert local_TiffImage.is_valid_frame_(0)
        with pytest.raises(ValueError) as msg:
            local_TiffImage.is_valid_frame_(nframes + 1)
        expected_msg = f"'frame_nbr' is {nframes+1} but this TIFF file only has {nframes} frames."
        assert str(msg.value)[:-1] == expected_msg