summaryrefslogtreecommitdiff
path: root/utils/tuning/libtuning/image.py
diff options
context:
space:
mode:
Diffstat (limited to 'utils/tuning/libtuning/image.py')
-rw-r--r--utils/tuning/libtuning/image.py136
1 files changed, 136 insertions, 0 deletions
diff --git a/utils/tuning/libtuning/image.py b/utils/tuning/libtuning/image.py
new file mode 100644
index 00000000..aa9d20b5
--- /dev/null
+++ b/utils/tuning/libtuning/image.py
@@ -0,0 +1,136 @@
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (C) 2019, Raspberry Pi Ltd
+#
+# image.py - Container for an image and associated metadata
+
+import binascii
+import numpy as np
+from pathlib import Path
+import pyexiv2 as pyexif
+import rawpy as raw
+import re
+
+import libtuning as lt
+import libtuning.utils as utils
+
+
+class Image:
+ def __init__(self, path: Path):
+ self.path = path
+ self.lsc_only = False
+ self.color = -1
+ self.lux = -1
+
+ try:
+ self._load_metadata_exif()
+ except Exception as e:
+ utils.eprint(f'Failed to load metadata from {self.path}: {e}')
+ raise e
+
+ try:
+ self._read_image_dng()
+ except Exception as e:
+ utils.eprint(f'Failed to load image data from {self.path}: {e}')
+ raise e
+
+ @property
+ def name(self):
+ return self.path.name
+
+ # May raise KeyError as there are too many to check
+ def _load_metadata_exif(self):
+ # RawPy doesn't load all the image tags that we need, so we use py3exiv2
+ metadata = pyexif.ImageMetadata(str(self.path))
+ metadata.read()
+
+ # The DNG and TIFF/EP specifications use different IFDs to store the
+ # raw image data and the Exif tags. DNG stores them in a SubIFD and in
+ # an Exif IFD respectively (named "SubImage1" and "Photo" by pyexiv2),
+ # while TIFF/EP stores them both in IFD0 (name "Image"). Both are used
+ # in "DNG" files, with libcamera-apps following the DNG recommendation
+ # and applications based on picamera2 following TIFF/EP.
+ #
+ # This code detects which tags are being used, and therefore extracts the
+ # correct values.
+ try:
+ self.w = metadata['Exif.SubImage1.ImageWidth'].value
+ subimage = 'SubImage1'
+ photo = 'Photo'
+ except KeyError:
+ self.w = metadata['Exif.Image.ImageWidth'].value
+ subimage = 'Image'
+ photo = 'Image'
+ self.pad = 0
+ self.h = metadata[f'Exif.{subimage}.ImageLength'].value
+ white = metadata[f'Exif.{subimage}.WhiteLevel'].value
+ self.sigbits = int(white).bit_length()
+ self.fmt = (self.sigbits - 4) // 2
+ self.exposure = int(metadata[f'Exif.{photo}.ExposureTime'].value * 1000000)
+ self.againQ8 = metadata[f'Exif.{photo}.ISOSpeedRatings'].value * 256 / 100
+ self.againQ8_norm = self.againQ8 / 256
+ self.camName = metadata['Exif.Image.Model'].value
+ self.blacklevel = int(metadata[f'Exif.{subimage}.BlackLevel'].value[0])
+ self.blacklevel_16 = self.blacklevel << (16 - self.sigbits)
+
+ # Channel order depending on bayer pattern
+ # The key is the order given by exif, where 0 is R, 1 is G, and 2 is B
+ # The value is the index where the color can be found, where the first
+ # is R, then G, then G, then B.
+ bayer_case = {
+ '0 1 1 2': (lt.Color.R, lt.Color.GR, lt.Color.GB, lt.Color.B),
+ '1 2 0 1': (lt.Color.GB, lt.Color.R, lt.Color.B, lt.Color.GR),
+ '2 1 1 0': (lt.Color.B, lt.Color.GB, lt.Color.GR, lt.Color.R),
+ '1 0 2 1': (lt.Color.GR, lt.Color.R, lt.Color.B, lt.Color.GB)
+ }
+ # Note: This needs to be in IFD0
+ cfa_pattern = metadata[f'Exif.{subimage}.CFAPattern'].value
+ self.order = bayer_case[cfa_pattern]
+
+ def _read_image_dng(self):
+ raw_im = raw.imread(str(self.path))
+ raw_data = raw_im.raw_image
+ shift = 16 - self.sigbits
+ c0 = np.left_shift(raw_data[0::2, 0::2].astype(np.int64), shift)
+ c1 = np.left_shift(raw_data[0::2, 1::2].astype(np.int64), shift)
+ c2 = np.left_shift(raw_data[1::2, 0::2].astype(np.int64), shift)
+ c3 = np.left_shift(raw_data[1::2, 1::2].astype(np.int64), shift)
+ self.channels = [c0, c1, c2, c3]
+ # Reorder the channels into R, GR, GB, B
+ self.channels = [self.channels[i] for i in self.order]
+
+ # \todo Move this to macbeth.py
+ def get_patches(self, cen_coords, size=16):
+ saturated = False
+
+ # Obtain channel widths and heights
+ ch_w, ch_h = self.w, self.h
+ cen_coords = list(np.array((cen_coords[0])).astype(np.int32))
+ self.cen_coords = cen_coords
+
+ # Squares are ordered by stacking macbeth chart columns from left to
+ # right. Some useful patch indices:
+ # white = 3
+ # black = 23
+ # 'reds' = 9, 10
+ # 'blues' = 2, 5, 8, 20, 22
+ # 'greens' = 6, 12, 17
+ # greyscale = 3, 7, 11, 15, 19, 23
+ all_patches = []
+ for ch in self.channels:
+ ch_patches = []
+ for cen in cen_coords:
+ # Macbeth centre is placed at top left of central 2x2 patch to
+ # account for rounding. Patch pixels are sorted by pixel
+ # brightness so spatial information is lost.
+ patch = ch[cen[1] - 7:cen[1] + 9, cen[0] - 7:cen[0] + 9].flatten()
+ patch.sort()
+ if patch[-5] == (2**self.sigbits - 1) * 2**(16 - self.sigbits):
+ saturated = True
+ ch_patches.append(patch)
+
+ all_patches.append(ch_patches)
+
+ self.patches = all_patches
+
+ return not saturated