树莓派IspPipeline LSC模块原理详解

目录

整体架构总览

核心类与结构体

整体调用链路

一、两种实现子类详解

[1. LscTable 网格查表模式(type: table)](#1. LscTable 网格查表模式(type: table))

关键行为

[2. LscPolynomial 径向多项式模式(type: polynomial)](#2. LscPolynomial 径向多项式模式(type: polynomial))

多项式核心计算

[二、上层控制逻辑 lsc.cpp/lsc.h](#二、上层控制逻辑 lsc.cpp/lsc.h)

LscAlgorithmBase

[LscAlgorithm 模板类](#LscAlgorithm 模板类)

[configure () 核心函数](#configure () 核心函数)

[interpolateComponents(unsigned int ct)](#interpolateComponents(unsigned int ct))

getComponents()

[三、Table 模式 vs Polynomial 模式对比](#三、Table 模式 vs Polynomial 模式对比)

四、完整调用时序

五、工程踩坑清单

六、代码实现


整体架构总览

采用虚基类 + 多实现的设计模式,将调参解析、增益生成与上层控制、色温插值、定点量化做解耦。

核心类与结构体

  1. LscImplementation(lsc_base.h)纯虚基类 定义统一接口,隔离两种 LSC 实现。

    • parseLscData():解析 tuning 配置文件,加载多色温 LSC 数据集。

    • sampleForCrop():根据 sensor 的 analogCrop,重采样生成适配当前画面的增益集合。

    • 两个别名:

      • Componentsstd::map<std::string, std::vector<float>>,key 为颜色通道名字,value 为通道对应的网格增益数组。注意:Table 和 Polynomial 模式下,vector<float>存储的数值语义完全不一样
      • ComponentsMapstd::map<unsigned int, Components>,key 为色温 ct,存储多套不同色温的 LSC 数据。
    • 派生实现:

      • LscTable:网格查表模式;不支持 crop 重采样,源码标记 TODO 待实现。
      • LscPolynomial:径向多项式模式;支持任意 analogCrop、任意输出网格,遵循 Adobe DNG FixVignetteRadial 晕圈模型。
  2. LscAlgorithmBase 非模板上层基类 负责 tuning 初始化、处理 APP 下发的 LSC 开关控制、metadata 元数据回填。

  3. LscAlgorithm<U> 模板子类(lsc.h) 模板参数U为平台定点量化工具类,负责浮点增益向 ISP 硬件定点格式转换; 内部持有Interpolator<Components> sets_,提供色温线性插值接口,输出可直接下发 ISP 的量化增益表。

  4. 状态结构体

    • ActiveState跨帧持久状态 ,仅成员bool enabled,保存 LSC 全局使能标记。
    • FrameContext单帧独立上下文 ,每帧重新生成;
      • enabled:本帧 LSC 是否开启;
      • update:标记本帧是否需要更新 LSC 参数。
  5. 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, ...]
关键行为
  1. parseLscData() 遍历 sets 数组,校验每个通道数组元素个数等于numHSamples * numVSamples; yaml 中的数值是ISP 定点寄存器字面量 ,读取存入std::vector<float>,只是容器类型为 float,没有做浮点增益换算

源码 TODO:未来希望 table 模式 tuning 直接存储物理浮点增益,统一在 configure 做量化。

  1. 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: {...}
多项式核心计算
  1. 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);
  1. 坐标归一化:所有像素坐标全部除以 M,得到 DNG 规范定义的归一化坐标系。

  2. 增益采样函数 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 图像归一化坐标。
  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 硬件网格加载顺序一致,否则颜色错乱。

  1. sampleForCrop() 遍历全部色温集合,对每个通道多项式,按传入的网格节点采样,输出物理浮点增益集合。✅支持任意 analogCrop,任意输出网格。

⚠️注意:调参的cx/cy是相对于完整 sensor 尺寸,不是 crop 后的画面。LscDescriptor::sensorSize必须和标定时使用的 sensor 尺寸完全一致。


二、上层控制逻辑 lsc.cpp/lsc.h

LscAlgorithmBase

  1. init() 读取 tuning 中type字段,实例化对应实现类LscTable / LscPolynomial;调用parseLscData解析 sets 数据集;注册控制项LensShadingCorrectionEnable

  2. queueRequest() 处理 Request 携带的LensShadingCorrectionEnable控制。

  • 如果使能状态发生改变,更新持久态ActiveState::enabled,标记FrameContext::update = true
  • 将状态同步至本帧上下文FrameContext::enabled
  1. 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 硬件无关,可跨平台复用
数据体积 大,保存多套完整网格 极小,仅保存多项式系数
依赖配置项 numHSamplesnumVSamples LscDescriptor::sensorSize(完整 sensor 像素尺寸)

四、完整调用时序

  1. IPA 初始化:实例化LscAlgorithm<U>,填充平台LscDescriptor描述符;
  2. 调用init(tuningData, controls, descriptor)解析 yaml 配置,实例化对应的 LSC 实现;
  3. 流媒体启动 /sensor 切换分辨率 /analogCrop:调用configure(state, analogCrop, xPos, yPos)
    • polynomial 模式:多项式按 crop 与网格节点采样,得到物理浮点增益,量化为 ISP 定点存入sets_
    • table 模式:返回原始数据集,仅做类型转换;
  4. 帧循环:
    1. queueRequest(state, context, request.controls):处理 APP 下发 LSC 开关;修改持久态ActiveState,填充本帧FrameContext
    2. 上层 IPA 拿到 AWB 输出色温 ct,调用interpolateComponents(ct),得到插值完成的量化增益集合;平台 IPA 负责将增益集合下发 ISP 硬件
    3. process(context, metadata):将本帧 LSC 使能状态写入 metadata 元数据。

五、工程踩坑清单

  1. LscDescriptor::keys必须与 yaml 中每个 set 下的通道 key 严格匹配(区分r/gr/gb/b),否则解析返回‑EINVAL。
  2. Table 模式下,sensor 使用 analog‑crop /binning,LSC 网格不会跟随画面变化,校正失效;项目条件允许优先使用 Polynomial 模式。
  3. Polynomial 模式,LscDescriptor::sensorSize必须等于标定多项式时的完整 sensor 尺寸,填错会造成坐标变换全部错误。
  4. configure()必须在interpolateComponents()之前调用,未配置时 sets_为空,输出空的增益集合。
  5. Polynomial 调参中cxcy完整 sensor 的归一化光学中心,不是 crop 之后画面的坐标
  6. 不要混用两种模式的增益语义:Table 的 yaml 数值是寄存器定点编码;Polynomial 输出 1.0 代表不需要亮度补偿。
  7. Polynomial 模式输出数组顺序:y 外层循环、x 内层循环(行优先);输出顺序必须和 ISP 硬件网格加载顺序保持一致,否则画面颜色错乱。
  8. 色温插值的越界限幅是通用Interpolator组件行为,不是 LSC 模块实现。
  9. 插值时要求两套参与插值的 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 */
相关推荐
zander2582 小时前
LeetCode 300. 最长递增子序列
算法·leetcode·深度优先
祖力553 小时前
Linux应用软件编程:目录IO与framebuffer
linux·运维·算法·framebuffer·目录io
名字还没想好☜3 小时前
Go 用 slices/maps 标准库泛型函数:告别手写 Contains、Sort、去重(Go 1.21)
开发语言·后端·算法·golang·go
地平线开发者3 小时前
【模型轻量化专题】深度学习模型为什么需要轻量化
算法
_Narcissus_3 小时前
链表算法题和静态链表
数据结构·c++·笔记·算法·链表·ai·力扣
Lyyaoo.3 小时前
【普通数组】【中等】除了自身以外数组的乘积
数据结构·算法·leetcode
threerocks4 小时前
《The AI-Native SDLC Playbook》万字拆解
算法·aigc·ai编程
夏玉林的学习之路4 小时前
算法8.环形队列
算法
O。O蛋黄酥啊5 小时前
GraphRAG 和 LightRAG 详解与对比
人工智能·python·算法·rag·graphrag·lightrag