Lens correction in the browser with Lensfun + WebAssembly

Lens correction is one of those things that sounds simple until you actually try to ship it on the web. The math exists. The database exists. But getting it into a browser bundle, loading the DB reliably, and keeping the API sane is usually where the weekend disappears.

I wanted something practical: run Lensfun in the browser, get correction maps back, and apply them in my own renderer (WebGL/WebGPU/canvas). So I packaged Lensfun into WebAssembly and wrapped it with a small, typed JS API:

  • npm: @neoanaloglabkk/lensfun-wasm
  • Lensfun compiled to WebAssembly
  • ships the official Lensfun DB as an Emscripten .data asset (preloaded to /lensfun-db)
  • generates correction maps for geometry/distortion, TCA, and vignetting

If you already have a remap step in your pipeline, this saves you the native glue work.

Links:

  • npm: https://www.npmjs.com/package/@neoanaloglabkk/lensfun-wasm
  • GitHub: https://github.com/lexluthor0304/LensfunWasm
  • jsDelivr (0.1.1): https://cdn.jsdelivr.net/npm/@neoanaloglabkk/lensfun-wasm@0.1.1/

What it is (and what it isn’t)

It is

  • Lens/camera search against the official Lensfun database
  • A correction map generator you can feed into a remap stage:
    • geometry/distortion (geometry)
    • TCA / subpixel distortion (tca)
    • vignetting gains (vignetting)

It isn’t

  • a full image-processing pipeline
  • a “one-call” photo editor

You still decide how to sample pixels and how to blend/interpolate. The library’s job is to give you the numbers in a form that’s easy to move into a shader or a CPU remapper.


Install (bundlers)

npm install @neoanaloglabkk/lensfun-wasm

Quick start (Vite-style)

import { createLensfun } from '@neoanaloglabkk/lensfun-wasm';
import createLensfunCoreModule from '@neoanaloglabkk/lensfun-wasm/core';
import wasmUrl from '@neoanaloglabkk/lensfun-wasm/core-wasm?url';
import dataUrl from '@neoanaloglabkk/lensfun-wasm/core-data?url';

const client = await createLensfun({
  moduleFactory: createLensfunCoreModule,
  wasmUrl,
  dataUrl,
});

const lenses = client.searchLenses({ lensModel: 'pEntax 50-200 ED' });
console.log(lenses[0]);

What’s happening here:

  • core provides the Emscripten module factory.
  • core-wasm and core-data are static assets, served by your app.
  • createLensfun() initializes the DB by default (autoInitDb: true).

Quick start (CDN / jsDelivr)

Pin versions in production. Example for 0.1.1:

<script src="https://cdn.jsdelivr.net/npm/@neoanaloglabkk/lensfun-wasm@0.1.1/dist/assets/lensfun-core.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@neoanaloglabkk/lensfun-wasm@0.1.1/dist/umd/index.iife.js"></script>
<script>
  (async () => {
    const client = await LensfunWasm.createLensfun();
    const lenses = client.searchLenses({ lensModel: 'pEntax 50-200 ED' });
    console.log(lenses[0]);
  })();
</script>

Generate correction maps

The workflow is simple:

  1. Search lenses/cameras.
  2. Pick a match and keep its handle.
  3. Build correction maps using your image parameters.
  4. Apply those maps in your renderer.

Here’s a compact example:

import {
  LF_MODIFY_TCA,
  LF_MODIFY_VIGNETTING,
  createLensfun,
} from '@neoanaloglabkk/lensfun-wasm';

const client = await createLensfun(/* ... */);

const [lens] = client.searchLenses({ lensModel: 'pEntax 50-200 ED' });
if (!lens) throw new Error('No lens match');

const width = 4000;
const height = 3000;
const focal = 70;
const crop = lens.cropFactor; // or your camera crop factor

const mods = client.getAvailableModifications(lens.handle, crop);

const maps = client.buildCorrectionMaps({
  lensHandle: lens.handle,
  width,
  height,
  focal,
  crop,

  // Grid spacing in pixels. Larger = smaller maps + faster build.
  // Your renderer usually interpolates between grid points.
  step: 8,

  includeTca: Boolean(mods & LF_MODIFY_TCA),
  includeVignetting: Boolean(mods & LF_MODIFY_VIGNETTING),
  aperture: 5.6, // required when includeVignetting=true
});

Map shapes (so you know what you’re moving around):

  • geometry: Float32Array length = gridW * gridH * 2 ([x0, y0, x1, y1, ...])
  • tca?: Float32Array length = gridW * gridH * 6 ([rx, ry, gx, gy, bx, by, ...])
  • vignetting?: Float32Array length = gridW * gridH * 3 ([rGain, gGain, bGain, ...])

Practical notes

  • Reuse a single LensfunClient if you can. DB init is the expensive part.
  • step is your main performance dial. Start with 8 or 16 for preview, then tighten it for export.
  • Call client.dispose() when you’re done to release native DB memory.
  • License: LGPL-3.0-or-later. If you’re distributing this in a product, check how you want to handle compliance.

镜头矫正这件事,只有在“真的要在网页里上线”时才会显得麻烦:算法不缺、数据库不缺,缺的是前端可落地的一套打包方式和一个不别扭的 API。

我想要的目标很具体:

  • Lensfun 在浏览器里跑
  • 直接拿到矫正 map(而不是一堆 native 细节)
  • 最后怎么 remap,交给我自己的渲染管线(WebGL/WebGPU/canvas)

所以做了这个包:@neoanaloglabkk/lensfun-wasm

它把 Lensfun 编译成 WebAssembly,并把官方镜头数据库打包成 Emscripten .data 资源(默认预载到 /lensfun-db)。你在前端拿到的,就是可以直接用的 map。

链接:

  • npm:https://www.npmjs.com/package/@neoanaloglabkk/lensfun-wasm
  • GitHub:https://github.com/lexluthor0304/LensfunWasm
  • jsDelivr(0.1.1):https://cdn.jsdelivr.net/npm/@neoanaloglabkk/lensfun-wasm@0.1.1/

它是什么(以及它不是什么)

它是

  • 官方 Lensfun 数据库的查询与匹配(搜镜头、搜机身)
  • 生成矫正 map 的工具,方便接入 remap 流程:
    • 几何/畸变(geometry
    • 横向色差 / subpixel distortion(tca
    • 暗角增益(vignetting

它不是

  • 一套完整的像素处理管线
  • “一键修图”的成品滤镜

你依然要决定采样方式、插值策略、渲染路径。这个库做的是把 Lensfun 的结果变成前端好用的数据结构。


安装(Bundler)

npm install @neoanaloglabkk/lensfun-wasm

快速开始(以 Vite 为例)

import { createLensfun } from '@neoanaloglabkk/lensfun-wasm';
import createLensfunCoreModule from '@neoanaloglabkk/lensfun-wasm/core';
import wasmUrl from '@neoanaloglabkk/lensfun-wasm/core-wasm?url';
import dataUrl from '@neoanaloglabkk/lensfun-wasm/core-data?url';

const client = await createLensfun({
  moduleFactory: createLensfunCoreModule,
  wasmUrl,
  dataUrl,
});

补充几点:

  • core 提供 Emscripten module factory。
  • .wasm.data 作为静态资源由你的应用托管。
  • createLensfun() 默认会初始化数据库(autoInitDb: true)。

CDN(jsDelivr)

生产环境建议固定版本。以下用 0.1.1

<script src="https://cdn.jsdelivr.net/npm/@neoanaloglabkk/lensfun-wasm@0.1.1/dist/assets/lensfun-core.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@neoanaloglabkk/lensfun-wasm@0.1.1/dist/umd/index.iife.js"></script>
<script>
  (async () => {
    const client = await LensfunWasm.createLensfun();
    const lenses = client.searchLenses({ lensModel: 'pEntax 50-200 ED' });
    console.log(lenses[0]);
  })();
</script>

生成矫正 map(核心用法)

实际接入时,我一般按这个顺序来:

  1. 搜镜头/机身
  2. 选一个匹配结果(拿到 handle
  3. 用图像参数生成 map
  4. 在渲染层做 remap

示例:

import {
  LF_MODIFY_TCA,
  LF_MODIFY_VIGNETTING,
  createLensfun,
} from '@neoanaloglabkk/lensfun-wasm';

const client = await createLensfun(/* ... */);

const [lens] = client.searchLenses({ lensModel: 'pEntax 50-200 ED' });
if (!lens) throw new Error('No lens match');

const width = 4000;
const height = 3000;
const focal = 70;
const crop = lens.cropFactor;

const mods = client.getAvailableModifications(lens.handle, crop);

const maps = client.buildCorrectionMaps({
  lensHandle: lens.handle,
  width,
  height,
  focal,
  crop,

  // 网格步长(像素)。越大越快、数据越小,
  // 渲染侧通常需要在网格点之间做插值。
  step: 8,

  includeTca: Boolean(mods & LF_MODIFY_TCA),
  includeVignetting: Boolean(mods & LF_MODIFY_VIGNETTING),
  aperture: 5.6, // includeVignetting=true 时必填
});

map 的数据规模(方便你提前估算传输/纹理大小):

  • geometry: Float32Array 长度 = gridW * gridH * 2[x0, y0, x1, y1, ...]
  • tca?: Float32Array 长度 = gridW * gridH * 6[rx, ry, gx, gy, bx, by, ...]
  • vignetting?: Float32Array 长度 = gridW * gridH * 3[rGain, gGain, bGain, ...]

经验小结

  • 尽量复用一个 LensfunClient,别频繁初始化数据库。
  • step 是你最重要的性能旋钮:预览可以从 8/16 起步,需要更细再往下调。
  • 用完记得 client.dispose(),它会释放 WASM 侧的数据库内存。
  • License 是 LGPL-3.0-or-later,如果你要做商业分发,建议提前想清楚合规方式。

途工街をもっと見る

購読すると最新の投稿がメールで送信されます。

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください