工业领域、机器视觉领域线激光器应用十分广泛,线激光质量直接决定了后续检测的精度。本算法可用于从图像角度粗略评估线激光质量,指导激光器的挑选或者调节。

        工业相机激光线质量评估可以用五个字概括: 细、直、亮、稳、匀

线宽(Laser line width)

        图像上激光线横截面的半高全宽(FWHM)或 1/e² 像素数。线越细,后续做亚像素中心提取的精度越高,理想值≤2~3 pixel。

线性度 / 直线度(Linearity or Straightness)

        把整帧激光中心点拟合成一条直线,计算各点到该直线的最大偏差或 RMS 偏差,常用单位 mm 或 pixel。偏差越小,激光线越“直”,系统几何误差越小。

RMS 偏差(Root-Mean-Square Error) 含义:激光中心点到拟合直线的“平均”距离,比最大偏差更能代表整体直线度。

能量均匀性(Intensity uniformity)

        沿激光线长度方向统计灰度峰值或面积的变异系数 CV(σ/μ)。CV≤5 % 认为均匀性良好,可保证不同位置信噪比一致,避免局部过曝或信号太弱。

CV(Coefficient of Variation)定义:标准差 / 均值,无量纲,可用来衡量“亮度沿激光线是否均匀”,也可衡量“面积沿激光线是否均匀”。
面积变异系数:把激光线每 1 列(或 1 行)当成一段,计算该段阈值后的面积,再求面积的 CV。

横截面一致性(Cross-section consistency)

        取线上多段做横截面灰度曲线,比较其峰值、半高宽、对称性。若各截面曲线形状接近,说明激光在 Z 方向(高度)变化时测量灵敏度一致。

业界最常用 Pearson 偏度系数(skewness左右能量比

信噪比(SNR)

        激光中心区域峰值灰度与背景噪声(无激光区 σ)之比。SNR>30 dB 可稳定阈值分割;SNR 过低会放大中心提取随机误差。

激光条纹场景:信号取峰值灰度,噪声取“无激光区域”灰度标准差。

时间稳定性(Temporal stability)

        连续采集 N 帧(通常≥1000 帧),统计中心位置抖动(3σ)和亮度抖动(CV)。抖动越小,后续 3D 轮廓的重复精度越高。

本算法实现除信噪比、时间稳定性以外的其他指标,这是激光图像:

下面是 .hpp文件:

#pragma once
#include <opencv2/opencv.hpp>
#include <vector>
#include <numeric>
#include <cmath>
#include <matplot/matplot.h>
#include <iostream>
#include <algorithm>

struct LaserColData {
    int              idx;      
    double           center;   
    double           fwhm;     
    cv::Mat1f        profile;  
};

class LaserAnalyzerBase {
public:
    explicit LaserAnalyzerBase(const cv::Mat& src, double cropRatio = 0.1)
        : raw_(src.clone()), cropRatio_(cropRatio) { preprocess(); }

    const std::vector<LaserColData>& data() const { return cols_; }
    int                              width() const { return W_; }

protected:
    cv::Mat                    raw_;
    double                     cropRatio_;
    int                        W_;
    std::vector<LaserColData>  cols_;

    void preprocess() {
        int left = static_cast<int>(raw_.cols * cropRatio_);
        int right = raw_.cols - left;
        raw_ = raw_.colRange(left, right).clone();
        W_ = raw_.cols;

        cols_.resize(W_);
        for (int x = 0; x < W_; ++x) {
            LaserColData& d = cols_[x];
            d.idx   = x;
            raw_.col(x).convertTo(d.profile, CV_32F);
            d.center = centroid1D(d.profile);
            d.fwhm   = calcFWHM(d.profile);
        }
    }

    static double centroid1D(const cv::Mat1f& prof) {
        double wSum = 0, wxSum = 0;
        for (int i = 0; i < prof.rows; ++i) {
            double v = std::max(prof(i), 0.0f);
            wSum += v;  wxSum += i * v;
        }
        return wSum > 0 ? wxSum / wSum : 0.0;
    }

    static double calcFWHM(const cv::Mat1f& prof) {
        double minVal, maxVal;
        cv::Point minLoc, maxLoc;
        cv::minMaxLoc(prof, &minVal, &maxVal, &minLoc, &maxLoc);
        float half = (maxVal - minVal) * 0.5f + minVal;
        int left = -1, right = -1;
        double left_value = 0,right_value = 0;
        for (int i = maxLoc.y; i >= 0; --i)      
            if (prof(i) < half) { left  = i; break; }
        for (int i = maxLoc.y; i < prof.rows; ++i) 
            if (prof(i) < half) { right = i; break; }
        left_value = (left*prof(left) + (left + 1)*prof(left + 1))/(prof(left) + prof(left + 1));
        right_value = (right*prof(right) + (right - 1)*prof(right - 1))/(prof(right) + prof(right - 1));
        return (left == -1 || right == -1) ? 
            std::nan("") : static_cast<double>(right_value - left_value);
    }
};

class WidthStats : public LaserAnalyzerBase {
public:
    using LaserAnalyzerBase::LaserAnalyzerBase;
    void compute() {
        double sum = 0, cnt = 0, sq_sum = 0;
        for (const auto& d : data()) {
            if (std::isnan(d.fwhm)) continue;
            cnt++;  sum += d.fwhm;  sq_sum += d.fwhm * d.fwhm;
        }
        mean_ = cnt ? sum / cnt : 0.0;
        double var = cnt ? (sq_sum / cnt - mean_ * mean_) : 0.0;
        stdev_ = std::sqrt(var);
        cv_ = (mean_ > 0) ? stdev_ / mean_ : 0.0;
    }
    void print() const {
        std::cout << "----- 线宽统计 -----\n";
        std::cout << "mean FWHM = " << mean_ << " px\n";
        std::cout << "stddev    = " << stdev_ << " px\n";
        std::cout << "CV        = " << cv_ << "\n";
    }
    void plot() const {
        std::vector<double> v;
        for (const auto& d : data()) if (!std::isnan(d.fwhm)) v.push_back(d.fwhm);
        matplot::hist(v, 30);
        matplot::xlabel("FWHM (px)");
        matplot::ylabel("count");
        matplot::title("Width distribution");
        matplot::show();
    }
private:
    double mean_=0, stdev_=0, cv_=0;
};

class Linearity : public LaserAnalyzerBase {
public:
    using LaserAnalyzerBase::LaserAnalyzerBase;
    void compute() {
        std::vector<cv::Point2f> pts;
        for (const auto& d : data()) pts.emplace_back(static_cast<float>(d.idx), static_cast<float>(d.center));
        cv::fitLine(pts, line_, cv::DIST_L2, 0, 0.01, 0.01);
        double A = line_[1], B = -line_[0], C = line_[0]*line_[3] - line_[1]*line_[2];
        double den = std::sqrt(A*A + B*B);
        dev_.resize(pts.size());
        double sum2 = 0, maxD = 0;
        for (size_t i = 0; i < pts.size(); ++i) {
            double d = std::abs(A*pts[i].x + B*pts[i].y + C) / den;
            dev_[i] = d;  sum2 += d*d;  if (d > maxD) maxD = d;
        }
        rms_ = std::sqrt(sum2 / pts.size());
        maxDev_ = maxD;
    }
    void print() const {
        std::cout << "----- 线性度 -----\n";
        std::cout << "RMS 偏差  = " << rms_ << " px\n";
        std::cout << "Max 偏差  = " << maxDev_ << " px\n";
    }
    void plot() const {
        std::vector<double> x, y;
        for (size_t i = 0; i < dev_.size(); ++i) { x.push_back(i); y.push_back(dev_[i]); }
        matplot::plot(x, y, "b-");
        matplot::xlabel("column");
        matplot::ylabel("deviation (px)");
        matplot::title("Linearity deviation");
        matplot::show();
    }
private:
    cv::Vec4f line_;
    std::vector<double> dev_;
    double rms_=0, maxDev_=0;
};

class PearsonSkew : public LaserAnalyzerBase {
public:
    using LaserAnalyzerBase::LaserAnalyzerBase;
    void compute() {
        skew_.resize(data().size());
        double sum = 0;  size_t cnt = 0;
        for (size_t i = 0; i < data().size(); ++i) {
            skew_[i] = pearsonSkew1D(data()[i].profile);
            sum += skew_[i];  ++cnt;
        }
        meanSkew_ = cnt ? sum / cnt : 0.0;
    }
    void print() const {
        std::cout << "----- 横截面对称性(Pearson) -----\n";
        std::cout << "mean skewness = " << meanSkew_ << "\n";
        std::cout << "|skew|<0.2 对称良好;|skew|>0.5 明显偏斜\n";
    }
    void plot() const {
        std::vector<double> x, y;
        for (size_t i = 0; i < skew_.size(); ++i) { x.push_back(i); y.push_back(skew_[i]); }
        matplot::plot(x, y, "r-");
        matplot::xlabel("column");
        matplot::ylabel("Pearson skew");
        matplot::title("Cross-section symmetry");
        matplot::show();
    }
private:
    std::vector<double> skew_;
    double meanSkew_=0;
    /* 工具:Pearson 偏度 */
    static double pearsonSkew1D(const cv::Mat1f& prof) {
        int n = prof.rows;
        double wSum=0, wxSum=0, w2Sum=0, w3Sum=0;
        for (int i=0;i<n;++i){ double v=std::max(prof(i),0.0f); wSum+=v; wxSum+=i*v; }
        if (wSum==0) return 0.0;
        double mu=wxSum/wSum;
        for (int i=0;i<n;++i){ double v=std::max(prof(i),0.0f), t=i-mu, t2=t*t; w2Sum+=v*t2; w3Sum+=v*t2*t; }
        double sig2=w2Sum/wSum; if (sig2<1e-10) return 0.0;
        double sig=std::sqrt(sig2);
        return (w3Sum/wSum)/(sig*sig2);
    }
};

下面是主函数.cpp调用:

#include "laser_analyzer.hpp"

int main(){
    cv::Mat img = cv::imread("图片路径", cv::IMREAD_GRAYSCALE);
    if (img.empty()) { std::cerr << "❗ 读图失败\n"; return -1; }
    //cv::imshow("laser",img);
    //cv::waitKey(0);

    WidthStats   w(img);   w.compute();  w.print();
    Linearity    l(img);   l.compute();  l.print();
    PearsonSkew  p(img);   p.compute();  p.print();

    return 0;
}

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐