树莓派相机自动白平衡详解(四)

树莓派相机自动白平衡详解(一)

树莓派相机自动白平衡详解(二)

树莓派相机自动白平衡详解(三)

以上说明的树莓派自动白平衡的一些基本思路。但是之前没有找到源码,因此有些理解还是存在一些误差。现在根据源码修正并详细说明。

目录

一、整体架构与调用流

二、核心函数逐条解析

三、上层后处理

四、代码实现

[五、原版 rpi‑controller vs libipa 移植版关键差异](#五、原版 rpi‑controller vs libipa 移植版关键差异)

五、数值示例


一、整体架构与调用流

算法运行于独立异步子线程;主线程只负责调度、拷贝结果、帧间 IIR 平滑、硬件输出。

复制代码
IPA主线程 process()
    ↓ 唤醒异步线程 asyncFunc()
        ↓ doAwb()(AwbBayes子类实现)
            1. prepareStats() 统计预处理
               └ generateStats()
                  · 过滤无效AWB统计网格region
                  · biasProportion:每个region叠加biasCT虚拟白点分量(无中性色块时兜底防发散)
                  · sensitivityR / sensitivityB 做传感器通道校正
            2. 判断有效zone数量,如果小于minRegions,直接跳过本次计算,复用上一帧结果
            3. awbBayes() 算法主体
                ① zone内部归一化:`R = R/(G+1)`,`B = B/(G+1)`,得到R/G、B/G色度
                ② interpolatePrior():依据当前lux照度,对多组yaml配置的prior做线性插值,生成当前帧prior(PWL:t→−logP(I))
                ③ prior整体乘以系数 `prior *= valid_zone_count / total_zone_count`;源码注释标注:属于经验调参,非严格贝叶斯推导
                ④ coarseSearch()【阶段1 粗搜索】:全部候选严格落在CT‑Curve普朗克轨迹上,输出`t_coarse`
                ⑤ fineSearch()【阶段2精细搜索】:允许白点离开CT‑Curve,适配LED、荧光等非黑体光源
                ⑥ 换算白平衡增益,乘以传感器校正系数,存入异步缓存`asyncResults_`
    ↓ fetchAsyncResults() 拷贝子线程结果
        · 帧间一阶IIR平滑(不属于Bayes算法本体)
        · gain硬件寄存器范围clamp限幅
        · 输出增益给ISP,metadata上报色温

二、核心函数逐条解析

1. generateStats / prepareStats

  1. 过滤 region:minPixels单 region 有效像素占比、minGG 通道最小值,不满足直接丢弃该 region。
  2. bias 兜底:不是新增 zone,在现有 region 的 R/B 累加虚拟白点分量,防止整张图无真实中性色块时算法完全失控。
  3. 乘以sensitivityR/sensitivityB,把传感器原始色度映射到 canonical 标准色度空间。

2. computeDelta2Sum 似然代价函数

输入候选白平衡增益 gainR、gainB,中性点偏移whitepointR/whitepointB(通常 = 0)

deltaLimit截断保护delta2 = min(delta2, deltaLimit);单个网格残差超过阈值直接钳位,抑制大片红墙、绿草等高饱和彩色物体干扰总打分。 返回所有 zone 截断后的残差平方累加和delta2Sum

总打分:finalLogLikelihood = delta2Sum − priorLogLikelihood;最小化该值等价最大化后验概率。

3. interpolatePrior

yaml 配置多组AwbPrior{lux, prior‑PWL}

  1. lux 小于最小配置 lux,取最小 lux 对应的 prior;lux 大于最大配置 lux,取最大 lux 对应的 prior。
  2. lux 落在两组 lux 中间,对两条 prior 折线做线性插值合成当前帧 prior。
  3. awbBayes 内执行经验缩放:prior *= valid_zone / total_zone

4. coarseSearch 阶段 1:CT‑Curve 全局粗搜索

  1. 在 mode 限定色温区间[ctLo, ctHi]遍历色温t;自适应步长:t += t/10 * coarseStep;色温越高步长越大。
  2. 每个t查表 CT‑Curve 得到曲线上白点;计算增益gainR=1/rgainB=1/b
  3. 调用computeDelta2Sum()得到似然代价;查表 prior 得到priorLogLikelihood;计算finalLogLikelihood
  4. 保存所有(t, finalLogLikelihood);选出代价最小采样点;取该点左右相邻点,调用interpolateQuadatric()三点二次抛物线插值,输出粗搜索最优色温t_coarse

所有候选点严格位于 CT‑Curve 上,不能横向偏移

5. fineSearch 阶段 2 精细搜索【重点,严格对齐原始 patch】

原始源码注释:Mostly the estimated CT is already good enough, but the fine search allows us to wander transverely off the CT curve. 翻译:绝大多数场景粗搜索输出的色温已经足够好;fineSearch主要设计目标:让白点可以离开普朗克 CT‑Curve,适配非黑体光源 ;色温发生变化属于循环带来的附带副作用,不是设计目标

执行步骤:

  1. 使用输入的粗搜索结果tIn = t_coarse;取tIn ± nsteps*step两点,计算差分得到切线,构造transverse向量,归一化为单位向量。

⚠️该向量仅在tIn位置几何上是 CT‑Curve 法线 ;向量一旦计算完成,全程固定不变 ;施加到其他tTest仅做简单矢量加法;代码不校验其他位置的几何法线关系。

  1. 如果向量模平方过小length2() <1e‑6,直接 return,不做横向偏移,输出保持 CT‑Curve 上结果。

  2. nsteps变量先用于计算 transverse 向量;之后执行nsteps += numDeltas,扩大外层循环的扫描范围;源码注释意图:横向方向采样点越多,顺带把 tTest 扫描范围放大。

  3. 外层 i 循环:生成一组测试色温tTest = tIn + i*step

    • 每个tTest查表 CT‑Curve,得到该色温对应的曲线上基准点 ;查表 prior 得到priorLogLikelihood
    • 内层 j 循环 :dist 取值区间硬边界 [-transverseNeg, transversePos];dist 是固定 transverse 单位向量的缩放系数

    ⚠️源码完全不计算候选白点到 CT‑Curve 的真实垂直几何距离没有 K・d² 二次距离惩罚项;仅限制 dist 缩放系数的取值范围。

    • 每个 dist,生成候选白点,计算 gainR/gainB,调用computeDelta2Sum(),得到该点总代价。
    • j 循环结束,对 j 方向采样点做interpolateQuadatric()二次插值,得到该tTest的最优distOpt必须重算插值点完整代价,不能复用采样数组内的代价
    • 和全局最优解对比更新bestT、bestR、bestB
  4. i 循环全部结束,把全局最优解写回输出参数t、r、b

输出bestT有可能和输入t_coarse不一致,这是 i 循环带来的附带副作用,不是该函数的设计目标。

6. awbBayes 主入口

  1. zone 做 R、B 除以 G 归一化;
  2. interpolatePrior 得到插值后的 prior,做经验缩放;
  3. 调用coarseSearch()得到t_coarse
  4. 调用fineSearch()得到优化后的bestT bestR bestB
  5. 换算增益:
  1. 结果存入异步线程缓存。

三、上层后处理

  1. 主线程拷贝异步计算结果;
  2. IIR 一阶帧间平滑,抑制帧与帧之间增益跳变;
  3. gain 做硬件寄存器允许范围的 clamp;
  4. 输出给 ISP 硬件,上报 metadata 色温。

四、代码实现

cpp 复制代码
#include <stdio.h>
#include <math.h>

// ====================== 基础工具、结构体 ======================
typedef struct
{
    double x;
    double y;
} Point2D;

/**
 * 极简PWL分段折线模拟
 * x数组:输入(色温K);y数组:输出;n:控制点数量
 */
typedef struct
{
    double *x;
    double *y;
    int n;
} Pwl;

/** PWL求值,返回插值结果 */
double pwlEval(const Pwl *p, double t)
{
    if (t <= p->x[0]) return p->y[0];
    if (t >= p->x[p->n-1]) return p->y[p->n-1];

    for(int i = 0; i < p->n-1; i++)
    {
        double x0 = p->x[i];
        double x1 = p->x[i+1];
        if(t >= x0 && t <= x1)
        {
            double alpha = (t - x0) / (x1 - x0);
            return p->y[i] * (1.0 - alpha) + p->y[i+1] * alpha;
        }
    }
    return 0.0;
}

/** PWL值域钳位 */
double pwlClamp(const Pwl *p, double t)
{
    if(t < p->x[0]) return p->x[0];
    if(t > p->x[p->n-1]) return p->x[p->n-1];
    return t;
}

/** PWL整体乘以一个标量(原版prior *= scale) */
void pwlScale(Pwl *p, double scale)
{
    for(int i = 0; i < p->n; i++)
        p->y[i] *= scale;
}

/** 三点二次抛物线插值求极小点,对应interpolateQuadatric */
double interpolateQuadatric(Point2D a, Point2D b, Point2D c)
{
    const double eps = 1e-3;
    Point2D ca = {c.x - a.x, c.y - a.y};
    Point2D ba = {b.x - a.x, b.y - a.y};

    double denom = 2.0 * (ba.y * ca.x - ca.y * ba.x);
    if(fabs(denom) < 1e-12)
        return b.x;

    double xmin = a.x - (ba.x*ba.x*(c.y-a.y) - ca.x*ca.x*(b.y-a.y)) / denom;

    if(a.y < c.y - eps)
        return a.x;
    else if(c.y < a.y - eps)
        return c.x;
    return xmin;
}

/** 二维向量运算 */
Point2D pointAdd(Point2D a, Point2D b)
{
    Point2D r = {a.x + b.x, a.y + b.y};
    return r;
}
Point2D pointMul(Point2D a, double s)
{
    Point2D r = {a.x * s, a.y * s};
    return r;
}
double pointLen2(Point2D p)
{
    return p.x*p.x + p.y*p.y;
}
double pointLen(Point2D p)
{
    return sqrt(pointLen2(p));
}

// ===================== AWB数据结构 =====================
/** 每个统计zone,R/G B/G */
typedef struct
{
    double R;
    double B;
} AwbZone;

/** bayes配置,对应yaml */
typedef struct
{
    double coarseStep;
    double transversePos;
    double transverseNeg;
    double deltaLimit;
    double whitepointR;
    double whitepointB;
} AwbBayesConfig;

/**
 * computeDelta2Sum:似然代价
 * gainR/gainB: 候选增益(GainG=1)
 * zones: 有效统计zone数组
 * zoneCnt: 有效zone数量
 * cfg: bayes配置
 */
double computeDelta2Sum(double gainR, double gainB,
                        const AwbZone *zones, int zoneCnt,
                        double wpR, double wpB,
                        const AwbBayesConfig *cfg)
{
    double sum = 0.0;
    for(int i = 0; i < zoneCnt; i++)
    {
        double dr = gainR * zones[i].R - 1.0 - wpR;
        double db = gainB * zones[i].B - 1.0 - wpB;
        double d2 = dr*dr + db*db;
        if(d2 > cfg->deltaLimit)
            d2 = cfg->deltaLimit;
        sum += d2;
    }
    return sum;
}

/**
 * coarseSearch 阶段1:粗搜索,全部候选落在CT‑Curve
 * ctR: t → R/G; ctB: t → B/G
 * prior: t → priorLogLikelihood(-logP(I))
 * tLo tHi: 色温搜索上下限
 * 返回:粗搜索最优色温 tCoarse
 */
double coarseSearch(const Pwl *ctR, const Pwl *ctB, const Pwl *prior,
                    double tLo, double tHi,
                    const AwbZone *zones, int zoneCnt,
                    const AwbBayesConfig *cfg)
{
    Point2D points[256];
    int pointCnt = 0;
    int bestIdx = 0;

    double t = tLo;
    while(1)
    {
        double r = pwlEval(ctR, t);
        double b = pwlEval(ctB, t);
        double gainR = 1.0 / r;
        double gainB = 1.0 / b;

        double delta2Sum = computeDelta2Sum(gainR, gainB, zones, zoneCnt,
                                             cfg->whitepointR, cfg->whitepointB, cfg);
        double tClamp = pwlClamp(prior, t);
        double priorVal = pwlEval(prior, tClamp);
        double finalCost = delta2Sum - priorVal;

        points[pointCnt].x = t;
        points[pointCnt].y = finalCost;
        if(points[pointCnt].y < points[bestIdx].y)
            bestIdx = pointCnt;
        pointCnt++;

        if(fabs(t - tHi) < 1e-6)
            break;

        /* 自适应步长,原版 t += t/10 * coarseStep */
        double step = t / 10.0 * cfg->coarseStep;
        t += step;
        if(t > tHi)
            t = tHi;
    }

    double tBest = points[bestIdx].x;
    if(pointCnt > 2 && bestIdx >= 1 && bestIdx < pointCnt-1)
    {
        Point2D a = points[bestIdx-1];
        Point2D b = points[bestIdx];
        Point2D c = points[bestIdx+1];
        tBest = interpolateQuadatric(a,b,c);
    }
    return tBest;
}

/**
 * fineSearch ------------【原版rpi‑controller核心】
 * 输入:tIn = coarseSearch输出tCoarse;输入输出tOut rOut bOut
 * 注意原版源码行为:
 * 1、transverse向量仅使用tIn附近差分计算**一次,全程固定复用**;
 * 2、i循环生成tTest是附带副作用;作者注释:fineSearch主要目的允许白点离开CT‑Curve,不是微调色温;
 * 3、dist是向量缩放系数,**不计算点到曲线垂直几何距离,无K*d²惩罚;仅硬限制dist区间**;
 * 4、若transverse向量退化,直接返回,不做横向偏移。
 */
void fineSearch(double tIn,
                double *tOut, double *rOut, double *bOut,
                const Pwl *ctR, const Pwl *ctB, const Pwl *prior,
                const AwbZone *zones, int zoneCnt,
                const AwbBayesConfig *cfg)
{
    int spanR_dummy = -1;
    int spanB_dummy = -1;
    (void)spanR_dummy;
    (void)spanB_dummy;

    double step = tIn / 10.0 * cfg->coarseStep * 0.1;
    int nsteps = 5;

    /* 利用输入tIn,计算transverse向量,仅此一次,之后不再重算 */
    double tPlus = tIn + nsteps * step;
    double tMinus = tIn - nsteps * step;
    double rP = pwlEval(ctR, tPlus);
    double bP = pwlEval(ctB, tPlus);
    double rM = pwlEval(ctR, tMinus);
    double bM = pwlEval(ctB, tMinus);

    double rDiff = rP - rM;
    double bDiff = bP - bM;

    Point2D transverse = {bDiff, -rDiff};
    if(pointLen2(transverse) < 1e-6)
    {
        /* 向量退化,直接返回,不做横向求精 */
        *tOut = tIn;
        *rOut = pwlEval(ctR, tIn);
        *bOut = pwlEval(ctB, tIn);
        return;
    }
    /* 归一化为单位向量 */
    double lenT = pointLen(transverse);
    transverse.x /= lenT;
    transverse.y /= lenT;

    double bestCost = 1e20;
    double bestT = tIn;
    double bestR = pwlEval(ctR, tIn);
    double bestB = pwlEval(ctB, tIn);

    double transverseRange = cfg->transverseNeg + cfg->transversePos;
    const int maxNumDeltas = 12;
    int numDeltas = (int)(transverseRange * 100.0 + 0.5) + 1;
    if(numDeltas < 3) numDeltas = 3;
    if(numDeltas > maxNumDeltas) numDeltas = maxNumDeltas;

    /* 源码:nsteps += numDeltas;横向采样越多,tTest扫描范围自动扩大(附带副作用) */
    nsteps += numDeltas;

    /* ======外层i循环:生成tTest;属于附带实现,不是设计主要目标 ====== */
    for(int i = -nsteps; i <= nsteps; i++)
    {
        double tTest = tIn + i * step;
        double tTestClamp = pwlClamp(prior, tTest);
        double priorVal = pwlEval(prior, tTestClamp);

        double rCurve = pwlEval(ctR, tTest);
        double bCurve = pwlEval(ctB, tTest);

        Point2D samp[12];
        int bestJ = 0;
        /* j内层循环:沿【固定transverse向量】做dist缩放采样 */
        for(int j = 0; j < numDeltas; j++)
        {
            double dist = -cfg->transverseNeg + (transverseRange * j) / (numDeltas - 1);
            Point2D curvePt = {rCurve, bCurve};
            Point2D rbTest = pointAdd(curvePt, pointMul(transverse, dist));

            double rTest = rbTest.x;
            double bTest = rbTest.y;
            double gainR = 1.0 / rTest;
            double gainB = 1.0 / bTest;

            double delta2Sum = computeDelta2Sum(gainR, gainB, zones, zoneCnt,
                                                 cfg->whitepointR, cfg->whitepointB, cfg);
            double cost = delta2Sum - priorVal;
            samp[j].x = dist;
            samp[j].y = cost;

            if(samp[j].y < samp[bestJ].y)
                bestJ = j;
        }

        /* 边界保护,保证插值左右点存在 */
        bestJ = (bestJ < 1) ? 1 : bestJ;
        bestJ = (bestJ > numDeltas-2) ? (numDeltas-2) : bestJ;

        /* 抛物线插值,求这条j采样线上最优dist */
        double distOpt = interpolateQuadatric(samp[bestJ-1], samp[bestJ], samp[bestJ+1]);
        /* 钳位dist,不能超出配置的hard‑limit */
        if(distOpt < -cfg->transverseNeg) distOpt = -cfg->transverseNeg;
        if(distOpt > cfg->transversePos) distOpt = cfg->transversePos;

        Point2D curvePt = {rCurve, bCurve};
        Point2D rbOpt = pointAdd(curvePt, pointMul(transverse, distOpt));
        double rOpt = rbOpt.x;
        double bOpt = rbOpt.y;

        /* 【重要】插值点必须完整重算代价,不直接复用samp数组y值 */
        double gainROpt = 1.0 / rOpt;
        double gainBOpt = 1.0 / bOpt;
        double deltaSumOpt = computeDelta2Sum(gainROpt, gainBOpt, zones, zoneCnt,
                                             cfg->whitepointR, cfg->whitepointB, cfg);
        double costOpt = deltaSumOpt - priorVal;

        /* 更新全局最优 */
        if(costOpt < bestCost)
        {
            bestCost = costOpt;
            bestT = tTest;
            bestR = rOpt;
            bestB = bOpt;
        }
    }

    *tOut = bestT;
    *rOut = bestR;
    *bOut = bestB;
}

/**
 * awbBayes 主计算入口(剥离lux插值、bias统计预处理,只保留核心搜索)
 * ctR ctB: CT‑Curve; priorScaled:已经lux插值+经验缩放后的prior PWL
 * tLo tHi: 色温搜索区间
 * zones zoneCnt:预处理完成的zone数组
 * cfg: bayes配置
 * out_gainR out_gainB out_t:输出结果
 */
void awbBayesCore(const Pwl *ctR, const Pwl *ctB, const Pwl *priorScaled,
                  double tLo, double tHi,
                  const AwbZone *zones, int zoneCnt,
                  const AwbBayesConfig *cfg,
                  double *out_gainR, double *out_gainB, double *out_t)
{
    /* 阶段1:粗搜索 */
    double tCoarse = coarseSearch(ctR, ctB, priorScaled, tLo, tHi, zones, zoneCnt, cfg);

    /* 阶段2 fineSearch */
    double tFine, rFine, bFine;
    fineSearch(tCoarse, &tFine, &rFine, &bFine, ctR, ctB, priorScaled, zones, zoneCnt, cfg);

    *out_gainR = 1.0 / rFine;
    *out_gainB = 1.0 / bFine;
    *out_t = tFine;
}

// ===================== 示例main调用演示 =====================
int main(void)
{
    /* -------- 模拟CT‑Curve(普朗克轨迹,极简示例控制点) -------- */
    double ctR_x[] = {2700.0, 3627.0, 5716.0, 8575.0};
    double ctR_y[] = {0.90, 0.75, 0.58, 0.42};
    double ctB_x[] = {2700.0, 3627.0, 5716.0, 8575.0};
    double ctB_y[] = {0.40, 0.54, 0.70, 0.80};
    Pwl ctR = {ctR_x, ctR_y, 4};
    Pwl ctB = {ctB_x, ctB_y, 4};

    /* 模拟prior先验PWL,示例 */
    double priorX[] = {2700, 3627, 5716, 8575};
    double priorY[] = {15.0, 5.0, 5.0, 20.0};
    Pwl prior = {priorX, priorY, 4};
    /* 原版经验缩放:prior *= (有效zone / total zone),这里示例:0.7 */
    pwlScale(&prior, 0.7);

    /* 模拟2个有效统计zone */
    AwbZone zones[] = {
        {0.59, 0.69},
        {0.57, 0.71}
    };
    int zoneCnt = sizeof(zones)/sizeof(zones[0]);

    AwbBayesConfig cfg = {
        .coarseStep = 0.2,
        .transversePos = 0.01,
        .transverseNeg = 0.01,
        .deltaLimit = 0.2,
        .whitepointR = 0.0,
        .whitepointB = 0.0
    };

    double gainR, gainB, tOut;
    awbBayesCore(&ctR, &ctB, &prior,
                 2500.0, 10000.0,
                 zones, zoneCnt,
                 &cfg,
                 &gainR, &gainB, &tOut);

    printf("==== awbBayesCore 输出结果 ====\n");
    printf("estimated CCT(K): %.2f K\n", tOut);
    printf("gainR=%.4f  gainB=%.4f  (gainG=1.0)\n", gainR, gainB);
    return 0;
}

五、原版 rpi‑controller vs libipa 移植版关键差异

对比项 rpi‑controller patch v5‑1 原版 libipa 移植版 AwbBayes
fineSearch 设计目标 主要目标:允许白点跳出 CT‑Curve;色温变化是 i 循环附带副作用 删除 i 循环;t 完全固定等于 coarseSearch 输出,不再扫描色温
transverse 向量 t_coarse位置计算一次,全程固定复用;其他 tTest 只做矢量相加 只使用粗搜点的局部法线,没有多组 tTest
dist/d dist:向量缩放系数;不计算点‑曲线真实垂直距离;无 K・d² 惩罚 d:真实几何垂直距离;代价函数增加 \(K\cdot d^2\)二次惩罚
prior 经验缩放 prior *= valid_zone / total_zone(源码注释存疑,经验调参) 删除该 prior 缩放逻辑
deltaLimit /bias 机制 全部保留 全部保留

提示:网络上很多资料描述的「固定 t、单条法线、\(K\cdot d^2\)距离惩罚」是libipa 移植版本不能套用到这份原始 rpi‑controller 源码

五、数值示例

场景条件

室内,lux=400lux;yaml 关键配置:

  • coarseStep=0.2;transversePos=0.01transverseNeg=0.01deltaLimit=0.2biasProportion=0.05biasCT=4500K
  • auto 模式色温搜索区间:2500‑10000K;
  • ISP 输出 24 个 AWB 网格 region;过滤后有效 zone=16,大于minRegions
  1. prepareStats :过滤无效 region;每个 region 叠加 biasCT 虚拟白点;做sensitivityR/sensitivityB校正。
  2. interpolatePrior:lux=400,在两组 lux‑prior 之间插值;执行 prior 经验缩放。
  3. coarseSearch :2500‑10000K 自适应步长遍历 CT‑Curve;每个 t 计算似然 + 先验代价;三点插值输出粗搜索最优:
  4. fineSearch
    • t_coarse=3720K计算固定 transverse 单位向量;
    • i 循环生成一组tTest(附带扫描,非设计目标);
    • 每个tTest,j 循环沿固定 transverse 向量做 dist 采样,生成可跳出 CT‑Curve 的候选白点;j 方向插值求最优 dist,重算代价;
    • 在全部候选中选出全局最小代价;因 i 循环副作用输出bestT=3660K,白点色度bestR=0.734,bestB=0.556,白点已经离开 CT‑Curve(本函数真正设计目标)
  5. 增益换算:,gainG=1.0。
  6. 主线程:IIR 帧间平滑、硬件限幅,输出 ISP 并上报 metadata 色温。

⚠️注意:bestT=3660K偏离 3720K 属于附带副作用;原始作者设计意图:fineSearch 只用来实现白点偏离普朗克曲线,不是用来微调色温。

相关推荐
一米阳光86611 小时前
软考(中级)软件设计师核心笔记(9)算法——背包问题
笔记·算法·职场发展·软考·软件设计师·中级职称
晚风醉蝶2 小时前
1-11-奇偶排序-OddEvenSort
java·数据结构·算法
旖旎夜光2 小时前
LeetCode 852:山脉数组的峰顶索引(二分查找) —— 题解
数据结构·c++·算法·leetcode·二分查找
白露与泡影3 小时前
解密 Pi 的 Harness 工程:Agent 会话如何实现持久化与恢复
java·人工智能·算法
happyprince5 小时前
01-整体观-Codex全貌解读(源码)
算法·ai编程
俊基科技5 小时前
别自己写算法了,让这颗37.5毫米的模组替你“闭嘴干活”
神经网络·算法·硬件开发·ai降噪·语音模块·回音消除
_Narcissus_6 小时前
动态规划初探(含完整代码实现)
c语言·数据结构·c++·算法·动态规划·背包问题·最短路径
世人万千丶6 小时前
鸿蒙项目实战 - 社区活动编排板:标签云布局算法与自动换行
学习·算法·华为·harmonyos·鸿蒙