资讯动态

MATLAB曲线图片识别:像素映射、坐标标定与误差评估

发布时间:2026/10/26 5:07:36 来源:尧图企业网站定制
简介「曲线图片识别程序」是一份面向科研人员与MATLAB学习者的图像数据处理工具包用于从图片中自动提取曲线坐标省去对照论文图形重写代码的麻烦。整套流程覆盖灰度化与二值化预处理、Canny等边缘检测、连通区域标记与轮廓追踪、polyfit曲线拟合以及plot绘图对比与imread/imwrite文件读写末尾还留有GUI交互与算法提速的扩展空间。压缩包共14个文件、约459KB主体为12张png示例图片配合1个mat数据文件和1个m脚本可直接跑通识别流程并对照样例验证效果。已有2019人学习下载适合需要复现文献曲线、做数据对比的中高级使用者参考。1. 一张曲线截图能不能直接变数据曲线图片识别程序的落地边界把一张论文截图里的曲线还原成可计算的数值是曲线图片识别程序最典型的用途。很多人第一反应是找现成软件其实在 matlab 里用图像处理工具箱搭一条最小流程半小时就能跑出第一版数据。这个资源适合手里只有图片、没有原始数据的学生和工程师也适合需要批量复现实验曲线的人。它解决的不是“读图”本身而是把像素位置映射回坐标轴数值再导出成可拟合、可对比的 CSV。别把它当成万能 OCR网格、图例和抗锯齿都会让结果变玄学先把边界认清再动手。适合谁能看懂 MATLAB 脚本、愿意花十分钟标定坐标轴、对误差有基本判断的人。不适合谁指望一键导入就得到完美数据、从不检查叠图的人。2. 从像素到数值MATLAB 曲线识别的主流程与四个函数选型曲线图片识别程序的核心不是某一个神奇函数而是一条从读图到导出的顺序链。我的习惯是先把“图像处理”和“坐标映射”拆开前半段只关心怎么把曲线从背景里剥出来后半段只关心像素点怎么变成数据点。拆开以后哪一步翻车都能单独回看不用在一大段脚本里猜黑匣子。常见做法是读图、转灰度、二值化、去噪、骨架化、取点、标定、映射、导出这九步里每一步都有可调参数但真正影响结果的只有通道选择、阈值、形态学核和坐标锚点。2.1 读图、转灰度与通道选择先确认曲线和网格是不是同一层信息直接rgb2gray最省事但它会把颜色对比度压扁。如果曲线是红色、网格是灰色灰度化后两者可能黏在一起后面阈值再调也分不开。我一般先看原图的通道分布再决定是走灰度还是走 HSV 分割。读图时用imfinfo看一眼位深和透明通道避免 PNG 的 alpha 通道在叠加后产生灰边。% 读取曲线截图保留原始信息统一转 double 便于后续运算 img imread(curve_demo.png); info imfinfo(curve_demo.png); fprintf(位深: %d, 颜色类型: %s\n, info.BitDepth, info.ColorType); gray rgb2gray(img); grayD im2double(gray); % 预览灰度图先肉眼判断曲线与网格是否分离 figure; imshow(gray); title(灰度预览先看曲线和网格是否粘连);这段代码的作用是建立统一入口。imread支持 png、jpg、bmp 等常见格式im2double把像素归一到 0 到 1避免后面做减法和滤波时 uint8 溢出。imfinfo不是必须但遇到透明背景图时很有用ColorType为truecolor且带 alpha 时最好先合成白底再处理。参数上唯一需要留意的是图片路径批量处理时不要用中文路径和空格混排MATLAB 对路径中的特殊字符并不总是友好。如果曲线是彩色而网格是灰色我会先做颜色分离而不是直接灰度。下面这段用 HSV 空间把高饱和度像素挑出来适合红、蓝、绿曲线。阈值不是固定的S通道阈值 0.35 只是起点实际要看直方图。% 彩色曲线优先走 HSV把高饱和度区域作为候选曲线 hsv rgb2hsv(img); S hsv(:, :, 2); V hsv(:, :, 3); % S 阈值控制“多彩算曲线”V 阈值排除过暗背景 curveMask (S 0.35) (V 0.25); % 去掉零碎噪点保留主要曲线 curveMask bwareaopen(curveMask, 50); figure; imshow(curveMask); title(HSV 饱和度分割候选曲线);逻辑说明S是饱和度曲线颜色越纯S越高V是明度用来排除黑色文字和深色边框。bwareaopen的面积阈值 50 需要按图片分辨率调整分辨率翻倍时面积阈值大约按平方放大。这个方案对彩色曲线有效对黑白曲线不适用黑白图还是回到灰度加 Otsu 的路线。选型理由很简单颜色信息是额外的维度能用就用不能用再退回灰度不要一上来就把彩色图压成灰度。2.2 阈值、形态学与连通域把“线”从背景里剥出来二值化是曲线图片识别程序里最容易“看起来能跑、实际全是毛刺”的一步。graythresh给的是全局 Otsu 阈值适合背景均匀、光照一致的截图如果图里有渐变背景或阴影全局阈值会把一半网格留下来。我的做法是先跑 Otsu 看效果再决定要不要局部自适应。二值化之后不要急着骨架化先做一次面积开运算和闭运算把孤点去掉、把小断点接上。% 灰度路线Otsu 全局阈值再按曲线深浅决定是否取反 level graythresh(grayD); bw imbinarize(grayD, level); % 曲线通常是深色背景浅色这里取反让曲线为 1 bw ~bw; % 去掉小面积噪点面积阈值按最小曲线片段估算 bw bwareaopen(bw, 30); % 闭运算连接 1 到 2 像素的断点开运算去掉毛刺 seClose strel(disk, 2); seOpen strel(square, 2); bw imclose(bw, seClose); bw imopen(bw, seOpen); figure; imshow(bw); title(二值化与形态学清理后的曲线候选);参数说明graythresh返回 0 到 1 的归一化阈值imbinarize直接使用bwareaopen的 30 是经验值如果曲线很细、分辨率很高30 可能把曲线本身切断这时要降到 10 左右。strel(disk, 2)的半径 2 适合 1 到 3 像素宽的曲线半径太大曲线会糊成块骨架化后位置偏移明显。imclose先闭后开不是固定顺序断点多就先闭毛刺多就先开两个都严重时分别调小核半径不要指望一个大核解决所有问题。连通域筛选是第二道保险。二值图里留下来的不只有曲线还有坐标轴、刻度、图例文字。regionprops可以按长宽比、面积、欧拉数筛掉明显不是曲线的东西。曲线通常细长长宽比大文字块面积小、长宽比接近 1坐标轴是水平或垂直的长条和曲线方向不同。% 连通域筛选保留细长且面积不过小的候选 cc bwconncomp(bw); stats regionprops(cc, Area, BoundingBox, MajorAxisLength, MinorAxisLength); keep false(cc.NumObjects, 1); for k 1:cc.NumObjects area stats(k).Area; major stats(k).MajorAxisLength; minor stats(k).MinorAxisLength; ratio major / max(minor, eps); % 面积下限排除文字长宽比下限排除方块状图例 if area 80 ratio 3 keep(k) true; end end bwCurve false(size(bw)); bwCurve(vertcat(cc.PixelIdxList{keep})) true; figure; imshow(bwCurve); title(连通域筛选后的曲线区域);逻辑说明bwconncomp把二值图拆成独立连通块regionprops给出每个块的面积、外接矩形和长短轴。ratio 3是“细长”的粗略门槛曲线弯折厉害时MajorAxisLength不一定大所以这个条件只适合做粗筛不要当成硬规则。面积下限 80 同样依赖分辨率批量处理时最好用图片总像素的比例来设比如area 0.0001 * numel(bw)。这一步的目标不是完美而是把明显不是曲线的块去掉剩下的交给骨架和排序。2.3 坐标轴四点标定像素坐标到数据坐标的仿射变换曲线识别最关键的参数不是阈值而是坐标锚点。没有标定骨架点只是像素坐标导出 CSV 也没有物理意义。最小标定需要四个点x 轴左端、x 轴右端、y 轴下端、y 轴上端通常 x 轴左端和 y 轴下端是同一个轴交点。手动取点用ginput最稳自动检测刻度线适合批量但容易把网格线误认成刻度。我的习惯是第一张图一定手动取点把比例因子记下来后面同版式的图再用自动或复用。% 手动取四个锚点按 x 轴左端、x 轴右端、y 轴下端、y 轴上端的顺序点 figure; imshow(img); title(依次点击x 轴左端、x 轴右端、y 轴下端、y 轴上端); [xpix, ypix] ginput(4); % 对应的数据坐标按图上的刻度填写 xData [0, 10]; % x 轴起止值 yData [0, 5]; % y 轴起止值 % 像素差与数据差的比例单位是 pixel/unit kx (xpix(2) - xpix(1)) / (xData(2) - xData(1)); ky (ypix(3) - ypix(4)) / (yData(2) - yData(1)); fprintf(kx %.4f pixel/unit, ky %.4f pixel/unit\n, kx, ky);逻辑说明ginput(4)会暂停脚本等待鼠标点击点的顺序必须固定否则后面索引会乱。kx用 x 轴两端像素差除以数据差ky用 y 轴下端减上端因为图像 y 轴向下增长而数据 y 轴向上增长。参数上要注意如果 x 轴不是从 0 开始xData要填实际起止值如果坐标轴是对数轴这个线性映射不成立需要先做对数变换。手动取点的误差一般在一到两个像素换算到数据坐标后是否可接受要看你的曲线精度要求。2.4 主流程骨架一个可跑的曲线识别脚本把前面的步骤串起来就是一个最小可跑的曲线图片识别程序。这个版本不追求自动排序只负责把骨架点映射成数据点并导出适合先验证“这套图能不能识别”。真正要用的时候排序和断点修复放在下一章处理。% 最小主流程读图 - 灰度 - 二值化 - 去噪 - 骨架 - 映射 - 导出 img imread(curve_demo.png); gray rgb2gray(img); grayD im2double(gray); level graythresh(grayD); bw ~imbinarize(grayD, level); bw bwareaopen(bw, 30); bw imclose(bw, strel(disk, 2)); bw imopen(bw, strel(square, 2)); % 骨架化Inf 表示反复细化到不再变化 skel bwmorph(bw, skel, Inf); % find 返回 [行, 列]转成 [x, y] 像素坐标 [y, x] find(skel); pts [x, y]; % 使用上一节手动标定得到的锚点 x1 xpix(1); x2 xpix(2); y1 ypix(4); y2 ypix(3); x0 xData(1); xN xData(2); y0 yData(1); yN yData(2); data zeros(size(pts)); data(:, 1) x0 (pts(:, 1) - x1) * (xN - x0) / (x2 - x1); data(:, 2) y0 (y2 - pts(:, 2)) * (yN - y0) / (y2 - y1); writematrix(data, curve_points_raw.csv); fprintf(导出 %d 个骨架点\n, size(data, 1));逻辑说明bwmorph(bw, skel, Inf)把粗线细化成单像素骨架Inf表示一直迭代到稳定。find的顺序是按列扫描得到的点没有沿曲线排序直接画出来会像散点所以这个版本只适合检查覆盖范围不适合直接拟合。映射公式里 x 方向用(pts(:,1)-x1)/(x2-x1)y 方向用(y2-pts(:,2))/(y2-y1)是因为图像 y 向下、数据 y 向上。参数上x1、x2来自手动点击x0、xN来自图上的刻度值这两组数必须对应同一根轴不要拿 x 轴锚点去算 y 轴。3. 坐标轴标定与像素映射两条轴、四个锚点和比例因子的确定方法坐标标定是曲线图片识别程序里最值得花时间的一步。阈值调坏了还能重跑锚点取错了后面所有数据整体平移或缩放肉眼还不一定看得出来。我见过最常见的翻车是x 轴锚点取在绘图区边缘而不是刻度线起点导致整条曲线往左偏半个刻度y 轴锚点取在轴线上端而不是最高刻度导致幅值整体压缩。这一章把标定拆成手动、自动、多曲线分组和残差校验四件事每件事都给可执行的写法。3.1 手动取点与自动检测刻度线什么时候该信人工第一张图一定手动取点这不是保守是成本最低的验证。自动检测刻度线听起来高级但它依赖刻度线比曲线更规则、更细、更短一旦图里有网格线、次刻度、图例边框检测结果很容易跑偏。手动取点的代价是每次点击四下换来的是比例因子可控。批量处理时如果所有图版式一致可以把第一张图的锚点像素保存下来复用如果版式有缩放就要按图片尺寸比例换算不能直接套。% 保存手动标定结果供同版式图片复用 calib.xpix xpix; calib.ypix ypix; calib.xData xData; calib.yData yData; save(calib_demo.mat, calib); % 复用前先检查图片尺寸是否一致 newImg imread(curve_demo_02.png); if ~isequal(size(newImg, 1:2), size(img, 1:2)) warning(图片尺寸不一致锚点需要按比例换算或重新取点); end逻辑说明calib结构体保存四个锚点的像素坐标和对应数据坐标save成 mat 文件方便下一张图加载。复用前比较size(newImg, 1:2)和原图尺寸尺寸不一致时不能直接套因为像素比例变了。常见做法是按宽高比例缩放锚点xpix_new xpix * width_new / width_old但前提是绘图区在图片中的相对位置没变。如果截图被裁剪过比例换算也会失效这种情况老老实实重新点四下。自动检测刻度线可以作为辅助不适合作为唯一依据。思路是先检测水平线和垂直线再按长度和位置筛选。下面这段用形态学开运算提取长直线只适合演示筛选逻辑实际参数要按图调。% 自动提取水平长线辅助定位 x 轴和网格线 bwInv ~imbinarize(grayD, graythresh(grayD)); horiz imopen(bwInv, strel(line, 40, 0)); vert imopen(bwInv, strel(line, 40, 90)); % 按连通域长度筛选保留接近整幅图宽度的水平线 ccH bwconncomp(horiz); statsH regionprops(ccH, BoundingBox); for k 1:ccH.NumObjects w statsH(k).BoundingBox(3); if w 0.6 * size(bwInv, 2) fprintf(候选水平轴线宽度 %.1f 像素\n, w); end end逻辑说明strel(line, 40, 0)是长度 40、角度 0 度的线结构元素用来保留水平长线角度 90 度保留垂直线。0.6 * size(bwInv, 2)是“接近整幅图宽度”的阈值适合坐标轴横跨大半张图的情况。这个检测只能给出候选不能直接当锚点因为网格线也可能满足条件。我一般用它来确认“x 轴大概在哪一行”再回到手动取点确认左右端点。3.2 仿射变换的矩阵写法与轴向反转两点比例映射适合正放的坐标轴遇到轻微旋转或非正交轴仿射变换更稳。仿射矩阵可以把像素坐标[x, y, 1]映射到数据坐标[X, Y, 1]用三个锚点就能解。常见做法是用fitgeotrans或手工解线性方程前者更省事后者更透明。我的习惯是手工写一遍知道矩阵里每个数在干什么再用affine2d做批量变换。% 用三个锚点解仿射矩阵像素坐标 - 数据坐标 % 锚点顺序x 轴左端、x 轴右端、y 轴上端 P [xpix(1), ypix(1), 1; xpix(2), ypix(2), 1; xpix(4), ypix(4), 1]; Q [xData(1), yData(1); xData(2), yData(1); xData(1), yData(2)]; % 解 3x3 仿射矩阵最后一行为 [0 0 1] A P \ [Q, ones(3, 1)]; tform affine2d(A); % 批量变换骨架点 [xr, yr] transformPointsForward(tform, pts(:, 1), pts(:, 2)); dataAffine [xr, yr];逻辑说明P是像素坐标齐次矩阵Q是对应数据坐标P \ [Q, ones(3,1)]解出 3x3 矩阵A。affine2d把它包装成变换对象transformPointsForward对任意数量的点做正向映射。参数上要注意锚点不能共线三个点要覆盖 x 和 y 两个方向如果 y 轴方向反了Q里的yData顺序要对应上端和下端不要照抄。仿射变换能处理轻微旋转但如果坐标轴弯曲或透视明显线性仿射不够用需要透视变换或多段标定。轴向反转是另一个高频问题。图像坐标 y 向下数据坐标 y 向上直接用y_data y_pix * ky会得到倒过来的曲线。正确写法是y_data y0 (y_bottom - y_pix) * (yN - y0) / (y_bottom - y_top)其中y_bottom是 y 轴下端像素y_top是上端像素。这个式子看起来长但把它写进函数后就不容易错。% 封装像素转数据函数固定轴向反转逻辑 toData (p) [xData(1) (p(:, 1) - xpix(1)) * (xData(2) - xData(1)) / (xpix(2) - xpix(1)), ... yData(1) (ypix(3) - p(:, 2)) * (yData(2) - yData(1)) / (ypix(3) - ypix(4))]; dataXY toData(pts);逻辑说明匿名函数toData接收 N×2 的像素点矩阵返回 N×2 的数据点矩阵。x 方向用左端锚点和右端锚点做线性插值y 方向用下端减当前像素再除以下端减上端。参数xpix(1)、xpix(2)、ypix(3)、ypix(4)必须来自同一张图的手动标定不能混用不同图片的锚点。这个函数适合正放坐标轴遇到旋转图还是用仿射矩阵。3.3 多曲线图的分组颜色分割和连通域筛选一张图里有多条曲线时先分组再取点不要把所有骨架点混在一起导出。分组依据优先看颜色其次看线型最后看空间位置。颜色分组用 HSV 最直接给每条曲线一个饱和度或色相范围黑白图只能靠连通域和位置交叉点会把两条曲线粘成一个连通块这时需要手动框选区域或按列扫描分段。% 按色相范围分三条彩色曲线适合红、绿、蓝区分明显的图 hsv rgb2hsv(img); H hsv(:, :, 1); S hsv(:, :, 2); V hsv(:, :, 3); maskRed (H 0.05 | H 0.95) S 0.4 V 0.2; maskGreen (H 0.25 H 0.45) S 0.4 V 0.2; maskBlue (H 0.55 H 0.75) S 0.4 V 0.2; % 每条曲线单独去噪、骨架化 curves {maskRed, maskGreen, maskBlue}; names {red, green, blue}; for k 1:numel(curves) m bwareaopen(curves{k}, 40); sk bwmorph(m, skel, Inf); [yy, xx] find(sk); fprintf(%s: %d 个骨架点\n, names{k}, numel(xx)); end逻辑说明色相H在 0 到 1 之间循环红色分布在 0 附近和 1 附近所以要写H 0.05 | H 0.95绿色大约在 0.33蓝色大约在 0.66。S 0.4排除灰色网格V 0.2排除黑色文字。每组单独bwareaopen和bwmorph避免不同颜色的曲线互相干扰。参数不是固定的截图色偏严重时先取一个像素看 HSV 值再定范围。黑白多曲线图不适合这段得用连通域加手动区域。3.4 用已知网格做残差校验标定完不要直接导出先拿图上的已知网格做一次残差校验。找几个网格交点用同一套映射算它们的像素坐标对应数据坐标再和刻度值比较误差超过半个小刻度就说明锚点或映射有问题。这个步骤花两分钟能省掉后面几个小时的对不上。% 选三个已知网格交点手动记录像素坐标和真实数据坐标 checkPix [300, 500; 500, 500; 500, 300]; checkTrue [3.0, 2.0; 5.0, 2.0; 5.0, 4.0]; checkPred toData(checkPix); residual checkPred - checkTrue; rmse sqrt(mean(residual(:).^2)); fprintf(网格校验 RMSE %.4f\n, rmse); % 误差超过半个小刻度就回查锚点 if rmse 0.05 warning(标定残差偏大检查锚点是否取在刻度线起点); end逻辑说明checkPix是网格交点的像素坐标checkTrue是对应刻度值toData复用前面的映射函数。residual是预测值减真实值rmse是整体误差。阈值 0.05 只是示例实际按你的数据精度要求设如果最小刻度是 0.1残差超过 0.05 就值得回查。常见误差来源是锚点取在轴线边缘而不是刻度线中心或者 x 轴起点不是 0 但xData填了 0。4. 曲线骨架提取与断点修复把粗线变成有序点列骨架化只是把粗线变细得到的点还是无序的。直接导出find的结果CSV 里是一堆散点拟合时曲线会自己连来连去。这一章解决三件事怎么得到单像素骨架、怎么把点按 x 或按路径排好、断点和交叉点怎么修。曲线图片识别程序能不能用于拟合差别主要在这里而不是阈值那一步。4.1 二值图细化与骨架化bwmorph 的用法和参数边界bwmorph的skel和thin都能细化区别是skel保留拓扑结构thin更接近中轴。曲线识别一般用skel因为后面要按连通路径走。迭代次数写Inf表示细化到稳定写具体数字会提前停止适合已知线宽的情况。参数边界在于线宽超过 5 像素时骨架化后可能出现小分支线宽只有 1 像素时骨架化基本不变但二值化噪声会直接变成假分支。% 对比 skel 和 thin 的骨架结果 skel bwmorph(bwCurve, skel, Inf); thin bwmorph(bwCurve, thin, Inf); % 统计骨架像素数差异过大时回看二值图 fprintf(skel 像素数: %d, thin 像素数: %d\n, nnz(skel), nnz(thin)); figure; subplot(1, 2, 1); imshow(skel); title(skel 骨架); subplot(1, 2, 2); imshow(thin); title(thin 细化);逻辑说明bwCurve是连通域筛选后的二值图nnz统计非零像素。skel和thin的像素数差太多时通常说明二值图里有粗块或毛刺先回去调形态学核不要指望骨架化修复。Inf迭代在大多数截图上是安全的但图片特别大时会慢可以先用skel, 5试跑确认效果后再改Inf。骨架化不会修复断点断点还在二值图里后面单独处理。4.2 按列扫描与连通路径处理多值函数和回环曲线如果是单值函数按 x 列扫描最稳每一列取骨架点的 y 中位数得到一条有序点列。多值函数比如闭合曲线或回环按列扫描会把上下两支压成一个值这时要按连通路径走从一端出发沿着 8 邻域找下一个点。我的习惯是先判断曲线是不是单值单值用列扫描多值用路径追踪不要一套代码硬套所有图。% 单值曲线按列扫描每列取骨架 y 的中位数 [yy, xx] find(skel); u unique(xx); xSorted zeros(numel(u), 1); ySorted zeros(numel(u), 1); for k 1:numel(u) idx xx u(k); xSorted(k) u(k); ySorted(k) median(yy(idx)); end % 去掉空列造成的 NaN再映射到数据坐标 valid ~isnan(ySorted); ptsSorted [xSorted(valid), ySorted(valid)]; dataSorted toData(ptsSorted);逻辑说明unique(xx)得到所有有骨架点的列median(yy(idx))取该列骨架点的中位数能压掉少量毛刺。valid去掉没有骨架点的列避免后面插值时出现 NaN。列扫描适合从左到右单调的曲线如果曲线有垂直段同一列会有多个 y中位数会把垂直段压成一个点导致形状失真。遇到这种情况改用路径追踪或者把图旋转到曲线尽量水平再扫描。路径追踪的核心是 8 邻域查找。下面这段从最左上的骨架点出发每次找未访问的邻居适合开放曲线。闭合曲线要额外处理首尾相连否则会在起点附近停住。% 8 邻域路径追踪适合开放曲线 [y0, x0] find(skel, 1, first); visited false(size(skel)); path [x0, y0]; visited(y0, x0) true; neighbors [-1 -1; -1 0; -1 1; 0 -1; 0 1; 1 -1; 1 0; 1 1]; while true found false; for k 1:size(neighbors, 1) ny path(end, 2) neighbors(k, 1); nx path(end, 1) neighbors(k, 2); if ny 1 ny size(skel, 1) nx 1 nx size(skel, 2) ... skel(ny, nx) ~visited(ny, nx) path(end1, :) [nx, ny]; visited(ny, nx) true; found true; break; end end if ~found break; end end逻辑说明neighbors是 8 邻域偏移visited防止回环。每次从当前点找第一个未访问的骨架邻居找不到就结束。这段代码在分叉点会选第一个方向可能走错支路所以只适合没有分叉的开放曲线。find(skel, 1, first)取的是按列扫描的第一个点不一定是曲线端点起点选错会导致路径从中间开始。更稳的做法是先找端点也就是只有一个邻居的骨架点。4.3 断点、毛刺和交叉点的修复策略断点来自二值化不连续毛刺来自骨架化分叉交叉点来自两条曲线重叠。三种问题的修法不同断点用闭运算或形态学桥接毛刺用剪枝交叉点只能靠分组或手动分段。不要用一个imclose大核同时解决核太大会把交叉点糊成块后面更麻烦。% 断点桥接先找端点再在短距离内连接两个端点 skelDub bwmorph(skel, spur, 5); % 先剪掉 5 像素以内的毛刺 endpoints bwmorph(skelDub, endpoints); [ey, ex] find(endpoints); % 端点两两配对距离小于 6 像素时画线桥接 bridge false(size(skelDub)); for i 1:numel(ex) for j i1:numel(ex) d hypot(ex(i)-ex(j), ey(i)-ey(j)); if d 0 d 6 bridge bridge | drawLineMask(size(skelDub), [ex(i), ey(i)], [ex(j), ey(j)]); end end end skelFixed skelDub | bridge;逻辑说明bwmorph(skel, spur, 5)剪掉长度不超过 5 像素的毛刺参数 5 按毛刺长度调太大会把真实短分支剪掉。endpoints找只有一个邻居的端点hypot算端点距离。距离阈值 6 像素适合小断点断口更大时要先回看二值图可能是阈值把曲线切断了光靠桥接会连错。drawLineMask是自定义函数用polyline或 Bresenham 生成线掩膜这里不展开思路是把两个端点之间的像素补上。交叉点不在这段处理交叉处端点不明显桥接容易把两条曲线连成一条。4.4 导出 CSV 与绘图复核导出前必须做一次叠图复核把识别出的数据点按映射反算回像素坐标叠在原图上。如果点偏离曲线先别改导出格式回查二值化和标定。CSV 列名用x,y或x_data,y_data不要用中文列名后面 Python、Excel 都好接。% 数据点反算回像素坐标叠在原图上复核 dataXY toData(ptsSorted); xBack xpix(1) (dataXY(:, 1) - xData(1)) * (xpix(2) - xpix(1)) / (xData(2) - xData(1)); yBack ypix(3) - (dataXY(:, 2) - yData(1)) * (ypix(3) - ypix(4)) / (yData(2) - yData(1)); figure; imshow(img); hold on; plot(xBack, yBack, g., MarkerSize, 6); title(识别点叠图复核绿点应落在曲线上); hold off; % 导出 CSV保留六位小数 writematrix(dataXY, curve_points.csv);逻辑说明xBack、yBack是映射的逆运算用来把数据坐标还原成像素坐标。绿点偏离曲线说明标定或骨架有问题不要直接拿 CSV 去拟合。writematrix导出数值矩阵默认逗号分隔六位小数对大多数截图够用需要更高精度时用writematrix(dataXY, curve_points.csv, Precision, 10)这种形式具体参数名按你的 MATLAB 版本查文档。叠图复核这一步别省它是曲线图片识别程序最便宜的质量保险。5. 曲线图片识别避坑六个翻车点从现象到解决曲线识别脚本跑通不难难的是结果看起来对、用起来错。这一章记录六个高频翻车点每条按“现象、原因、解决”写都是我在不同图上踩过的血泪经验。你不需要全部记住遇到对应现象时回来查一行就行。5.1 现象识别结果整体平移现象导出的数据曲线形状对但整体往左或往右偏半个刻度拟合斜率也跟着偏。原因x 轴锚点取在绘图区边缘而不是刻度线起点或者xData填的是第一个刻度值但锚点点在了轴线最左端。解决重新取 x 轴左端时放大图片点在第一个刻度线与轴线的交点中心不要点轴线端点然后用 3.4 节的网格残差校验残差超过半个小刻度就回查。% 平移排查用已知网格点算偏差 checkPix [300, 500]; trueXY [3.0, 2.0]; predXY toData(checkPix); offset predXY - trueXY; fprintf(x 偏差 %.4f, y 偏差 %.4f\n, offset(1), offset(2));这段只做诊断不自动修。offset(1)明显大于offset(2)时优先查 x 轴锚点两个方向都偏查图片是否被裁剪或缩放。解决平移没有捷径锚点必须落在刻度定义的位置不能凭感觉点轴线边缘。5.2 现象曲线在交叉处被“抄近路”现象两条曲线交叉后识别点从一条曲线跳到另一条导出数据在交叉点附近出现尖刺。原因二值化后两条曲线粘成一个连通域骨架化在交叉点形成分叉路径追踪选了错误分支。解决彩色图先按颜色分组黑白图在交叉点附近手动分段或者用列扫描加连续性约束禁止相邻列 y 跳变超过阈值。% 列扫描连续性约束相邻列 y 跳变过大时标记为可疑 dy abs(diff(ySorted)); jumpIdx find(dy 40); fprintf(发现 %d 处大跳变检查交叉点\n, numel(jumpIdx));阈值 40 是像素经验值按曲线斜率调。大跳变不一定是错垂直段也会跳所以要结合叠图看。黑白交叉图不要指望全自动手动把交叉点前后分成两段分别扫描再按 x 拼接稳定性高得多。5.3 现象抗锯齿边缘导致双线现象骨架化后一条曲线变成两条平行细线或者骨架在旁边多出一条毛刺。原因截图有抗锯齿曲线边缘有半透明过渡像素Otsu 阈值把过渡带也判成前景。解决二值化前先做轻度高斯滤波或者提高阈值让过渡带落到背景形态学开运算核加大到 2 到 3 像素去掉边缘毛刺。不要用太大闭运算否则两条线粘回去更粗。% 抗锯齿处理轻度高斯滤波后再二值化 grayBlur imgaussfilt(grayD, 0.8); bwBlur ~imbinarize(grayBlur, graythresh(grayBlur)); bwBlur bwareaopen(bwBlur, 40);imgaussfilt的 0.8 是标准差越小越保留边缘越大越平滑。抗锯齿严重时试 1.0 到 1.2但别超过 1.5否则细曲线会糊掉。处理完还是双线就回看原图分辨率低分辨率截图的抗锯齿没有太多信息可恢复只能接受误差。5.4 现象批量处理时结果对不上现象单张图跑得好好的批量跑十张有几张数据整体缩放或坐标轴对不上。原因图片尺寸不一致、截图裁剪范围不同、坐标轴刻度范围不同但脚本复用了同一套锚点和xData。解决批量前先按尺寸分组同尺寸同版式的图复用锚点不同尺寸的图重新标定或按比例换算每张图记录图片宽高、锚点像素、数据范围出问题能追溯。% 批量日志每张图记录尺寸和标定参数 logCell {file, width, height, x1, x2, y1, y2}; files dir(*.png); for k 1:numel(files) im imread(files(k).name); logCell(end1, :) {files(k).name, size(im,2), size(im,1), ... xpix(1), xpix(2), ypix(3), ypix(4)}; end writecell(logCell, batch_log.csv);这段是日志骨架实际批量时每张图的锚点要单独存不能全用xpix。日志的价值在于出问题后能快速定位是哪张图、哪个参数变了而不是靠回忆。5.5 现象导出数据振荡现象CSV 里的 y 值在相邻点之间来回跳拟合曲线毛刺很多。原因骨架点没有排序或者同一列有多个骨架点被中位数压成了错误值也可能是毛刺没有剪干净。解决先按 x 排序再做一次中值滤波或滑动平均毛刺用bwmorph(spur, n)剪掉n 按毛刺长度设。不要用大窗口平滑会把真实峰谷抹平。% 导出前排序与轻度平滑 [~, ord] sort(dataXY(:, 1)); dataSorted dataXY(ord, :); ySmooth movmedian(dataSorted(:, 2), 3); dataOut [dataSorted(:, 1), ySmooth];movmedian窗口 3 只压单点跳变适合毛刺窗口 5 以上会改变曲线形状慎用。排序按 x 做多值曲线不能这么排会破坏回环。振荡的根因通常在骨架质量平滑只是补救不要把它当常规步骤。6. 从能跑到敢用曲线识别结果的误差评估与批量脚本习惯曲线图片识别程序交付前我一般做两件事叠图复核和误差指标。叠图看形状指标看数值两个都过才敢把 CSV 给别人。重建曲线叠图最直观把识别点按逆映射画回原图偏一点都能看出来。误差指标用已知网格点算 RMSE 和最大偏差采样密度看每单位 x 有多少个点点太稀拟合会丢峰谷。% 重建叠图 误差指标 dataXY toData(ptsSorted); xBack xpix(1) (dataXY(:,1) - xData(1)) * (xpix(2) - xpix(1)) / (xData(2) - xData(1)); yBack ypix(3) - (dataXY(:,2) - yData(1)) * (ypix(3) - ypix(4)) / (yData(2) - yData(1)); figure; imshow(img); hold on; plot(xBack, yBack, g., MarkerSize, 5); hold off; checkPred toData(checkPix); rmse sqrt(mean((checkPred(:) - checkTrue(:)).^2)); maxErr max(abs(checkPred(:) - checkTrue(:))); fprintf(RMSE%.4f, MaxErr%.4f, 采样密度%.2f 点/单位x\n, ... rmse, maxErr, numel(xSorted) / (xData(2) - xData(1)));检查项合格线不达标时先查叠图偏差绿点基本落在曲线上锚点、轴向反转网格 RMSE小于半个小刻度锚点位置、xData 起点采样密度每单位 x 不少于 20 点骨架断裂、阈值过高相邻点跳变无持续大跳变交叉点、毛刺未剪批量脚本我固定三个习惯每张图单独存标定参数、每张图输出一张叠图、日志里记录图片尺寸和锚点。这样后面发现某张图不对不用重跑全部直接看日志和叠图就能定位。从那以后我每次批量处理前都强制先跑三张样图确认版式一致再放开全量这个习惯省下的返工时间比调阈值多得多。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑