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:
headerThe representation header (specific to each frame), containing:
capture_datetimecapture_device_technology_idcapture_device_vendor_idcapture_device_type_idquality_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 textnumber: the image numberscale_units: the scale unit as a texthorizontal_scan_sampling_ratevertical_scan_sampling_ratehorizontal_image_sampling_ratevertical_image_sampling_rateimage_compression_algo: the compression algo as a textimpression_type: the impression type as text
When reading an image the fields
position,scale_units,image_compression_algoandimpression_typeare 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,020or030) nb_facial_images- The number of representations, i.e. the number of frames
In addition, each frame has the following additional attributes:
headerThe representation header (specific to each frame), containing:
For version
010:landmark_pointsgendereye_colourhair_colourproperty_maskexpressionpose_yawpose_pitchpose_rollpose_uncertainty_yawpose_uncertainty_pitchpose_uncertainty_rollface_image_typeimage_data_typesource_typedevice_typequality
When reading an image the fields
gender,eye_colour,hair_colour,property_mask,expression,face_image_type,image_data_typeandsource_typeare 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
010or030. 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