Merge pull request #1692 from Exiv2/hassec_canon_lens_test

New Canon Lens Identification + Automatic Test of all Lenses
This commit is contained in:
Christoph Hasse
2021-06-20 22:28:34 +02:00
committed by GitHub
12 changed files with 610 additions and 499 deletions
+340 -467
View File
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -2577,7 +2577,11 @@ namespace Exiv2 {
float fnumber(float apertureValue)
{
return std::exp(std::log(2.0F) * apertureValue / 2.F);
float result = std::exp(std::log(2.0F) * apertureValue / 2.F);
if (std::abs(result - 3.5) < 0.1) {
result = 3.5;
}
return result;
}
URational exposureTime(float shutterSpeedValue)
+10 -10
View File
@@ -1093,7 +1093,7 @@ File 6/16: 20030925_201850.jpg
20030925_201850.jpg Exif.CanonCs.0x0015 Short 1 32767
20030925_201850.jpg Exif.CanonCs.LensType Short 1 n/a
20030925_201850.jpg Exif.CanonCs.Lens Short 3 18.0 - 55.0 mm
20030925_201850.jpg Exif.CanonCs.MaxAperture Short 1 F3.6
20030925_201850.jpg Exif.CanonCs.MaxAperture Short 1 F3.5
20030925_201850.jpg Exif.CanonCs.MinAperture Short 1 F22
20030925_201850.jpg Exif.CanonCs.FlashActivity Short 1 Did not fire
20030925_201850.jpg Exif.CanonCs.FlashDetails Short 1
@@ -1906,7 +1906,7 @@ File 14/16: 20001004_015404.jpg
20001004_015404.jpg Exif.CanonCs.AFPoint Short 1 Auto-selected
20001004_015404.jpg Exif.CanonCs.ExposureProgram Short 1 Aperture priority (Av)
20001004_015404.jpg Exif.CanonCs.0x0015 Short 1 0
20001004_015404.jpg Exif.CanonCs.LensType Short 1 Canon EF 28-70mm f/2.8L USM
20001004_015404.jpg Exif.CanonCs.LensType Short 1 Canon EF 28-70mm f/2.8L USM *OR* Sigma 28-70mm f/2.8 EX
20001004_015404.jpg Exif.CanonCs.Lens Short 3 28.0 - 70.0 mm
20001004_015404.jpg Exif.CanonCs.MaxAperture Short 1 F2.8
20001004_015404.jpg Exif.CanonCs.MinAperture Short 1 F22
@@ -2734,7 +2734,7 @@ Compare image data and extracted data ------------------------------------
< 20030925_201850.jpg Exif.CanonCs.0x0015 Short 1 32767
< 20030925_201850.jpg Exif.CanonCs.LensType Short 1 n/a
< 20030925_201850.jpg Exif.CanonCs.Lens Short 3 18.0 - 55.0 mm
< 20030925_201850.jpg Exif.CanonCs.MaxAperture Short 1 F3.6
< 20030925_201850.jpg Exif.CanonCs.MaxAperture Short 1 F3.5
< 20030925_201850.jpg Exif.CanonCs.MinAperture Short 1 F22
< 20030925_201850.jpg Exif.CanonCs.FlashActivity Short 1 Did not fire
< 20030925_201850.jpg Exif.CanonCs.FlashDetails Short 1
@@ -3546,7 +3546,7 @@ Compare image data and extracted data ------------------------------------
< 20001004_015404.jpg Exif.CanonCs.AFPoint Short 1 Auto-selected
< 20001004_015404.jpg Exif.CanonCs.ExposureProgram Short 1 Aperture priority (Av)
< 20001004_015404.jpg Exif.CanonCs.0x0015 Short 1 0
< 20001004_015404.jpg Exif.CanonCs.LensType Short 1 Canon EF 28-70mm f/2.8L USM
< 20001004_015404.jpg Exif.CanonCs.LensType Short 1 Canon EF 28-70mm f/2.8L USM *OR* Sigma 28-70mm f/2.8 EX
< 20001004_015404.jpg Exif.CanonCs.Lens Short 3 28.0 - 70.0 mm
< 20001004_015404.jpg Exif.CanonCs.MaxAperture Short 1 F2.8
< 20001004_015404.jpg Exif.CanonCs.MinAperture Short 1 F22
@@ -4297,7 +4297,7 @@ Compare image data and extracted data ------------------------------------
> 20030925_201850.exv Exif.CanonCs.0x0015 Short 1 32767
> 20030925_201850.exv Exif.CanonCs.LensType Short 1 n/a
> 20030925_201850.exv Exif.CanonCs.Lens Short 3 18.0 - 55.0 mm
> 20030925_201850.exv Exif.CanonCs.MaxAperture Short 1 F3.6
> 20030925_201850.exv Exif.CanonCs.MaxAperture Short 1 F3.5
> 20030925_201850.exv Exif.CanonCs.MinAperture Short 1 F22
> 20030925_201850.exv Exif.CanonCs.FlashActivity Short 1 Did not fire
> 20030925_201850.exv Exif.CanonCs.FlashDetails Short 1
@@ -5109,7 +5109,7 @@ Compare image data and extracted data ------------------------------------
> 20001004_015404.exv Exif.CanonCs.AFPoint Short 1 Auto-selected
> 20001004_015404.exv Exif.CanonCs.ExposureProgram Short 1 Aperture priority (Av)
> 20001004_015404.exv Exif.CanonCs.0x0015 Short 1 0
> 20001004_015404.exv Exif.CanonCs.LensType Short 1 Canon EF 28-70mm f/2.8L USM
> 20001004_015404.exv Exif.CanonCs.LensType Short 1 Canon EF 28-70mm f/2.8L USM *OR* Sigma 28-70mm f/2.8 EX
> 20001004_015404.exv Exif.CanonCs.Lens Short 3 28.0 - 70.0 mm
> 20001004_015404.exv Exif.CanonCs.MaxAperture Short 1 F2.8
> 20001004_015404.exv Exif.CanonCs.MinAperture Short 1 F22
@@ -6098,7 +6098,7 @@ Compare original and inserted image data ---------------------------------
< 20030925_201850.jpg Exif.CanonCs.0x0015 Short 1 32767
< 20030925_201850.jpg Exif.CanonCs.LensType Short 1 n/a
< 20030925_201850.jpg Exif.CanonCs.Lens Short 3 18.0 - 55.0 mm
< 20030925_201850.jpg Exif.CanonCs.MaxAperture Short 1 F3.6
< 20030925_201850.jpg Exif.CanonCs.MaxAperture Short 1 F3.5
< 20030925_201850.jpg Exif.CanonCs.MinAperture Short 1 F22
< 20030925_201850.jpg Exif.CanonCs.FlashActivity Short 1 Did not fire
< 20030925_201850.jpg Exif.CanonCs.FlashDetails Short 1
@@ -6910,7 +6910,7 @@ Compare original and inserted image data ---------------------------------
< 20001004_015404.jpg Exif.CanonCs.AFPoint Short 1 Auto-selected
< 20001004_015404.jpg Exif.CanonCs.ExposureProgram Short 1 Aperture priority (Av)
< 20001004_015404.jpg Exif.CanonCs.0x0015 Short 1 0
< 20001004_015404.jpg Exif.CanonCs.LensType Short 1 Canon EF 28-70mm f/2.8L USM
< 20001004_015404.jpg Exif.CanonCs.LensType Short 1 Canon EF 28-70mm f/2.8L USM *OR* Sigma 28-70mm f/2.8 EX
< 20001004_015404.jpg Exif.CanonCs.Lens Short 3 28.0 - 70.0 mm
< 20001004_015404.jpg Exif.CanonCs.MaxAperture Short 1 F2.8
< 20001004_015404.jpg Exif.CanonCs.MinAperture Short 1 F22
@@ -7661,7 +7661,7 @@ Compare original and inserted image data ---------------------------------
> 20030925_201850.exv Exif.CanonCs.0x0015 Short 1 32767
> 20030925_201850.exv Exif.CanonCs.LensType Short 1 n/a
> 20030925_201850.exv Exif.CanonCs.Lens Short 3 18.0 - 55.0 mm
> 20030925_201850.exv Exif.CanonCs.MaxAperture Short 1 F3.6
> 20030925_201850.exv Exif.CanonCs.MaxAperture Short 1 F3.5
> 20030925_201850.exv Exif.CanonCs.MinAperture Short 1 F22
> 20030925_201850.exv Exif.CanonCs.FlashActivity Short 1 Did not fire
> 20030925_201850.exv Exif.CanonCs.FlashDetails Short 1
@@ -8473,7 +8473,7 @@ Compare original and inserted image data ---------------------------------
> 20001004_015404.exv Exif.CanonCs.AFPoint Short 1 Auto-selected
> 20001004_015404.exv Exif.CanonCs.ExposureProgram Short 1 Aperture priority (Av)
> 20001004_015404.exv Exif.CanonCs.0x0015 Short 1 0
> 20001004_015404.exv Exif.CanonCs.LensType Short 1 Canon EF 28-70mm f/2.8L USM
> 20001004_015404.exv Exif.CanonCs.LensType Short 1 Canon EF 28-70mm f/2.8L USM *OR* Sigma 28-70mm f/2.8 EX
> 20001004_015404.exv Exif.CanonCs.Lens Short 3 28.0 - 70.0 mm
> 20001004_015404.exv Exif.CanonCs.MaxAperture Short 1 F2.8
> 20001004_015404.exv Exif.CanonCs.MinAperture Short 1 F22
Binary file not shown.
+1 -1
View File
@@ -9,7 +9,7 @@ class Sigma24_105mmRecognization(metaclass=system_tests.CaseMeta):
filename = "$data_path/exiv2-g45.exv"
commands = ["$exiv2 -pa --grep lens/i " + filename]
stdout = ["""Exif.CanonCs.LensType Short 1 Sigma 24-105mm F4 DG OS HSM | A
stdout = ["""Exif.CanonCs.LensType Short 1 Sigma 24-105mm f/4 DG OS HSM | A
Exif.CanonCs.Lens Short 3 24.0 - 105.0 mm
Exif.CanonCf.LensAFStopButton Short 1 0
Exif.Canon.LensModel Ascii 74 24-105mm F4 DG OS HSM | Art 013
+1 -1
View File
@@ -10,7 +10,7 @@ class CheckTokina11_20mm(metaclass=system_tests.CaseMeta):
commands = [ "$exiv2 -pa --grep lens/i $filename" ]
stdout = [ """Exif.CanonCs.LensType Short 1 Tokina AT-X 11-20 F2.8 PRO DX Aspherical 11-20mm f/2.8
stdout = [ """Exif.CanonCs.LensType Short 1 Tokina AT-X 11-20 f/2.8 PRO DX Aspherical 11-20mm f/2.8
Exif.CanonCs.Lens Short 3 11.0 - 20.0 mm
Exif.Canon.LensModel Ascii 74 11-20mm
Exif.Photo.LensSpecification Rational 4 11/1 20/1 0/1 0/1
+1 -1
View File
@@ -10,7 +10,7 @@ class CheckSigma35mm(metaclass=system_tests.CaseMeta):
commands = [ "$exiv2 -pa --grep lens/i $filename" ]
stdout = [ """Exif.CanonCs.LensType Short 1 Sigma 35mm f/1.4 DG HSM
stdout = [ """Exif.CanonCs.LensType Short 1 Sigma 35mm f/1.4 DG HSM *OR* Sigma 35mm f/1.5 FF High-Speed Prime | 017
Exif.CanonCs.Lens Short 3 35.0 mm
Exif.Canon.LensModel Ascii 74 35mm
Exif.Photo.LensSpecification Rational 4 35/1 35/1 0/1 0/1
+1 -1
View File
@@ -16,7 +16,7 @@ class CanonLenses(metaclass=system_tests.CaseMeta):
"$exiv2 -pa --grep lens/i " + filenames[1],
]
stdout = ["""Exif.CanonCs.LensType Short 1 Sigma APO 120-300mm f/2.8 EX DG OS HSM
stdout = ["""Exif.CanonCs.LensType Short 1 Sigma APO 120-300mm f/2.8 EX DG OS HSM *OR* Sigma 120-300mm f/2.8 DG OS HSM S013
Exif.CanonCs.Lens Short 3 120.0 - 300.0 mm
Exif.Canon.LensModel Ascii 74 120-300mm
Exif.Photo.LensSpecification Rational 4 120/1 300/1 0/1 0/1
View File
+46
View File
@@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
import re
import os
import system_tests
from lens_tests.utils import extract_lenses_from_cpp, make_test_cases, aperture_to_raw_exif
# NOTE
# Normally the canon maker note holds the max aperture of the lens at the focal length
# the picture was taken at. Thus for a f/4-6.3 lens, this value could be anywhere in that range.
# For the below tests we only test the scenario where the lens was used at it's shortest focal length.
# Thus we always pick the 'aperture_max_short' of a lens as the value to write into the
# Exif.CanonCs.MaxAperture field.
# get directory of the current file
file_dir = os.path.dirname(os.path.realpath(__file__))
# to get the canon maker note cpp file that contains list of all supported lenses
canon_lens_file = os.path.abspath(os.path.join(file_dir, "./../../src/canonmn_int.cpp"))
# tell the below function what the start of the lens array looks like
startpattern = "constexpr TagDetails canonCsLensType[] = {"
# use utils function to extract all lenses
lenses = extract_lenses_from_cpp(canon_lens_file, startpattern)
# use utils function to define test case data
test_cases = make_test_cases(lenses)
for lens_tc in test_cases:
testname = lens_tc["id"] + "_" + lens_tc["desc"]
globals()[testname] = system_tests.CaseMeta(
"canon_lenses." + testname,
tuple(),
{
"filename": "$data_path/template.exv",
"commands": [
'$exiv2 -M"set Exif.CanonCs.LensType $lens_id" -M"set Exif.CanonCs.Lens $focal_length_max $focal_length_min 1" -M"set Exif.CanonCs.MaxAperture $aperture_max" $filename && $exiv2 -pa -K Exif.CanonCs.LensType $filename'
],
"stderr": [""],
"stdout": ["Exif.CanonCs.LensType Short 1 $lens_description\n"],
"retval": [0],
"lens_id": lens_tc["id"],
"lens_description": lens_tc["target"],
"aperture_max": aperture_to_raw_exif(lens_tc["aperture_max_short"] * lens_tc["tc"]),
"focal_length_min": int(lens_tc["focal_length_min"] * lens_tc["tc"]),
"focal_length_max": int(lens_tc["focal_length_max"] * lens_tc["tc"]),
},
)
+203
View File
@@ -0,0 +1,203 @@
import re
import os
import logging
import math
from itertools import groupby
log = logging.getLogger(__name__)
LENS_ENTRY_DEFAULT_RE = re.compile('^\{\s*(?P<lens_id>[0-9]+),\s*"(?P<lens_description>.*)"')
LENS_META_DEFAULT_RE = re.compile(
(
# anything at the start
".*?"
# maybe min focal length and hyphen, surely max focal length e.g.: 24-70mm
"(?:(?P<focal_length_min>[0-9]+)-)?(?P<focal_length_max>[0-9]+)mm"
# anything in-between
".*?"
# maybe short focal length max aperture and hyphen, surely at least single max aperture e.g.: f/4.5-5.6
# short and tele indicate apertures at the short (focal_length_min) and tele (focal_length_max) position of the lens
"(?:(?:f\/)|T)(?:(?P<aperture_max_short>[0-9]+(?:\.[0-9]+)?)-)?(?P<aperture_max_tele>[0-9]+(?:\.[0-9])?)"
# check if there is a teleconverter pattern e.g. + 1.4x
"(?:.*?\+.*?(?P<tc>[0-9.]+)x)?"
)
)
def aperture_to_raw_exif(aperture):
# see https://github.com/exiftool/exiftool/blob/master/lib/Image/ExifTool/Canon.pm#L9678
"""Transform aperture value to Canon maker note style hex format."""
# for apertures < 1 the below is negative
num = math.log(aperture) * 2 / math.log(2)
# temporarily make the number positive
if num < 0:
num = -num
sign = -1
else:
sign = 1
val = int(num)
frac = num - val
if abs(frac - 0.33) < 0.05:
frac = 0x0C
elif abs(frac - 0.67) < 0.05:
frac = 0x14
else:
frac = int(frac * 0x20 + 0.5)
return sign * (val * 0x20 + frac)
def raw_exif_to_aperture(raw):
"""The inverse operation of aperture_to_raw_exif"""
val = raw
if val < 0:
val = -val
sign = -1
else:
sign = 1
frac = val & 0x1F
val -= frac
# Convert 1/3 and 2/3 codes
if frac == 0x0C:
frac = 0x20 / 3
elif frac == 0x14:
frac = 0x40 / 3
ev = sign * (val + frac) / 0x20
return math.exp(ev * math.log(2) / 2)
def parse_lens_entry(text, pattern=LENS_ENTRY_DEFAULT_RE):
"""
get the ID, and description from a lens entry field
Expected input format:
{ 748, "Canon EF 100-400mm f/4.5-5.6L IS II USM + 1.4x" }
We return a dict of:
lens_id = 748
lens_description = "Canon EF 100-400mm f/4.5-5.6L IS II USM + 1.4x"
"""
result = pattern.match(text)
return result.groups() if result else None
def extract_meta(text, pattern=LENS_META_DEFAULT_RE):
"""
Extract metadata from lens description.
Input expected in the form of e.g. "Canon EF 100-400mm f/4.5-5.6L IS II USM + 1.4x"
We return a dict of:
focal_length_min = 100
focal_length_max = 400
aperture_max_short = 4.5
aperture_max_tele = 5.6
tc = 1.4
"""
result = pattern.match(text)
if not result:
# didn't match
return None
ret = result.groupdict()
# set min to max value if we didn't get a range but a single value
ret["focal_length_min"] = int(ret["focal_length_min"] or ret["focal_length_max"])
ret["focal_length_max"] = int(ret["focal_length_max"])
ret["aperture_max_short"] = float(ret["aperture_max_short"] or ret["aperture_max_tele"])
ret["aperture_max_tele"] = float(ret["aperture_max_tele"])
ret["tc"] = float(ret["tc"] or 1)
return ret
def lens_is_match(l1, l2):
"""
Test if lens l2 is compatible with lens l1
This assumes we write l1's metadata and pick its 'aperture_max_short' value
as the maximum aperture value to write into exif.
Normally the canon maker note holds the max aperture of the lens at the focal length
the picture was taken at. Thus for a f/4-6.3 lens, this value could be anywhere in that range.
"""
# the problem is that the round trip transformation isn't exact
# so we need to account for this here as well to not define a target
# which isn't achievable for exiv2
reconstructed_aperture = raw_exif_to_aperture(aperture_to_raw_exif(l1["aperture_max_short"] * l1["tc"]))
return all(
[
l1["focal_length_min"] * l1["tc"] == l2["focal_length_min"] * l2["tc"],
l1["focal_length_max"] * l1["tc"] == l2["focal_length_max"] * l2["tc"],
(l2["aperture_max_short"] * l2["tc"]) - 0.1
<= reconstructed_aperture
<= (l2["aperture_max_tele"] * l2["tc"]) + 0.1,
]
)
def make_test_cases(lenses):
"""
Creates a test case for each lens
Main job of this function is to collect all ambiguous lenses and define a test target
as the " *OR* " joined string of all ambiguous lens descriptions
"""
test_cases = []
for lens_id, group in groupby(lenses, lambda x: x["id"]):
lens_group = list(group)
test_cases += [
{
**lens["meta"],
"id": lens["id"],
"desc": lens["desc"],
"target": " *OR* ".join([l["desc"] for l in lens_group if lens_is_match(lens["meta"], l["meta"])]),
}
for lens in lens_group
]
return test_cases
def extract_lenses_from_cpp(filename, start_pattern):
"""
Extract lens information from the lens descriptions array in a maker note cpp file
filename: path to cpp file
start_pattern: start_pattern == line.strip() should return True for
the starting line of the array containing the lenses.
returns: a list of lens entries containing a tuple of the form:
(lens ID, lens description, metadata dictionary)
for content of metadata see extract_meta() function.
"""
lenses = []
with open(filename, "r") as f:
in_lens_array = False
for line in f.readlines():
stripped = line.strip()
if stripped == start_pattern:
in_lens_array = True
continue
if stripped == "};":
in_lens_array = False
continue
if in_lens_array:
lens_entry = parse_lens_entry(stripped)
if not lens_entry:
log.error(f"Failure parsing lens entry: {stripped}.")
continue
if lens_entry[1] == "n/a":
continue
meta = extract_meta(lens_entry[1])
if not meta:
log.error(f"Failure extracting metadata from lens description: {lens_entry[0]}: {lens_entry[1]}.")
continue
lenses.append({"id": lens_entry[0], "desc": lens_entry[1], "meta": meta})
return lenses
+2 -17
View File
@@ -5,33 +5,18 @@ import os
import inspect
import subprocess
import threading
import shlex
import sys
import shutil
import string
import unittest
from bash_tests import utils as BT
if sys.platform in [ 'win32', 'msys', 'cygwin' ]:
#: invoke subprocess.Popen with shell=True on Windows
_SUBPROCESS_SHELL = True
def _cmd_splitter(cmd):
return cmd
def _process_output_post(output):
return output.replace('\r\n', '\n')
else:
#: invoke subprocess.Popen with shell=False on Unix
_SUBPROCESS_SHELL = False
def _cmd_splitter(cmd):
return shlex.split(cmd)
def _process_output_post(output):
return output
@@ -586,13 +571,13 @@ def test_run(self):
)
proc = subprocess.Popen(
_cmd_splitter(command),
command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
stdin=subprocess.PIPE if stdin is not None else None,
env=self._get_env(),
cwd=self.work_dir,
shell=_SUBPROCESS_SHELL
shell=True,
)
# Setup a threading.Timer which will terminate the command if it takes