目录
[1. LscTable 网格查表模式(type: table)](#1. LscTable 网格查表模式(type: table))
[2. LscPolynomial 径向多项式模式(type: polynomial)](#2. LscPolynomial 径向多项式模式(type: polynomial))
[二、上层控制逻辑 lsc.cpp/lsc.h](#二、上层控制逻辑 lsc.cpp/lsc.h)
[LscAlgorithm 模板类](#LscAlgorithm 模板类)
[configure () 核心函数](#configure () 核心函数)
[interpolateComponents(unsigned int ct)](#interpolateComponents(unsigned int ct))
[三、Table 模式 vs Polynomial 模式对比](#三、Table 模式 vs Polynomial 模式对比)
整体架构总览
采用虚基类 + 多实现的设计模式,将调参解析、增益生成与上层控制、色温插值、定点量化做解耦。
核心类与结构体
-
LscImplementation(lsc_base.h)纯虚基类 定义统一接口,隔离两种 LSC 实现。-
parseLscData():解析 tuning 配置文件,加载多色温 LSC 数据集。 -
sampleForCrop():根据 sensor 的 analogCrop,重采样生成适配当前画面的增益集合。 -
两个别名:
Components:std::map<std::string, std::vector<float>>,key 为颜色通道名字,value 为通道对应的网格增益数组。注意:Table 和 Polynomial 模式下,vector<float>存储的数值语义完全不一样。ComponentsMap:std::map<unsigned int, Components>,key 为色温 ct,存储多套不同色温的 LSC 数据。
-
派生实现:
LscTable:网格查表模式;不支持 crop 重采样,源码标记 TODO 待实现。LscPolynomial:径向多项式模式;支持任意 analogCrop、任意输出网格,遵循 Adobe DNG FixVignetteRadial 晕圈模型。
-
-
LscAlgorithmBase非模板上层基类 负责 tuning 初始化、处理 APP 下发的 LSC 开关控制、metadata 元数据回填。 -
LscAlgorithm<U>模板子类(lsc.h) 模板参数U为平台定点量化工具类,负责浮点增益向 ISP 硬件定点格式转换; 内部持有Interpolator<Components> sets_,提供色温线性插值接口,输出可直接下发 ISP 的量化增益表。 -
状态结构体
ActiveState:跨帧持久状态 ,仅成员bool enabled,保存 LSC 全局使能标记。FrameContext:单帧独立上下文 ,每帧重新生成;enabled:本帧 LSC 是否开启;update:标记本帧是否需要更新 LSC 参数。
-
LscDescriptor平台描述符(平台 IPA 必须填充)
cpp
struct LscDescriptor {
std::vector<std::string> keys; // 通道名称集合 {"r","gr","gb","b"} 或者 {"r","g","b"}
unsigned int numHSamples; // ISP LSC网格水平采样点数
unsigned int numVSamples; // ISP LSC网格垂直采样点数
Size sensorSize; // 完整原始sensor像素尺寸,多项式模式必需
};
- Table 模式:使用
numHSamples * numVSamples校验 yaml 增益数组长度。 - Polynomial 模式:
sensorSize用于多项式的坐标归一化与 M 值计算。
整体调用链路
plaintext
cpp
init() → 根据tuning中type字段创建LscTable/LscPolynomial实例,解析sets多色温数据集
queueRequest() → 处理Request携带的LensShadingCorrectionEnable控制,更新持久态、标记帧上下文update
configure() → 调用impl_->sampleForCrop()完成crop重采样;将增益量化为ISP定点格式存入sets_插值容器
interpolateComponents(ct) → 根据AWB输出色温,在多色温数据集之间做线性插值,输出最终增益集合
process() → 将本帧LSC使能状态写入metadata元数据
重要:LSC 算法模块不操作 ISP 硬件寄存器,仅输出增益数据集;由上层平台 IPA 负责把增益写入 ISP。
一、两种实现子类详解
1. LscTable 网格查表模式(type: table)
tuning yaml 中直接存储每个色温下各通道完整的网格增益数组。
yaml
cpp
- Lsc:
type: "table"
sets:
- ct: 2500
r: [定点编码值0, 定点编码值1, ...]
g: [定点编码值0, 定点编码值1, ...]
b: [定点编码值0, 定点编码值1, ...]
关键行为
parseLscData()遍历 sets 数组,校验每个通道数组元素个数等于numHSamples * numVSamples; yaml 中的数值是ISP 定点寄存器字面量 ,读取存入std::vector<float>,只是容器类型为 float,没有做浮点增益换算。
源码 TODO:未来希望 table 模式 tuning 直接存储物理浮点增益,统一在 configure 做量化。
sampleForCrop()打印警告日志,不执行重采样,直接返回原始加载的数据集。
限制:当 sensor 开启 analog crop、binning,输出网格不会跟随画面变化,LSC 校正效果失效。
⚠️语义重点:Table 模式的Components内部vector<float>存储的是ISP 定点编码,不是物理增益,1.0 不代表无校正。
2. LscPolynomial 径向多项式模式(type: polynomial)
遵循 Adobe DNG FixVignetteRadial 径向晕圈模型,每个颜色通道存储一组参数:光学中心cx, cy,多项式系数k0~k4。
yaml
html
- Lsc:
type: "polynomial"
sets:
- ct: 2500
r: {cx:0.500, cy:0.510, k0:1.539, k1:-1.143, k2:4.332, k3:0, k4:0}
gr: {...}
gb: {...}
b: {...}
多项式核心计算
- M 值计算
getM()M:原始 sensor 尺寸下,光学中心到图像最远角点的欧几里得像素距离。
注意:
cx_、cy_是调参得到的相对于完整 sensor 的归一化光学中心,不一定等于图像几何中心。
cpp
double cpx = imageSize_.width * cx_;
double cpy = imageSize_.height * cy_;
double mx = std::max(cpx, std::fabs(imageSize_.width - cpx));
double my = std::max(cpy, std::fabs(imageSize_.height - cpy));
return sqrt(mx * mx + my * my);
-
坐标归一化:所有像素坐标全部除以 M,得到 DNG 规范定义的归一化坐标系。
-
增益采样函数
sampleAtNormalizedPixelPos(x,y)输入为除以 M 后的归一化坐标 xp、yp:
cpp
double dx = x - cnx_;
double dy = y - cny_;
double r = sqrt(dx * dx + dy * dy);
double res = 1.0;
for (unsigned int i = 0; i < coefficients_.size(); i++)
res += coefficients_[i] * std::pow(r, (i + 1) * 2);
return res;
公式:
- 返回值为物理浮点增益 ,
1.0代表无需校正补偿。 - r:除以 M 后的归一化径向距离,不是原始像素距离,也不是普通 0‑1 图像归一化坐标。
samplePolynomial()两层坐标映射xPos/yPos:相对于 crop 区域的 0‑1 网格顶点坐标。
cpp
double x0 = cropRectangle.x / m;
double y0 = cropRectangle.y / m;
double w = cropRectangle.width / m;
double h = cropRectangle.height / m;
double xp = x0 + x * w;
double yp = y0 + y * h;
变换链路:crop 局部归一坐标 → DNG M 归一化坐标系。 采样输出顺序:y 外层循环、x 内层循环(行优先),输出数组排布必须和 ISP 硬件网格加载顺序一致,否则颜色错乱。
sampleForCrop()遍历全部色温集合,对每个通道多项式,按传入的网格节点采样,输出物理浮点增益集合。✅支持任意 analogCrop,任意输出网格。
⚠️注意:调参的
cx/cy是相对于完整 sensor 尺寸,不是 crop 后的画面。LscDescriptor::sensorSize必须和标定时使用的 sensor 尺寸完全一致。
二、上层控制逻辑 lsc.cpp/lsc.h
LscAlgorithmBase
-
init() 读取 tuning 中
type字段,实例化对应实现类LscTable/LscPolynomial;调用parseLscData解析 sets 数据集;注册控制项LensShadingCorrectionEnable。 -
queueRequest() 处理 Request 携带的
LensShadingCorrectionEnable控制。
- 如果使能状态发生改变,更新持久态
ActiveState::enabled,标记FrameContext::update = true; - 将状态同步至本帧上下文
FrameContext::enabled。
- process() 将本帧的
context.enabled写入 metadata 元数据,回传给上层应用。
LscAlgorithm++模板类++
configure () 核心函数
cpp
//1. 根据当前analogCrop重采样得到浮点ComponentsMap
LscImplementation::ComponentsMap data = impl_->sampleForCrop(analogCrop, xPos, yPos);
//2. 做定点转换,存入插值容器sets_
for (auto &[t, c] : data) {
for (auto &[k, gains] : c) {
for(auto &gain : gains) {
if (polynomial_)
//多项式:物理浮点增益 → ISP定点格式,执行量化运算
quantizedGains.push_back(U(gain).quantized());
else
//table模式:已经是定点字面量,仅做隐式类型转换,不做量化
quantizedGains.push_back(gain);
}
}
}
sets_.setData(std::move(lscData));
state.enabled = true;
关键差异:
- Polynomial:输出物理浮点增益,运行时执行浮点→定点量化。
- Table:数据来自 yaml 的定点字面量,只做类型转换,不做量化运算。
interpolateComponents(unsigned int ct)
cpp
const Components interpolateComponents(unsigned int ct)
{
return sets_.getInterpolated(ct);
}
- 输入 AWB 输出的色温 ct;调用
Interpolator工具,在已加载的多色温数据集之间做线性插值。 - 底层模板特化实现:对每个颜色通道的增益 vector 逐元素线性插值:
dest[i] = a[i]*(1‑lambda)+b[i]*lambda。 - 插值的 clamp 限幅行为来自通用
Interpolator组件,不属于 LSC 模块内部逻辑。
约束:调用该接口前必须先调用
configure(),否则 sets_为空,返回空数据集。
getComponents()
返回全部未插值的原始多色温数据集,用于调试。
三、Table 模式 vs Polynomial 模式对比
| 项目 | LscTable 网格查表模式 | LscPolynomial 径向多项式模式 |
|---|---|---|
| 调参存储 | 每个色温存储完整二维增益网格数组,yaml 存放 ISP 定点字面量 | 每个通道存储多项式系数 cx,cy,k0~k4 |
| Components 内 vector<float>语义 | 存储ISP 定点寄存器编码值,不是物理增益 | 存储物理浮点增益,1.0 代表无校正 |
| analogCrop 重采样 | ❌不支持,直接返回原始表并打印警告 | ✅支持任意 crop、任意输出网格节点 |
| configure 阶段处理 | 仅做数值隐式转换,不执行浮点‑定点量化 | 采样输出浮点增益,调用 U 执行浮点→定点量化 |
| 调参可移植性 | 绑定 ISP 定点格式,不能跨平台直接复用 | 纯浮点参数,与 ISP 硬件无关,可跨平台复用 |
| 数据体积 | 大,保存多套完整网格 | 极小,仅保存多项式系数 |
| 依赖配置项 | numHSamples、numVSamples |
LscDescriptor::sensorSize(完整 sensor 像素尺寸) |
四、完整调用时序
- IPA 初始化:实例化
LscAlgorithm<U>,填充平台LscDescriptor描述符; - 调用
init(tuningData, controls, descriptor)解析 yaml 配置,实例化对应的 LSC 实现; - 流媒体启动 /sensor 切换分辨率 /analogCrop:调用
configure(state, analogCrop, xPos, yPos)- polynomial 模式:多项式按 crop 与网格节点采样,得到物理浮点增益,量化为 ISP 定点存入
sets_; - table 模式:返回原始数据集,仅做类型转换;
- polynomial 模式:多项式按 crop 与网格节点采样,得到物理浮点增益,量化为 ISP 定点存入
- 帧循环:
queueRequest(state, context, request.controls):处理 APP 下发 LSC 开关;修改持久态ActiveState,填充本帧FrameContext;- 上层 IPA 拿到 AWB 输出色温 ct,调用
interpolateComponents(ct),得到插值完成的量化增益集合;平台 IPA 负责将增益集合下发 ISP 硬件; process(context, metadata):将本帧 LSC 使能状态写入 metadata 元数据。
五、工程踩坑清单
LscDescriptor::keys必须与 yaml 中每个 set 下的通道 key 严格匹配(区分r/gr/gb/b),否则解析返回‑EINVAL。- Table 模式下,sensor 使用 analog‑crop /binning,LSC 网格不会跟随画面变化,校正失效;项目条件允许优先使用 Polynomial 模式。
- Polynomial 模式,
LscDescriptor::sensorSize必须等于标定多项式时的完整 sensor 尺寸,填错会造成坐标变换全部错误。 configure()必须在interpolateComponents()之前调用,未配置时 sets_为空,输出空的增益集合。- Polynomial 调参中
cx、cy是完整 sensor 的归一化光学中心,不是 crop 之后画面的坐标。 - 不要混用两种模式的增益语义:Table 的 yaml 数值是寄存器定点编码;Polynomial 输出 1.0 代表不需要亮度补偿。
- Polynomial 模式输出数组顺序:y 外层循环、x 内层循环(行优先);输出顺序必须和 ISP 硬件网格加载顺序保持一致,否则画面颜色错乱。
- 色温插值的越界限幅是通用
Interpolator组件行为,不是 LSC 模块实现。 - 插值时要求两套参与插值的 Components 的 key 集合、每个 vector 的长度完全一致,否则触发 ASSERT 崩溃,该条件由 configure 流程保证。
六、代码实现
cpp
/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
* Copyright (C) 2024, Ideas On Board
*
* Polynomial based lens shading correction
*/
#include "lsc_polynomial.h"
#include <assert.h>
#include <cmath>
#include <libcamera/base/log.h>
/**
* \file lsc_polynomial.h
* \brief LscPolynomial class
*/
namespace libcamera {
LOG_DEFINE_CATEGORY(LscPolynomial)
namespace ipa {
namespace lsc {
/**
* \class Polynomial
* \brief Class for handling even polynomials used in lens shading correction
*
* Shading artifacts of camera lenses can be modeled using even radial
* polynomials. This class implements a polynomial with 5 coefficients which
* follows the definition of the FixVignetteRadial opcode in the Adobe DNG
* specification.
*/
/**
* \fn Polynomial::Polynomial(double cx = 0.0, double cy = 0.0, double k0 = 0.0,
double k1 = 0.0, double k2 = 0.0, double k3 = 0.0,
double k4 = 0.0)
* \brief Construct a polynomial using the given coefficients
* \param cx Center-x relative to the image in normalized coordinates (0..1)
* \param cy Center-y relative to the image in normalized coordinates (0..1)
* \param k0 Coefficient of the polynomial
* \param k1 Coefficient of the polynomial
* \param k2 Coefficient of the polynomial
* \param k3 Coefficient of the polynomial
* \param k4 Coefficient of the polynomial
*/
/**
* \brief Sample the polynomial at the given normalized pixel position
*
* This functions samples the polynomial at the given pixel position divided by
* the value returned by getM().
*
* \param x x position in normalized coordinates
* \param y y position in normalized coordinates
* \return The sampled value
*/
double Polynomial::sampleAtNormalizedPixelPos(double x, double y) const
{
double dx = x - cnx_;
double dy = y - cny_;
double r = sqrt(dx * dx + dy * dy);
double res = 1.0;
for (unsigned int i = 0; i < coefficients_.size(); i++)
res += coefficients_[i] * std::pow(r, (i + 1) * 2);
return res;
}
/**
* \brief Get the value m as described in the dng specification
*
* Returns m according to dng spec. m represents the Euclidean distance
* (in pixels) from the optical center to the farthest pixel in the
* image.
*
* \return The sampled value
*/
double Polynomial::getM() const
{
double cpx = imageSize_.width * cx_;
double cpy = imageSize_.height * cy_;
double mx = std::max(cpx, std::fabs(imageSize_.width - cpx));
double my = std::max(cpy, std::fabs(imageSize_.height - cpy));
return sqrt(mx * mx + my * my);
}
/**
* \brief Set the reference image size
*
* Set the reference image size that is used for subsequent calls to getM() and
* sampleAtNormalizedPixelPos()
*
* \param size The size of the reference image
*/
void Polynomial::setReferenceImageSize(const Size &size)
{
assert(!size.isNull());
imageSize_ = size;
/* Calculate normalized centers */
double m = getM();
cnx_ = (size.width * cx_) / m;
cny_ = (size.height * cy_) / m;
}
} /* namespace lsc */
/**
* \class LscPolynomial
* \brief Radial Polynomial LSC algorithm implementation
*
* Polynomial-based LSC algorithm implementation. The LscPolynomial class
* implements LSC support using a Polynomial to represent the shading artifacts
* map.
*
* \sa LscImplementation
*/
/**
* \brief Parse polynomial LSC data
* \param[in] sets The tuning file content
* \param[in] descriptor The LSC engine descriptor
*
* Parse the LSC data in polyomial form from the \a sets tuning data.
*
* \return 0 on success or a negative error number otherwise
*/
int LscPolynomial::parseLscData(const ValueNode &sets,
const LscDescriptor &descriptor)
{
for (const auto &set : sets.asList()) {
uint32_t ct = set["ct"].get<uint32_t>(0);
PolynomialComponents components;
for (auto &k : descriptor.keys) {
auto polynomial = set[k].get<lsc::Polynomial>();
if (!polynomial) {
LOG(LscPolynomial, Error)
<< "Missing polynomial for component "
<< k;
return -EINVAL;
}
auto [it, inserted] =
components.try_emplace(k, std::move(*polynomial));
ASSERT(inserted);
it->second.setReferenceImageSize(descriptor.sensorSize);
}
auto [it, inserted] = lscData_.try_emplace(ct, std::move(components));
if (!inserted) {
LOG(LscPolynomial, Error)
<< "Multiple sets found for "
<< "color temperature " << ct;
return -EINVAL;
}
}
if (lscData_.empty()) {
LOG(LscPolynomial, Error) << "Failed to load any sets";
return -EINVAL;
}
return 0;
}
/**
* \brief Re-sample the LSC components for \a cropRectangle
* \param[in] cropRectangle The sensor analogue crop rectangle
* \param[in] xPos List of horizontal positions of the LSC grid nodes
* \param[in] yPos List of vertical positions of the LSC grid nodes
*
* LSC tables have to be re-sampled every time a new sensor configuration is
* used, as each streaming session might use a different sensor crop rectangle.
*
* Polynomial LSC tables can be re-sampled for a given sensor frame resolution
* using a list of horizontal and vertical nodes that define the LSC grid on
* which the polynomial is re-sampled on.
*
* \a cropRectangle represents the size of the frame on which the LSC tables
* have to be re-sampled on.
*
* \a xPos and \a yPos represent the position of the grid nodes vertexes in
* the [0, 1] interval. In example an equally spaced grid of 16 nodes will have
* each segment of size 0.0625 and the list of nodes position will be
* [0, 0.0625, 0.125, 0.1875, ... , 1]. It is expected that the first position
* is 0 and the last position is 1.
*/
LscImplementation::ComponentsMap
LscPolynomial::sampleForCrop(const Rectangle &cropRectangle,
std::vector<double> xPos, std::vector<double> yPos)
{
LscImplementation::ComponentsMap components;
for (const auto &[t, c] : lscData_) {
LscImplementation::Components &comp = components[t];
for (const auto &[k, p] : c)
comp.try_emplace(k, samplePolynomial(p, xPos, yPos,
cropRectangle));
}
return components;
}
std::vector<float>
LscPolynomial::samplePolynomial(const lsc::Polynomial &poly,
Span<const double> xPositions,
Span<const double> yPositions,
const Rectangle &cropRectangle)
{
double m = poly.getM();
double x0 = cropRectangle.x / m;
double y0 = cropRectangle.y / m;
double w = cropRectangle.width / m;
double h = cropRectangle.height / m;
std::vector<float> samples;
samples.reserve(xPositions.size() * yPositions.size());
for (double y : yPositions) {
for (double x : xPositions) {
double xp = x0 + x * w;
double yp = y0 + y * h;
samples.push_back(static_cast<float>
(poly.sampleAtNormalizedPixelPos(xp, yp)));
}
}
return samples;
}
} /* namespace ipa */
#ifndef __DOXYGEN__
template<>
std::optional<ipa::lsc::Polynomial>
ValueNode::Accessor<ipa::lsc::Polynomial>::get(const ValueNode &obj) const
{
std::optional<double> cx = obj["cx"].get<double>();
std::optional<double> cy = obj["cy"].get<double>();
std::optional<double> k0 = obj["k0"].get<double>();
std::optional<double> k1 = obj["k1"].get<double>();
std::optional<double> k2 = obj["k2"].get<double>();
std::optional<double> k3 = obj["k3"].get<double>();
std::optional<double> k4 = obj["k4"].get<double>();
if (!(cx && cy && k0 && k1 && k2 && k3 && k4)) {
LOG(LscPolynomial, Error)
<< "Polynomial is missing a parameter";
return std::nullopt;
}
return ipa::lsc::Polynomial(*cx, *cy, *k0, *k1, *k2, *k3, *k4);
}
#endif /* __DOXYGEN__ */
} /* namespace libcamera */