使用最小二乘进行多项式曲线拟合

news2024/11/13 21:36:51

目录

  • 写在前面
  • 曲线拟合方法
    • pcl实现的b样条曲线拟合
    • 最小二乘曲线拟合
      • 原理
      • 代码
      • 注:
      • 结果
  • 参考

写在前面

1、本文内容
使用Eigen进行最小二乘拟合曲线

2、平台/环境
Eigen(open3d), cmake, pcl

3、转载请注明出处:
https://blog.csdn.net/qq_41102371/article/details/131407456

曲线拟合方法

pcl实现的b样条曲线拟合

参考:
https://blog.csdn.net/qq_36686437/article/details/114160557
从效果上看,pcl里面的曲线拟合得到的结果比较差

最小二乘曲线拟合

原理

在二维XOY平面上,曲线由多项式 f ( x ) f(x) f(x)表示:
f ( x ) = a 0 + a 1 x + a 2 x 2 + a 3 x 3 + . . . + a n x n = ∑ i = 0 n a i x i f(x) = a_0+a_1x+a_2x^2+a_3x^3+...+a_nx^n=\sum_{i=0}^n{a_ix^i} f(x)=a0+a1x+a2x2+a3x3+...+anxn=i=0naixi
其中 i ∈ { 0 , 1 , 2... n } i\in\{0,1,2...n\} i{0,1,2...n}是多项式的阶数, a i a_i ai是多项式的各阶系数
有一真实曲线 l l l,由m个点组成 ( x j , y j ) , j ∈ { 0 , 1 , . . . m } (x_j,y_j),j\in\{0,1,...m\} (xj,yj),j{0,1,...m},假设 l l l就是由一条确定的多项式曲线 f ( x ) f(x) f(x)采样得到,那么点都在一定曲线上:
y j = f ( x j ) y_j = f(x_j) yj=f(xj)
但实际上,我们是想通过一条拟合出来的曲线 f ( x ) f(x) f(x)来逼近真实点 y j y_j yj以满足每个真实点都在曲线上或者离曲线很近,设每个点离曲线的距离为:
e j = y j − f ( x j ) e_j = y_j-f(x_j) ej=yjf(xj)
那么我们希望所有真实点离曲线 f ( x ) f(x) f(x)越近越好,则需要最小化所有点到曲线的距离:
m i n x   ∑ j = 0 j = m y j − f ( x j ) \mathop{min}\limits_{x}\ \sum_{j=0}^{j=m}{y_j-f(x_j)} xmin j=0j=myjf(xj)
假设我们以一个二阶多项式 f ( x ) = a 0 + a 1 x 1 + a 2 x 2 f(x) = a_0+a_1x^1+a_2x^2 f(x)=a0+a1x1+a2x2拟合数据 ( x j , y j ) , j ∈ { 0 , 1 , . . . m } (x_j,y_j),j\in\{0,1,...m\} (xj,yj),j{0,1,...m},则有方程组:
{ a 0 + a 1 x 0 + a 2 x 0 2 = y 0 a 0 + a 1 x 1 + a 2 x 1 2 = y 1 . . . a 0 + a j x 1 + a 2 x j 2 = y j . . . a 0 + a 1 x m + a 2 x m 2 = y m \begin{cases} a_0 + a_1x_0+a_2x_0^2=y_0 \\ a_0 + a_1x_1+a_2x_1^2=y_1 \\ ...\\ a_0 + a_jx_1+a_2x_j^2=y_j \\ ...\\ a_0 + a_1x_m+a_2x_m^2=y_m \end{cases} a0+a1x0+a2x02=y0a0+a1x1+a2x12=y1...a0+ajx1+a2xj2=yj...a0+a1xm+a2xm2=ym
写成矩阵形式则为
[ 1 x 0 x 0 2 1 x 1 x 1 2 . . . 1 x m x m 2 ] [ a 0 a 1 a 2 ] = [ y 0 y 1 . . . y m ] \begin{bmatrix} 1 & x_0& x_0^2 \\ 1 & x_1& x_1^2 \\ ... \\ 1 & x_m& x_m^2 \\ \end{bmatrix} \begin{bmatrix} a_0\\a_1\\a_2\\ \end{bmatrix}= \begin{bmatrix} y_0\\y_1\\...\\y_m \end{bmatrix} 11...1x0x1xmx02x12xm2 a0a1a2 = y0y1...ym
这其实是一个非齐次线性方程组 A x = b Ax = b Ax=b的最小二乘解问题,为了不和上面的a和x引起歧义,我们使用 X θ = b X\theta=b =b表示,其最小二乘问题表示为:
m i n θ ∣ ∣ X θ − b ∣ ∣ 2 \mathop{min}\limits_{\theta}||X\theta-b||^2 θmin∣∣b2
b b b就是 y j y_j yj, X X X就是我们的数据:
[ 1 x 0 x 0 2 1 x 1 x 1 2 . . . 1 x m x m 2 ] \begin{bmatrix} 1 & x_0& x_0^2 \\ 1 & x_1& x_1^2 \\ ... \\ 1 & x_m& x_m^2 \\ \end{bmatrix} 11...1x0x1xmx02x12xm2
我们要求的就是系数 [ a 0 , a 1 , a 2 ] T [a_0,a_1,a_2]^T [a0,a1,a2]T X θ = b X\theta=b =b里面的 θ \theta θ
对于 X θ = b X\theta=b =b的解,可以做如下变换:
X T X θ = X T b X^TX\theta=X^Tb XT=XTb
我们假设 X T X X^TX XTX可逆,则有:
θ = ( X T X ) − 1 X T b \theta=(X^TX)^{-1}X^Tb θ=(XTX)1XTb
这里有个问题
1、对于通用最小二乘来说, X T X X^TX XTX可能不可逆,因为 X X X的行向量可能线性相关(详解岭回归与L2正则化),但是在这里,我们注意到每行都有齐次项1,所以当 x 0 ≠ x 1 ≠ . . . ≠ x m x_0 \ne x_1 \ne ...\ne x_m x0=x1=...=xm时, X X X的行向量线性无关,即当数据中无重复点时,我们认为 X T X X^TX XTX可逆,于是通过 θ = ( X T X ) − 1 X T b \theta=(X^TX)^{-1}X^Tb θ=(XTX)1XTb,我们便可以通过解析解直接得到我们想要的 θ \theta θ

2、我们也可以认为 X T X X^TX XTX不可逆那么 θ = ( X T X ) − 1 X T b \theta=(X^TX)^{-1}X^Tb θ=(XTX)1XTb的推导可以参考,并且计算时使用Qr分解:
基于最小二乘法的多项式曲线拟合:从原理到c++实现
最小二乘问题和基于HouseHolder变换的QR分解
一文让你彻底搞懂最小二乘法(超详细推导)

代码

提供两种求解的代码,一种是直接通过 θ = ( X T X ) − 1 X T b \theta=(X^TX)^{-1}X^Tb θ=(XTX)1XTb并利用Eigen的矩阵运算实现,另一种是qr分解。代码目录结构如下,请将least_square_fit_curve.cpp和CMakeLists.txt放入src,然后使用compile.bat和run.bat进行编译和运行,请修改对应PCL对应的路径
在这里插入图片描述

least_square_fit_curve.cpp

#include <iostream>
#include <string>
#include <vector>
#include <random>

#include <pcl/visualization/pcl_visualizer.h>
#include <pcl/visualization/pcl_plotter.h>
#include <pcl/point_types.h>
#include <pcl/io/pcd_io.h>
#include <vtkPolyLine.h>

#include <Eigen/Dense>
#include <Eigen/Core>

std::vector<Eigen::Vector2d> GenerateGaussNoise(const std::vector<Eigen::Vector2d> &points_origin, double mu, double sigma)
{
    std::vector<Eigen::Vector2d> points_noise;
    points_noise = points_origin;
    std::normal_distribution<> norm{mu, sigma};
    std::random_device rd;
    std::default_random_engine rng{rd()};

    for (size_t i = 0; i < points_noise.size(); ++i)
    {
        points_noise[i][0] = points_noise[i][0] + norm(rng);
        points_noise[i][1] = points_noise[i][1] + norm(rng);
    }
    return points_noise;
}

void DisplayLine2D(std::vector<double> vector_1, std::vector<double> vector_2, std::vector<double> coeff, double min_x, double max_x)
{
    pcl::visualization::PCLPlotter *plot_line1(new pcl::visualization::PCLPlotter);
    // std::vector<double> func1(2, 0);
    // func1[0] = c;
    // func1[1] = b;
    // func1[2] = a;
    // plot_line1->addPlotData(func1, point_min.x, point_max.x);
    std::cout << "min_x: " << min_x << " max_x: " << max_x << std::endl;
    plot_line1->addPlotData(coeff, min_x, max_x);
    plot_line1->addPlotData(vector_1, vector_2, "display", vtkChart::POINTS); // X,Y均为double型的向量
    plot_line1->setShowLegend(true);
    plot_line1->plot();
}

std::vector<Eigen::Vector2d> GeneratePolynomialCurve2D(std::vector<double> &coefficients, Eigen::Vector2d range = Eigen::Vector2d(-10, 10), double step = 0.2)
{
    std::cout << "GeneratePolynomialCurve2D" << std::endl;
    // for (auto i : coefficients)
    // {
    //     std::cout << i << std::endl;
    // }
    // std::cout << "range: " << range(0) << " " << range(1) << std::endl;
    std::vector<Eigen::Vector2d> points;
    points.resize(int((range(1) - range(0)) / step));
    double x, y;
    for (std::size_t i = 0; i < points.size(); ++i)
    {
        x = range(0) + step * i;
        y = 0;
        for (std::size_t j = 0; j < coefficients.size(); ++j)
        {
            // y = a_0*x^0 + a_1*x^1 + a_2*x^2 + a_n*x^n
            y += coefficients[j] * std::pow(x, j);
        }
        // std::cout << x << " " << y << "         ";
        points[i] = Eigen::Vector2d(x, y);
    }
    return points;
}

/// @brief 最小二乘拟合,直接用公式
/// @param data_x
/// @param data_y
/// @param coeff 多项式系数 a_0, a_1, ... a_n
/// @param order 需要拟合的阶数
void PolynomailCurveFit(const std::vector<double> &data_x,
                        const std::vector<double> &data_y,
                        std::vector<double> &coeff,
                        int order

)
{
    // Create Matrix Placeholder of size n x k, n= number of datapoints, k = order of polynomial, for exame k = 3 for cubic polynomial
    Eigen::MatrixXd X(data_x.size(), order + 1);
    Eigen::VectorXd b = Eigen::VectorXd::Map(&data_y.front(), data_y.size());
    Eigen::VectorXd A;

    // check to make sure inputs are correct
    assert(data_x.size() == data_y.size());
    assert(data_x.size() >= order + 1);
    // Populate the matrix
    for (size_t i = 0; i < data_x.size(); ++i)
    {
        for (size_t j = 0; j < order + 1; ++j)
        {
            X(i, j) = pow(data_x.at(i), j);
        }
    }
    // std::cout << T << std::endl;

    // Solve for linear least square fit
    A = (X.transpose() * X).inverse() * X.transpose() * b;
    coeff.resize(order + 1);
    for (int k = 0; k < order + 1; k++)
    {
        coeff[k] = A[k];
    }
}

/// @brief 最小二乘拟合,用Qr分解
/// @param data_x
/// @param data_y
/// @param coeff 多项式系数 a_0, a_1, ... a_n
/// @param order 需要拟合的阶数
void PolynomailCurveFitQr(const std::vector<double> &data_x,
                          const std::vector<double> &data_y,
                          std::vector<double> &coeff,
                          int order

)
{
    // Create Matrix Placeholder of size n x k, n= number of datapoints, k = order of polynomial, for exame k = 3 for cubic polynomial
    Eigen::MatrixXd X(data_x.size(), order + 1);
    Eigen::VectorXd b = Eigen::VectorXd::Map(&data_y.front(), data_y.size());
    Eigen::VectorXd A;

    // check to make sure inputs are correct
    assert(data_x.size() == data_y.size());
    assert(data_x.size() >= order + 1);
    // Populate the matrix
    for (size_t i = 0; i < data_x.size(); ++i)
    {
        for (size_t j = 0; j < order + 1; ++j)
        {
            X(i, j) = pow(data_x.at(i), j);
        }
    }
    // std::cout << T << std::endl;
    // Solve for linear least square fit
    A = X.householderQr().solve(b);
    coeff.resize(order + 1);
    for (int k = 0; k < order + 1; k++)
    {
        coeff[k] = A[k];
    }
}
void PrintPolynomailCoeff(const std::vector<double> &coeff, std::string name = "coeff")
{
    std::cout << name << ": ";
    for (auto i : coeff)
    {
        std::cout << i << " ";
    }
    std::cout << std::endl;
}

int main(int argc, char *argv[])
{
    std::chrono::high_resolution_clock::time_point all_s = std::chrono::high_resolution_clock::now();

    Eigen::Vector2d range = Eigen::Vector2d(-10, 10);
    std::vector<double> coeff_gt({1, 2, 4});
    std::vector<double> coeff_fit;
    std::vector<double> coeff_fit_qr;
    auto points_curve = GeneratePolynomialCurve2D(coeff_gt, range);
    std::vector<double> points_x;
    std::vector<double> points_y;
    // 添加噪声
    auto points_curve_noise = GenerateGaussNoise(points_curve, 0, 0.05);
    // 不添加噪声
    // auto points_curve_noise = points_curve;
    for (auto i : points_curve_noise)
    {
        points_x.push_back(i(0));
        points_y.push_back(i(1));
    }
    PolynomailCurveFit(points_x, points_y, coeff_fit, 2);
    PolynomailCurveFitQr(points_x, points_y, coeff_fit_qr, 2);
    PrintPolynomailCoeff(coeff_gt, "coeff_gt");
    PrintPolynomailCoeff(coeff_fit, "coeff_fit");
    PrintPolynomailCoeff(coeff_fit_qr, "coeff_fit_qr");

    std::chrono::high_resolution_clock::time_point all_e = std::chrono::high_resolution_clock::now();
    auto all_cost = std::chrono::duration_cast<std::chrono::milliseconds>(all_e - all_s).count();
    std::cout << "all cost: " << all_cost << " ms" << std::endl;

    DisplayLine2D(points_x, points_y, coeff_gt, range(0), range(1));
    DisplayLine2D(points_x, points_y, coeff_fit, range(0), range(1));
    DisplayLine2D(points_x, points_y, coeff_fit_qr, range(0), range(1));

    return 0;
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.18)

project(LeastSquareFitCurve)

find_package(PCL 1.8 REQUIRED)
include_directories(${PCL_INCLUDE_DIRS})
link_directories(${PCL_LIBRARY_DIRS})
add_definitions(${PCL_DEFINITIONS})

add_executable(least_square_fit_curve ./least_square_fit_curve.cpp)
target_link_libraries(least_square_fit_curve ${PCL_LIBRARIES})

compile.bat

cmake -S ./src -B ./build
cmake --build ./build --parallel 8

run.bat

set PATH=D:\carlos\install\PCL 1.10.0\bin;D:\carlos\install\PCL 1.10.0\3rdParty\FLANN\bin;D:\carlos\install\PCL 1.10.0\3rdParty\VTK\bin;D:\carlos\install\PCL 1.10.0\3rdParty\Qhull\bin;D:\carlos\install\PCL 1.10.0\3rdParty\OpenNI2\Tools;%PATH%

.\build\Release\least_square_fit_curve.exe

编译:

cd least_square_fit_curv
./compile.bat

运行

run.bat

注:

其中使用了pcl的曲线绘制,而pcl自带Eigen库,可以使用Open3d和pcl里面提供的Eigen,如果没有pcl和open3d,可以直接安装Eigen,并且把pcl可视化的部分注掉,单独安装Eigen参考:
cmake+Eigen库

qr的参考实现:
最小二乘问题和基于HouseHolder变换的QR分解
使用 C++ Eigen 包的最小二乘多项式拟合
直接矩阵求逆做的代码参考:
3D 空间中拟合曲线
C++/PCL:最小二乘拟合平面直线,平面多项式曲线,空间多项式曲线
C++ 最小二乘法 直线拟合、曲线拟合、平面拟合、高斯拟合

结果

用代码手动生成了二次曲线 f ( x ) = 1 + 2 x + 4 x 2 f(x) = 1+2x+4x^2 f(x)=1+2x+4x2的采样点,不添加噪声的情况下,拟合结果和GT一致
系数:
在这里插入图片描述
原始点和曲线:
在这里插入图片描述因为系数一致,拟合结果曲线和原始曲线一致,不展示

添加高斯噪声的情况下:
系数:
在这里插入图片描述
原始点和曲线:
在这里插入图片描述
拟合结果曲线:
在这里插入图片描述
可以看出直接法和qr分解得到的结果是一致的

参考

3D曲线1:多项式曲线 https://zhuanlan.zhihu.com/p/267985141
定方程的求解、最小二乘解、Ax=0、Ax=b的解,求解齐次方程组,求解非齐次方程组(推导十分详细)
生成高斯噪声https://zhuanlan.zhihu.com/p/458994530
其余文中已列出

主要做激光/影像三维重建,配准/分割等常用点云算法,技术交流、咨询可私信

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/735942.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Git】Github 上传文件到远程仓库时,经常发生网络错误,一个比较稳定的连接方法及我的示例

文章目录 一、问题导读二、完整的一个流程2.1 初始化2.2 从远程仓库拉取最新的更改并合并到当前分支2.3 远程仓库的 SSH URL2.4 添加到暂存区2.5 提交操作2.6 将一个远程仓库添加为 Git 仓库的远程别名2.7 推送到远程仓库2.8 最后的结果 三、HTTP和SSH的理解3.1 两者的区别3.1.…

【网络系统集成】网络认证实验

1.实验名称 网络认证实验 2.实验目的 学习网络认证配置 3.实验内容 3.1拓扑结构图 3.2地址分配 <

OpenCV实战(28)——光流估计

OpenCV实战&#xff08;28&#xff09;——光流估计 0. 前言1. 光流估计原理2. 光流算法实现3. 完整代码小结系列链接 0. 前言 当相机进行拍摄时&#xff0c;拍摄到的亮度图案会投射到图像传感器上&#xff0c;从而形成图像。在视频序列中&#xff0c;我们通常需要捕捉运动模式…

HCIP--OSPF实验1

1、合理规划IP地址&#xff0c;启用ospf单区域 2、R1-R2之间启用ppp的单向认证 3、R2-R3之间启用ppp的chap认证 4、R3-R5-F6之间使用MGRE&#xff0c;R3为hub端&#xff0c;R5,R6为spoke端&#xff1b; 要求MGRE接口网络型为BMA&#xff0c;spoke之间通信必须经过hub端 5、全…

Linux--进程

什么叫做进程&#xff1f; 程序加载到内存就叫进程&#xff08;看不懂是吧&#xff0c;看下面更详细一些&#xff09; 进程对应的代码和数据进程对应的PCB结构体

MySQL索引原理和优化

目录 1 什么是索引&#xff1f;1.1 引言1.2 索引原理1.3 索引分类1.3.1 主键索引1.3.2 普通索引&#xff08;单列索引&#xff09;1.3.3 复合索引&#xff08;组合索引&#xff09;1.3.4 唯一索引1.3.5 全文索引1.3.6 索引的查询和删除 1.4 索引的优缺点 2 索引数据结构2.1 Has…

做题遇见的PHP函数汇总

mb_substr函数 mb_substr() 函数返回字符串的一部分&#xff0c;之前学过 substr() 函数&#xff0c;它只针对英文字符&#xff0c;如果要分割的中文文字则需要使用 mb_substr() 语法&#xff1a; mb_substr ( $str ,$start [, $length NULL [, $encoding mb_encoding() ]] …

改进版简化路径。

美图 在原有的基础上增加对 cd - 的处理。 在 Unix 命令中&#xff0c;cd - 表示返回上一次所在的目录。我们可以使用一个变量来记录上一次所在的目录&#xff0c;在遇到 cd - 时将当前目录设置为上一次所在的目录。 以下是增加对 cd - 的处理后的代码&#xff1a; 4 cd /…

016 - STM32学习笔记 - SPI读写FLASH(一)

016 - STM32学习笔记 - SPI访问Flash&#xff08;一&#xff09; 之前csdn的名称是宥小稚&#xff0c;后来改成放学校门口见了&#xff0c;所以前面内容看到图片水印不要在意&#xff0c;都是自己学习过程中整理的&#xff0c;不涉及版权啥的。 1、什么是SPI&#xff1f; SP…

Linux项目自动化构建工具-make/Makefile以及git三板斧

目录 一、关于make/makefile的背景知识二、依赖关系和依赖方法三、make/makefile如何书写&#xff1f;四、文件的三个时间(Access、Modify、Change)五、Linux下倒计时和进度条代码的书写5.1 回车换行5.2 缓冲区5.3 倒计时代码实现5.4 进度条代码实现 六、git三板斧6.1 什么是gi…

10.15资源加载

定义&#xff1a; 1.直接属性引用 生成一个actor和声音&#xff1a; 运行时就会产生一个Myactor中设置的Actor并且播放Myactor中设置的声音。 音频&#xff1a;class USoundCue&#xff1b; 纹理&#xff1a;class UTexture&#xff1b; 材质&#xff1a; class UMaterial 模…

TCP/IP出现的背景及其历史【图解TCP/IP(笔记八)】

文章目录 TCP/IP出现的背景及其历史从军用技术的应用谈起ARPANET的诞生TCP/IP的诞生UNIX系统的普及与互联网的扩张商用互联网服务的启蒙 TCP/IP出现的背景及其历史 从军用技术的应用谈起 20世纪60年代&#xff0c;很多大学和研究机构都开始着力于新的通信技术。其中有一家以美…

jmeter列表数据断言

在jmeter接口请求中&#xff0c;通常需要根据接口data列表有无返回的数据断言是接口请求成功&#xff0c;如图1&#xff0c; 通常有这么几种方法&#xff1a; beanshell断言 json断言 响应断言 图1&#xff1a; 失败请求&#xff1a;{"code":0,"msg"…

小甲鱼- python -洗牌算法 —— Fisher-Yates

练习1 自己的原始代码 &#xff08;比较复杂&#xff09; 1.没有把字符串转为列表&#xff0c;所以不能利用pop # 打乱的次数 for i in range(1,4):s "ABCDEF"n 4list []l len(s)while l > 0:k random.randint(1, l)list.append(s[k-1])s s.replac…

电子设备电池容量与充电器功率的关系

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhang.cn] 目录 抛出问题 手机的工作电压 手机的工作电流 手机的电池容量 电能转换公式 充电器功率 充电时间计算 总结 抛出问题 你是否也想过&#xff0c;你的手机电池容量是5000mAh&#xff0c;手机充电器是120W快…

基于低代码平台的项目设计的一般流程及低代码平台(基于iVX)与MVC的关系

基于低代码平台的项目设计的一般流程及低代码平台&#xff08;基于iVX&#xff09;与MVC的关系 1.基于低代码平台的项目设计的一般流程a.流程图b.MVC架构应用于iVX项目的各分层排序&#xff1a;&#xff08;1&#xff09;第一步&#xff1a;写M&#xff08;2&#xff09;第二步…

LeetCode[912]排序数组

难度&#xff1a;Medium 题目&#xff1a; 给你一个整数数组 nums&#xff0c;请你将该数组升序排列。 示例 1&#xff1a; 输入&#xff1a;nums [5,2,3,1] 输出&#xff1a;[1,2,3,5]示例 2&#xff1a; 输入&#xff1a;nums [5,1,1,2,0,0] 输出&#xff1a;[0,0,1,1,2,…

Openlayers实战:显示海量数据

Openlayers地图中通常的加载方式是canvas,另外还一种加载方式是webGL,在绘制海量数据时,使用GPU进行绘制可有效减少CPU的负载,提升绘制时的速度在浏览器中,可以使用WebGL的方式与GPU交互。 在本实战中,使用WebGLPoints显示海量数据。 效果图 源代码 /* * @Author: 大剑…

表单标签from

七、表单标签 form text name属性必须添加&#xff0c;否则后端不知道这个值是什么意思。且name不能重复 添加label主要是方便程序员&#xff0c;一看到用户名称这个label就是为username的&#xff0c;添加或者不添加页面效果一样 2、possword 用户密码显式出来&#xff0c;所…

与一款医疗仪器的往事

这是一款比较冷门的医疗仪器&#xff0c;SOD型多普勒脐动脉血流检测仪。 大约是20年前&#xff0c;有个朋友找到了俺&#xff0c;让俺给他写一个医疗仪器的软件。当时这个仪器是这样子的。这是唯一还能找到的当时的图片。朋友要求用C写。俺就选择了C Builder&#xff0c;比用VC…