Blame tools/test_autoscale/test_autoscale.py

Packit Service 48484a
#!/usr/bin/env python3
Packit Service 48484a
# -*- coding: utf-8 -*-
Packit Service 48484a
Packit Service 48484a
"""This program checks the accuracy of Lensfun's autoscaling algorithm.
Packit Service 48484a
Lensfun calculates the autoscaling by testing all four corner points and the
Packit Service 48484a
four edge centres.  For each of these 8 points, it calculates the scaling
Packit Service 48484a
necessary, and then takes the maximal value.
Packit Service 48484a
Packit Service 48484a
It calculates the scaling necessary by determining the distance of the point of
Packit Service 48484a
the corrected image edge which lies in the same direction as the corner point.
Packit Service 48484a
(Because these two points should lie in top of each other after the
Packit Service 48484a
autoscaling.)  Then, it divides the distance of the corner point by the
Packit Service 48484a
distance of the corrected edge point.  “Distance” means “distance from origin”.
Packit Service 48484a
Packit Service 48484a
The choice of those 8 points is the critical compromise here.  In most cases,
Packit Service 48484a
the maximal scaling is indeed necessary for one of these points.  For example,
Packit Service 48484a
a corrected pincushion distortion pulls the corners most closely to the centre,
Packit Service 48484a
so they need the maximal scaling.  On the other hand, a corrected barrel
Packit Service 48484a
distorion leaves the edge centres most closely to the origin, so their position
Packit Service 48484a
is critical.  In case of a nasty moustache distortion, the maximal scaling may
Packit Service 48484a
be inbetween, and in these cases, Lensfun's approach fails.
Packit Service 48484a
Packit Service 48484a
This program shows for which lenses it fails, and how badly.  For
Packit Service 48484a
non-rectilinear lenses, the projection is changed to rectilinear first.
Packit Service 48484a
Currenly (as of April 2015), there is nothing to worry about.  Only three
Packit Service 48484a
lenses fail, and all of them exhibit only very slim black areas after the
Packit Service 48484a
correction.  If one leaves the original projection, two fisheyes join the
Packit Service 48484a
pack.
Packit Service 48484a
Packit Service 48484a
The simple reason why the current approach works well is that the used
Packit Service 48484a
distortion polynomials usually have monotonic behaviour in the important
Packit Service 48484a
region, so the maximal values are at the borders of the region.  This is even
Packit Service 48484a
more true for projection changes.
Packit Service 48484a
Packit Service 48484a
For problematic lenses, this program prints the number of sampling points on
Packit Service 48484a
the longer edges necessary to get a decent autoscaling (< 1‰).  Lensfun itself
Packit Service 48484a
only uses 2.  If the problem of black areas increases, one can increase the
Packit Service 48484a
number of edge sampling points to the maximal value this program prints.
Packit Service 48484a
Packit Service 48484a
The only safe approach is to test *all* edge pixels.  Really.  This is most
Packit Service 48484a
wasteful in almost all cases, so Lensfun doesn't do it (for now).  On the other
Packit Service 48484a
hand, its complexity is O(size).  All other operations of Lensfun are O(size²).
Packit Service 48484a
"""
Packit Service 48484a
Packit Service 48484a
import math, glob, multiprocessing, argparse, os.path
Packit Service 48484a
from xml.etree import ElementTree
Packit Service 48484a
from scipy.optimize import brentq
Packit Service 48484a
Packit Service 48484a
Packit Service 48484a
parser = argparse.ArgumentParser(description="Check autoscaling algorithm.")
Packit Service 48484a
parser.add_argument("database_path", metavar="database_path", help="The full path to the Lensfun database.")
Packit Service 48484a
parser.add_argument("--original-geometry", dest="original_geometry", action="store_true",
Packit Service 48484a
                    help="Use original geometry of fisheyes rather than transforming to rectilinear.")
Packit Service 48484a
args = parser.parse_args()
Packit Service 48484a
Packit Service 48484a
Packit Service 48484a
class Calibration:
Packit Service 48484a
Packit Service 48484a
    def __init__(self, f_per_half_height, a=None, b=None, c=None, k1=None, type_="rectilinear", aspect_ratio=1.5):
Packit Service 48484a
        self.f_per_half_height, self.a, self.b, self.c, self.k1, self.type_, self.aspect_ratio = \
Packit Service 48484a
                f_per_half_height, a, b, c, k1, type_, aspect_ratio
Packit Service 48484a
Packit Service 48484a
    def de_ptlens(self, r_):
Packit Service 48484a
        try:
Packit Service 48484a
            return brentq(lambda r: r * (self.a * r**3 + self.b * r**2 + self.c * r + 1 - self.a - self.b - self.c) - r_, 0, 2.1)
Packit Service 48484a
        except ValueError:
Packit Service 48484a
            return float("inf")
Packit Service 48484a
Packit Service 48484a
    def de_poly3(self, r_):
Packit Service 48484a
        try:
Packit Service 48484a
            return brentq(lambda r: r * (self.k1 * r**2 + 1 - self.k1) - r_, 0, 2)
Packit Service 48484a
        except ValueError:
Packit Service 48484a
            return float("inf")
Packit Service 48484a
Packit Service 48484a
    def de_fisheye(self, r):
Packit Service 48484a
        angle = r / self.f_per_half_height
Packit Service 48484a
        if angle < math.pi / 2:
Packit Service 48484a
            return self.f_per_half_height * math.tan(angle)
Packit Service 48484a
        else:
Packit Service 48484a
            return float("inf")
Packit Service 48484a
Packit Service 48484a
    def de_equisolidangle(self, r):
Packit Service 48484a
        try:
Packit Service 48484a
            return self.f_per_half_height * math.tan(2 * math.asin(r / self.f_per_half_height / 2))
Packit Service 48484a
        except ValueError:
Packit Service 48484a
            return float("inf")
Packit Service 48484a
Packit Service 48484a
    def de_stereographic(self, r):
Packit Service 48484a
        angle = 2 * math.atan(r / self.f_per_half_height / 2)
Packit Service 48484a
        if angle < math.pi / 2:
Packit Service 48484a
            return self.f_per_half_height * math.tan(angle)
Packit Service 48484a
        else:
Packit Service 48484a
            return float("inf")
Packit Service 48484a
Packit Service 48484a
    def get_scaling(self, x, y):
Packit Service 48484a
        r_ = math.hypot(x, y)
Packit Service 48484a
        r = self.de_poly3(r_) if self.a is None else self.de_ptlens(r_)
Packit Service 48484a
        if self.type_ == "rectilinear" or args.original_geometry:
Packit Service 48484a
            pass
Packit Service 48484a
        elif self.type_ == "fisheye":
Packit Service 48484a
            r = self.de_fisheye(r)
Packit Service 48484a
        elif self.type_ == "equisolid":
Packit Service 48484a
            r = self.de_equisolidangle(r)
Packit Service 48484a
        elif self.type_ == "stereographic":
Packit Service 48484a
            r = self.de_stereographic(r)
Packit Service 48484a
        return r_ / r
Packit Service 48484a
Packit Service 48484a
    def get_autoscale(self, samples):
Packit Service 48484a
        scalings = []
Packit Service 48484a
        for i in range(samples):
Packit Service 48484a
            x = self.aspect_ratio * (-1 + i / samples * 2)
Packit Service 48484a
            scalings.append(self.get_scaling(x, +1))
Packit Service 48484a
        samples = int(math.ceil(samples / self.aspect_ratio / 2)) * 2
Packit Service 48484a
        for i in range(samples):
Packit Service 48484a
            y = -1 + i / samples * 2
Packit Service 48484a
            scalings.append(self.get_scaling(+ self.aspect_ratio, y))
Packit Service 48484a
        return max(scalings) or 1
Packit Service 48484a
Packit Service 48484a
    def get_perfect_sample_number(self):
Packit Service 48484a
        perfect_autoscale = self.get_autoscale(100)
Packit Service 48484a
        samples = 2
Packit Service 48484a
        scalings = []
Packit Service 48484a
        while True:
Packit Service 48484a
            scalings.append(self.get_autoscale(samples))
Packit Service 48484a
            if all(abs(scaling - perfect_autoscale) / perfect_autoscale < 1e-3 for scaling in scalings[-3:]):
Packit Service 48484a
                return samples
Packit Service 48484a
            samples += 2
Packit Service 48484a
Packit Service 48484a
Packit Service 48484a
def process_calibration(distortion, lens, crop_factor, type_, aspect_ratio):
Packit Service 48484a
    f_per_half_height = float(distortion.attrib.get("focal")) / (12 / crop_factor)
Packit Service 48484a
    model = distortion.attrib.get("model")
Packit Service 48484a
    if model == "ptlens":
Packit Service 48484a
        calibration = Calibration(f_per_half_height,
Packit Service 48484a
                                  a=float(distortion.attrib.get("a", 0)),
Packit Service 48484a
                                  b=float(distortion.attrib.get("b", 0)),
Packit Service 48484a
                                  c=float(distortion.attrib.get("c", 0)), type_=type_, aspect_ratio=aspect_ratio)
Packit Service 48484a
    elif model == "poly3":
Packit Service 48484a
        calibration = Calibration(f_per_half_height, k1=float(distortion.attrib.get("k1", 0)),
Packit Service 48484a
                                  type_=type_, aspect_ratio=aspect_ratio)
Packit Service 48484a
    else:
Packit Service 48484a
        return None, None, None
Packit Service 48484a
    return lens.find("model").text, distortion.attrib.get("focal"), calibration.get_perfect_sample_number()
Packit Service 48484a
Packit Service 48484a
results = set()
Packit Service 48484a
pool = multiprocessing.Pool()
Packit Service 48484a
for filename in glob.glob(os.path.join(args.database_path, "*.xml")):
Packit Service 48484a
    tree = ElementTree.parse(filename)
Packit Service 48484a
    for lens in tree.findall("lens"):
Packit Service 48484a
        type_element = lens.find("type")
Packit Service 48484a
        type_ = "rectilinear" if type_element is None else type_element.text.strip()
Packit Service 48484a
        crop_factor = float(lens.find("cropfactor").text)
Packit Service 48484a
        aspect_ratio_element = lens.find("aspect-ratio")
Packit Service 48484a
        if aspect_ratio_element is None:
Packit Service 48484a
            aspect_ratio = 1.5
Packit Service 48484a
        else:
Packit Service 48484a
            numerator, __, denominator = aspect_ratio_element.text.partition(":")
Packit Service 48484a
            if denominator:
Packit Service 48484a
                aspect_ratio = float(numerator) / float(denominator)
Packit Service 48484a
            else:
Packit Service 48484a
                aspect_ratio = float(numerator)
Packit Service 48484a
        calibration_element = lens.find("calibration")
Packit Service 48484a
        if calibration_element is not None:
Packit Service 48484a
            for distortion in calibration_element.findall("distortion"):
Packit Service 48484a
                results.add(pool.apply_async(process_calibration, (distortion, lens, crop_factor, type_, aspect_ratio)))
Packit Service 48484a
pool.close()
Packit Service 48484a
pool.join()
Packit Service 48484a
Packit Service 48484a
problematic_lenses = {}
Packit Service 48484a
for result in results:
Packit Service 48484a
    model, focal_length, perfect_sample_number = result.get()
Packit Service 48484a
    if perfect_sample_number and perfect_sample_number > 2:
Packit Service 48484a
        problematic_lenses.setdefault(perfect_sample_number, set()).add((model, focal_length))
Packit Service 48484a
if problematic_lenses:
Packit Service 48484a
    print("Number of equidistant samples on the long edge:\n")
Packit Service 48484a
    for perfect_sample_number, lenses in problematic_lenses.items():
Packit Service 48484a
        print(",\n".join("{} at {}mm".format(*lens) for lens in lenses), ": ", perfect_sample_number)