|
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)
|