iso19794 package

This package defines a Pillow extension to support ISO 19794 images.

ISO 19794-4 Images (Print)

Reading

The open() method sets the following info properties

version
Fixed version (020)
nb_representation
The number of representations, i.e. the number of frames
nb_position
The number of position, i.e. the different finger ranks present in the frames
certification_flag
A flag indicating if the certification blocks are included or not

In addition, each frame has the following additional attributes:

header

The representation header (specific to each frame), containing:

  • capture_datetime
  • capture_device_technology_id
  • capture_device_vendor_id
  • capture_device_type_id
  • quality_records: a list of quality records (score, algo_vendor_id, algo_id)
  • certification_records: a list of certification records (authority_id, scheme_id)
  • position: the finger/plam position as a text
  • number: the image number
  • scale_units: the scale unit as a text
  • horizontal_scan_sampling_rate
  • vertical_scan_sampling_rate
  • horizontal_image_sampling_rate
  • vertical_image_sampling_rate
  • image_compression_algo: the compression algo as a text
  • impression_type: the impression type as text

When reading an image the fields position, scale_units, image_compression_algo and impression_type are converted to readable text.

Writing

The save() method can take the following keyword arguments:

save_all
If true, Pillow will save all frames of the image to a multirepresentation file.
append_images
A list of images to append as additional frames. Each of the images in the list can be a single or multiframe image.

Usage

First, let’s create a sample image looking like a fingerprint:

>>> from PIL import Image, ImageDraw
>>> sample = Image.new("L",(200,300),255)
>>> draw = ImageDraw.Draw(sample)
>>> for i in range(20,100,10):
...     for n in range(5):
...         draw.ellipse( (i+n,i+n,200-i-n,300-i-n),outline=0)

To build a single frame image, we first need a representation header. This can be built from a list of key/value.

>>> import datetime
>>> header = dict(
...     capture_datetime = datetime.datetime.now(),
...     capture_device_technology_id=b'\x00',          # unknown
...     capture_device_vendor_id=b'\xab\xcd',
...     capture_device_type_id=b'\x12\x34',
...     quality_records=[],
...     certification_records=[],
...     position='LEFT_INDEX_FINGER',
...     number=1,
...     scale_units='PPI',
...     horizontal_scan_sampling_rate=500,
...     vertical_scan_sampling_rate=500,
...     horizontal_image_sampling_rate=500,
...     vertical_image_sampling_rate=500,
...     image_compression_algo='RAW',
...     impression_type='LIVESCAN_ROLLED'
... )

An image with no representation header will not be generated

>>> import io
>>> buffer = io.BytesIO()
>>> sample.save(buffer,"FIR")
Traceback (most recent call last):
    ...
AttributeError: 'Image' object has no attribute 'header'

Header must be defined on the image for the save operation to work correctly, but a minimal header is also possible (default values will be provided)

>>> sample.header = dict(image_compression_algo='RAW')
>>> buffer = io.BytesIO()
>>> sample.save(buffer,"FIR")

Using a fully defined header:

>>> sample.header = header
>>> buffer = io.BytesIO()
>>> sample.save(buffer,"FIR")
>>> print(len(buffer.getvalue()))   # should be 200*300 + 41 + 16
60057
>>> print(buffer.getvalue()[0:3])
b'FIR'
>>> print(buffer.getvalue()[4:7])
b'020'
>>> print(buffer.getvalue()[14])
0

Multi-frames image is generated with the save_all option:

>>> buffer_multi = io.BytesIO()
>>> sample.save(buffer_multi,"FIR",save_all=True,append_images=[sample])
>>> print(len(buffer_multi.getvalue()))   # should be 2*(200*300 + 41) + 16
120098

Certification blocks will alter the flag in the header:

>>> header['certification_records'] = [FIRCertificationRecord(b'\x78\xab',b'\x01')]
>>> sample.header = header
>>> buffer = io.BytesIO()
>>> sample.save(buffer,"FIR")
>>> print(len(buffer.getvalue()))   # should be 200*300 + 42 + 3 + 16
60061
>>> print(buffer.getvalue()[14])
1

Image format is automatically detected:

>>> nsample = Image.open(buffer)
>>> nsample.mode
'L'
>>> nsample.size
(200, 300)
>>> nsample.header['certification_records'][0].authority_id
b'x\xab'

For a single frame image, seek will fail if we want to access the second frame:

>>> nsample.seek(1)
Traceback (most recent call last):
    ...
EOFError: attempt to seek outside sequence

But it will not fail for a true multi-frame image:

>>> nsample = Image.open(buffer_multi)
>>> nsample.info['nb_representation']
2
>>> nsample.info['nb_position']
1
>>> nsample.seek(1)
>>> nsample.mode
'L'
>>> nsample.size
(200, 300)

Image can be saved in JPEG format:

>>> buffer = io.BytesIO()
>>> sample.header['image_compression_algo'] ='JPEG'
>>> sample.save(buffer,"FIR")
>>> print(len(buffer.getvalue()) < 60061)   # should be less than 200*300 + 42 + 3 + 16
True

The same for a multiframe image:

>>> nsample = Image.open(buffer_multi)
>>> buffer = io.BytesIO()
>>> nsample.header['image_compression_algo'] ='JPEG'
>>> nsample.save(buffer,"FIR",save_all=True)
>>> print(len(buffer.getvalue())>61000 and  len(buffer.getvalue())<120098)
True

Both frames can be compressed:

>>> buffer = io.BytesIO()
>>> nsample.seek(1)
>>> nsample.header['image_compression_algo'] ='JPEG'
>>> nsample.save(buffer,"FIR",save_all=True)
>>> print(len(buffer.getvalue())>61000 and  len(buffer.getvalue())<90000)
True

And then read again:

>>> nsample2 = PIL.Image.open(buffer)
>>> data = nsample2.load()  # force decoding of the image
>>> nsample2.seek(1)
>>> data = nsample2.load()

Jpeg2000 is also supported (see https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html#jpeg-2000 for the prerequisites)

>>> buffer = io.BytesIO()
>>> sample.header['image_compression_algo'] ='JPEG2000_LOSSY'
>>> sample.save(buffer,"FIR")
>>> print(len(buffer.getvalue()) < 6000)
True
>>> sample2 = PIL.Image.open(buffer)
>>> data = sample2.load()
>>> sample2.tobytes()==sample.tobytes()
False
>>> buffer = io.BytesIO()
>>> sample.header['image_compression_algo'] ='JPEG2000_LOSSLESS'
>>> sample.save(buffer,"FIR")
>>> print(len(buffer.getvalue()) > 20000)
True
>>> sample2 = PIL.Image.open(buffer)
>>> data = sample2.load()
>>> sample2.tobytes()==sample.tobytes()
True

Using an invalid compression algo will raise an exception:

>>> buffer = io.BytesIO()
>>> sample.header['image_compression_algo'] ='UNKNOWN'
>>> sample.save(buffer,"FIR")
Traceback (most recent call last):
    ...
SyntaxError: Unknown compression algo UNKNOWN

ISO 19794-5 Images (Face)

Reading

The open() method sets the following info properties

version
Version (010, 020 or 030)
nb_facial_images
The number of representations, i.e. the number of frames

In addition, each frame has the following additional attributes:

header

The representation header (specific to each frame), containing:

  • For version 010:

    • landmark_points
    • gender
    • eye_colour
    • hair_colour
    • property_mask
    • expression
    • pose_yaw
    • pose_pitch
    • pose_roll
    • pose_uncertainty_yaw
    • pose_uncertainty_pitch
    • pose_uncertainty_roll
    • face_image_type
    • image_data_type
    • source_type
    • device_type
    • quality

    When reading an image the fields gender, eye_colour, hair_colour, property_mask, expression, face_image_type, image_data_type and source_type are converted to readable text.

Writing

The save() method can take the following keyword arguments:

save_all
If true, Pillow will save all frames of the image to a multirepresentation file.
append_images
A list of images to append as additional frames. Each of the images in the list can be a single or multiframe image.
version
The version of the format to use, one of 010 or 030. If not provided and if the image was loaded from an ISO 19794 image, the same version will be used.

Usage

First, let’s create a sample image:

>>> from PIL import Image, ImageDraw
>>> sample = Image.new("RGB",(200,300),255)
>>> draw = ImageDraw.Draw(sample)
>>> for i in range(20,100,10):
...     for n in range(5):
...         draw.ellipse( (i+n,i+n,200-i-n,300-i-n),outline=0)

To build a single frame image, we first need a representation header. This can be built from a list of key/value.

>>> import datetime
>>> header = dict(
...     landmark_points=[],
...     gender='M',
...     eye_colour='BLUE',
...     hair_colour='BLACK',
...     property_mask=['GLASSES'],
...     expression='NEUTRAL',
...     pose_yaw=0,
...     pose_pitch=0,
...     pose_roll=0,
...     pose_uncertainty_yaw=0,
...     pose_uncertainty_pitch=0,
...     pose_uncertainty_roll=0,
...     face_image_type='FULL_FRONTAL',
...     image_data_type='JPEG',
...     source_type='STATIC_CAMERA',
...     device_type=b'\x00\x00',
...     quality=b'\x00\x00',
...    )

An image with no representation header will not be generated

>>> import io
>>> buffer = io.BytesIO()
>>> sample.save(buffer,"FAC", version='010')
Traceback (most recent call last):
    ...
AttributeError: 'Image' object has no attribute 'header'

Header must be defined on the image for the save operation to work correctly, but a minimal header is also possible (default values will be provided)

>>> sample.header = dict()
>>> buffer = io.BytesIO()
>>> sample.save(buffer,"FAC", version='010')

Using a fully defined header:

>>> sample.header = header
>>> buffer = io.BytesIO()
>>> sample.save(buffer,"FAC", version='010')
>>> print(len(buffer.getvalue()))   # should be 200*300 + 41 + 16
55373
>>> print(buffer.getvalue()[0:3])
b'FAC'
>>> print(buffer.getvalue()[4:7])
b'010'
>>> print(buffer.getvalue()[14])
0

Multi-frames image is generated with the save_all option:

>>> buffer_multi = io.BytesIO()
>>> sample.save(buffer_multi,"FAC",save_all=True,append_images=[sample], version='010')
>>> print(len(buffer_multi.getvalue()))   # should be 2*(200*300 + 41) + 16
110732
>>> nsample = Image.open(buffer)
>>> nsample.mode
'RGB'
>>> nsample.size
(200, 300)

For a single frame image, seek will fail if we want to access the second frame:

>>> nsample.seek(1)
Traceback (most recent call last):
    ...
EOFError: attempt to seek outside sequence

But it will not fail for a true multi-frame image:

>>> nsample = Image.open(buffer_multi)
>>> nsample.info['nb_facial_images']
2
>>> nsample.seek(1)
>>> nsample.mode
'RGB'
>>> nsample.size
(200, 300)

Image can be saved in JPEG format:

>>> buffer = io.BytesIO()
>>> sample.header['image_data_type'] ='JPEG'
>>> sample.save(buffer,"FAC", version="010")
>>> print(len(buffer.getvalue()) < 60061)   # should be less than 200*300 + 42 + 3 + 16
True

The same for a multiframe image:

>>> nsample = Image.open(buffer_multi)
>>> buffer = io.BytesIO()
>>> nsample.header['image_data_type'] = 'JPEG'
>>> nsample.save(buffer,"FAC",version='010',save_all=True)
>>> print(len(buffer.getvalue())>61000 and  len(buffer.getvalue())<120098)
True

Both frames can be compressed:

>>> buffer = io.BytesIO()
>>> nsample.seek(1)
>>> nsample.header['image_data_type'] = 'JPEG'
>>> nsample.save(buffer,"FAC",version='010',save_all=True)
>>> print(len(buffer.getvalue())>61000 and  len(buffer.getvalue())<90000)
True

And then read again:

>>> nsample2 = PIL.Image.open(buffer)
>>> data = nsample2.load()  # force decoding of the image
>>> nsample2.seek(1)
>>> data = nsample2.load()

Jpeg2000 is also supported (see https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html#jpeg-2000 for the prerequisites)

>>> buffer = io.BytesIO()
>>> sample.header['image_data_type'] = 'JPEG2000'
>>> sample.save(buffer,"FAC",version='010')
>>> print(len(buffer.getvalue()) < 27000)
True
>>> sample2 = PIL.Image.open(buffer)
>>> data = sample2.load()
>>> sample2.tobytes()==sample.tobytes()
True
>>> buffer = io.BytesIO()
>>> sample.header['image_data_type'] = 'JPEG2000'
>>> sample.save(buffer,"FAC",version='010')
>>> print(len(buffer.getvalue()) > 20000)
True
>>> sample2 = PIL.Image.open(buffer)
>>> data = sample2.load()
>>> sample2.tobytes()==sample.tobytes()
True

Using an invalid compression algo will raise an exception:

>>> buffer = io.BytesIO()
>>> sample.header['image_data_type'] = 'UNKNOWN'
>>> sample.save(buffer,"FAC",version='010')
Traceback (most recent call last):
    ...
SyntaxError: Unknown compression algo UNKNOWN