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 usingTiffImageFile(path)
. It operates likeImage
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 theTiffImageFile
object will apply to that particular frame..n_frames
to get the total numer of frames in the file. It will inherit properties such assize
, ...> - IFD predefined tags can be accessed throughPIL.TiffTags
. > - Add new tags using the correct IFD structure withPIL.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}")
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}")
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())
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]
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]
Class TiffImage
Create a new instance of TiffImage
and display its default representation.
tiff = TiffImage(p2tiff)
tiff
Explore the TIFF file frames
tiff.show(4)
tiff.show_all()
Access the TiffImage
properties:
tiff.n_frames, tiff.size
tiff.tags
tiff.tags[65001]
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)
Review all the tags for one frame
tiff.summary_tags(0)
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()]))
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()]))
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()]))
# 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