Skip to content

Commit 6981354

Browse files
committed
Database-driven lens corrections with bundled lensfun calibration data
1 parent cce43c6 commit 6981354

28 files changed

Lines changed: 2810 additions & 314 deletions

.gitignore

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,53 @@
1010
# Where third-party dependencies like ExDoc output generated docs.
1111
/doc/
1212

13+
# Ignore .fetch files in case you like to edit your project deps locally.
14+
/.fetch
15+
1316
# If the VM crashes, it generates a dump, let's ignore it too.
1417
erl_crash.dump
1518

1619
# Also ignore archive artifacts (built via "mix archive.build").
1720
*.ez
1821

1922
# Ignore package tarball (built via "mix hex.build").
20-
image_lens_correction-*.tar
23+
image-*.tar
2124

2225
# Temporary files, for example, from tests.
2326
/tmp/
27+
28+
# Validation error images
29+
/test/support/images/did_not_match
30+
31+
# Mac metadata
32+
.DS_Store
33+
34+
# Minio
35+
.minio.sys/
36+
37+
# Performance result images
38+
test/perf/*.png
39+
test/perf/*.jpeg
40+
41+
# Local .iex.exs
42+
.iex.exs
43+
44+
# ML model
45+
*.pt
46+
*.onnx
47+
48+
# ASDF
49+
.tool-versions
50+
51+
# Mise
52+
mise.toml
53+
54+
# Don't add the lensfun DTD to the repo
55+
lensfun-database.dtd
56+
57+
# Failed tests
58+
test/support/did_not_match/
59+
60+
# Claude
61+
CLAUDE.md
62+
.claude

CHANGELOG.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Changelog
2+
3+
## ImageLensCorrection 0.1.0
4+
5+
This is the initial release of `ImageLensCorrection`, a lens-correction companion package for [`:image`](https://hex.pm/packages/image).
6+
7+
### Features
8+
9+
* **Radial distortion correction** for the lensfun `:ptlens`, `:poly3` and `:poly5` models. Each model is inverted per pixel via six iterations of Newton's method evaluated as `libvips` arithmetic on the radius image — no per-pixel branching, no NIF callouts. Standalone entry points `Image.LensCorrection.radial_distortion_correction/4`, `Image.LensCorrection.poly3_correction/2` and `Image.LensCorrection.poly5_correction/3`, plus `Image.LensCorrection.apply_distortion/2` for the lensfun-shaped `%{model:, terms:}` map.
10+
11+
* **Vignetting correction** via the lensfun `:pa` model, inverted analytically. `Image.LensCorrection.vignette_correction/4` takes raw `k1, k2, k3`; `Image.LensCorrection.apply_vignetting/2` accepts the interpolated map.
12+
13+
* **Lateral chromatic aberration (TCA) correction** via the lensfun `:linear` and `:poly3` models. `Image.LensCorrection.Tca` decomposes the input into bands, remaps the red and blue channels per channel, leaves green as the reference, and rejoins. RGBA inputs preserve their alpha channel.
14+
15+
* **Geometric projection** between `:rectilinear`, `:fisheye` and `:equirectangular` projections, plus a `Image.LensCorrection.Geometry.focal_length_in_pixels/3` helper that derives the pixel focal length from the focal length in millimetres, the camera's crop factor and the image's diagonal.
16+
17+
* **EXIF-driven correction pipeline**`Image.LensFun.Correct.correct/2` reads the camera and lens identification, focal length, aperture and (where available) focus distance from the input image's EXIF metadata, looks the lens up in the bundled lensfun database, interpolates the calibration to the image's focal length and aperture, rescales coefficients to the camera's crop factor and applies all enabled corrections in one pipeline. Every parameter is overridable via options for images that lack EXIF metadata.
18+
19+
* **Bundled lensfun database**`priv/lensfun/lensfun.etf` (~5 MB) covers 1013 camera bodies and 1459 lens calibration sets across the lensfun XML database. Decoded once into a `:persistent_term` cache on first use. Rebuildable from a lensfun checkout via `Image.LensFun.Importer.import/1`.
20+
21+
* **Database lookup primitives**`Image.LensFun.find_lens/3` (case-insensitive maker matching, fuzzy substring fallback for the model, optional crop-factor narrowing), `Image.LensFun.find_camera/2`, `Image.LensFun.interpolate_distortion/2` (Catmull-Rom Hermite spline matching lensfun's `lfLens::InterpolateDistortion`), `Image.LensFun.interpolate_vignetting/4` (inverse-distance weighting over focal length, aperture and focus distance with `p = 3.5`, matching lensfun's `lfLens::InterpolateVignetting`), `Image.LensFun.interpolate_tca/2`, and `Image.LensFun.metrics_from_exif_and_options/2` for unified EXIF + options metric resolution.
22+
23+
* **Coefficient rescaling**`Image.LensCorrection.rescale_coefficients/4` implements lensfun's `rescale_polynomial_coefficients` so calibration coefficients can be applied to images captured on a different sensor than the one the lens was calibrated against.
24+
25+
* **Database lens search**`Image.LensFun.search_lenses/2` performs free-form, word-by-word, case-insensitive lookup across the bundled database with `:maker`, `:limit` and `:has` (capability) filters.
26+
27+
### Notes
28+
29+
* The Adobe Camera Model (`acm`) variants of the lensfun distortion, vignetting and TCA models are not supported. As of the current lensfun release no calibration record in the upstream XML database uses `acm`, and upstream lensfun itself does not implement reverse ACM correction. `apply_distortion/2`, `apply_vignetting/2` and `Tca.apply_tca/2` return a clear `{:error, {:unsupported_*_model, ...}}` tuple if given an ACM map.

LICENSE.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
Apache License
2+
Version 2.0, January 2004
3+
http://www.apache.org/licenses/
4+
5+
Copyright 2026 Kip Cole
6+
7+
Licensed under the Apache License, Version 2.0 (the "License");
8+
you may not use this file except in compliance with the License.
9+
You may obtain a copy of the License at
10+
11+
http://www.apache.org/licenses/LICENSE-2.0
12+
13+
Unless required by applicable law or agreed to in writing, software
14+
distributed under the License is distributed on an "AS IS" BASIS,
15+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
See the License for the specific language governing permissions and
17+
limitations under the License.
18+
19+
## Bundled lensfun calibration data
20+
21+
The Erlang term file `priv/lensfun/lensfun.etf` is generated from the
22+
[lensfun](https://github.com/lensfun/lensfun) project's XML calibration
23+
database. That database is distributed under the
24+
[Creative Commons Attribution-ShareAlike 3.0 Unported License](https://creativecommons.org/licenses/by-sa/3.0/).
25+
26+
The lensfun reference C library is licensed under LGPL v3. This
27+
package does not link to or distribute that library; it re-implements
28+
the calibration math in Elixir.

README.md

Lines changed: 112 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,30 @@
11
# ImageLensCorrection
22

3-
**TODO: Add description**
3+
`ImageLensCorrection` provides corrections for the four most common camera-lens defects — radial (barrel/pincushion) distortion, vignetting, lateral chromatic aberration (TCA), and geometric projection — for images created or processed with the [`:image`](https://hex.pm/packages/image) library. Calibration coefficients come from the [`lensfun`](https://github.com/lensfun/lensfun) project's community-maintained database, which is bundled with the package as a compact ~5 MB Erlang term file covering 1000+ camera bodies and 1400+ lenses.
4+
5+
Corrections are evaluated as `libvips` arithmetic expressions on top of `Vix`, so every correction stays inside the Vix pipeline; no external C library is required at runtime.
6+
7+
Documentation can be found at <https://hexdocs.pm/image_lens_correction>.
8+
9+
## Features
10+
11+
* **Radial distortion correction** — implements the lensfun `:ptlens`, `:poly3` and `:poly5` models, all inverted per pixel via Newton's method evaluated as `libvips` arithmetic.
12+
13+
* **Vignetting correction** — implements the lensfun `:pa` model `Cd = Cs * (1 + k1*r² + k2*r⁴ + k3*r⁶)`, inverted analytically.
14+
15+
* **Lateral chromatic aberration (TCA)** — implements the lensfun `:linear` and `:poly3` TCA models. The image is decomposed into bands, red and blue are remapped per channel via `mapim`, green is taken as the reference, and RGBA inputs preserve their alpha channel.
16+
17+
* **Geometric projection** — converts between `:rectilinear`, `:fisheye` and `:equirectangular` projections.
18+
19+
* **EXIF-driven, one-call correction**`Image.LensFun.Correct.correct/2` reads camera and lens metadata from the image's EXIF tags, looks the lens up in the bundled database, interpolates the calibration to the image's focal length and aperture, rescales coefficients to the camera's crop factor, and applies the requested corrections in one pipeline. Every EXIF tag is overridable via options for images that lack metadata.
20+
21+
* **Database lookup primitives**`Image.LensFun.find_lens/3`, `Image.LensFun.find_camera/2`, `Image.LensFun.interpolate_distortion/2`, `Image.LensFun.interpolate_vignetting/4` and `Image.LensFun.interpolate_tca/2` for direct programmatic access to the bundled calibration data, with case-insensitive maker matching, fuzzy-substring lens-model matching and crop-factor-narrowing.
22+
23+
* **Bundled calibration database**`priv/lensfun/lensfun.etf` is generated from the lensfun XML database at build time and decoded once into a `:persistent_term` cache on first use.
424

525
## Installation
626

7-
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
8-
by adding `image_lens_correction` to your list of dependencies in `mix.exs`:
27+
The package can be installed by adding `image_lens_correction` to your list of dependencies in `mix.exs`:
928

1029
```elixir
1130
def deps do
@@ -15,7 +34,94 @@ def deps do
1534
end
1635
```
1736

18-
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
19-
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
20-
be found at <https://hexdocs.pm/image_lens_correction>.
37+
## Usage
38+
39+
### One-shot correction from EXIF
40+
41+
For an image whose EXIF metadata identifies the camera and lens, all available corrections (distortion, vignetting and TCA) can be applied with a single call:
42+
43+
```elixir
44+
{:ok, image} = Image.open("photo.jpg")
45+
{:ok, corrected} = Image.LensFun.Correct.correct(image)
46+
```
47+
48+
For an image without complete EXIF metadata, supply the missing parameters as options. Every parameter that `correct/2` reads from EXIF is overridable:
49+
50+
```elixir
51+
{:ok, corrected} =
52+
Image.LensFun.Correct.correct(image,
53+
make: "Canon",
54+
model: "Canon EOS 5D Mark III",
55+
lens_make: "Canon",
56+
lens_model: "Canon EF 100mm f/2.8 Macro USM",
57+
focal_length: 100.0,
58+
aperture: 5.6
59+
)
60+
```
61+
62+
The `:corrections` option selects which corrections to run; the default is `[:distortion, :vignetting, :tca]`. Projection conversion is opt-in (since it changes the field of view) via `corrections: [:projection]` and an optional `:target_projection`.
63+
64+
### Direct correction with explicit coefficients
65+
66+
When you have your own calibration data — or you're experimenting — each model is exposed as a standalone function that takes raw coefficients in the Hugin coordinate convention (`r = 1` at the half short-edge for distortion / TCA / projection, `r = 1` at the corner for vignetting):
67+
68+
```elixir
69+
# ptlens distortion (Hugin form)
70+
{:ok, image} = Image.LensCorrection.radial_distortion_correction(image, -0.0077, 0.087, 0.0)
71+
72+
# poly3 distortion
73+
{:ok, image} = Image.LensCorrection.poly3_correction(image, -0.005)
74+
75+
# poly5 distortion
76+
{:ok, image} = Image.LensCorrection.poly5_correction(image, -0.005, 0.001)
77+
78+
# Vignetting (Adobe Camera Model "pa")
79+
{:ok, image} = Image.LensCorrection.vignette_correction(image, -0.2764, -1.26031, 0.7727)
80+
81+
# Linear TCA
82+
{:ok, image} = Image.LensCorrection.Tca.linear_tca_correction(image, 1.0004, 1.0002)
83+
84+
# Fisheye → rectilinear
85+
focal = Image.LensCorrection.Geometry.focal_length_in_pixels(8.0, 1.5, 5000.0)
86+
{:ok, image} = Image.LensCorrection.Geometry.fisheye_to_rectilinear(image, focal)
87+
```
88+
89+
### Database lookup
90+
91+
```elixir
92+
# Find a lens (case-insensitive maker, fuzzy-substring model)
93+
{:ok, lens} = Image.LensFun.find_lens("Canon", "Canon EF 100mm f/2.8 Macro USM")
94+
95+
# Interpolate the calibration to a specific focal length
96+
{:ok, distortion} = Image.LensFun.interpolate_distortion(lens, 100.0)
97+
98+
# Vignetting needs focal length, aperture and focus distance
99+
{:ok, vignetting} = Image.LensFun.interpolate_vignetting(lens, 100.0, 5.6, 1000.0)
100+
101+
# TCA needs focal length only
102+
{:ok, tca} = Image.LensFun.interpolate_tca(lens, 100.0)
103+
104+
# Apply the result
105+
{:ok, image} = Image.LensCorrection.apply_distortion(image, distortion)
106+
{:ok, image} = Image.LensCorrection.apply_vignetting(image, vignetting)
107+
{:ok, image} = Image.LensCorrection.Tca.apply_tca(image, tca)
108+
```
109+
110+
## Background
111+
112+
The lens defects this library corrects, the mathematical models it uses, and the relationship to the upstream lensfun project are described in detail in the [Lens corrections guide](guides/lens_corrections.md).
113+
114+
## Rebuilding the bundled database
115+
116+
```sh
117+
git clone https://github.com/lensfun/lensfun ../lensfun
118+
mix run -e 'Image.LensFun.Importer.import()'
119+
```
120+
121+
## Attribution
122+
123+
This library re-implements the calibration math and bundles a snapshot of the calibration data from [the lensfun project](https://github.com/lensfun/lensfun). Lens calibration data is distributed under [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/); the lensfun reference C library is LGPL v3 (this library does not link to or distribute it).
124+
125+
## License
21126

127+
`ImageLensCorrection` is licensed under the [Apache 2.0 License](LICENSE.md).

0 commit comments

Comments
 (0)