
简介HaoCurve薅曲线是一套面向科研人员、工程师与数据分析者的MATLAB曲线绘制工具源码适合需要在MATLAB环境中快速生成、编辑和分析数据曲线图的用户尤其对希望深入理解绘图脚本实现、进行二次开发的进阶使用者更有参考价值。资源包共3个文件包含1个m脚本、1个mat数据文件和1个p文件压缩包约161KB其中m文件承担曲线绘制与参数调用的核心逻辑mat文件用于存放示例数据p文件则可能为预编译的辅助函数整体结构精简、便于直接导入运行。目前已有1602人学习下载说明其在数据可视化场景中具有一定认可度。借助这份源码读者可以了解曲线颜色、线型、标记样式及坐标轴范围、刻度、标签等视觉属性的定制方式掌握图例、标题、网格线等增强图形可读性的实现思路并在此基础上按自身需求修改脚本、扩展新功能从而更高效地展示复杂数据关系。1. 薅曲线到底在薅什么从一条手绘曲线到可复现代码很多人第一次看到“薅曲线”这个词以为是某种自动化脚本或者爬虫工具其实它解决的是一个非常具体的工程问题如何把一张图片里的曲线快速、准确地还原成可计算、可编辑、可复用的数值数据。你手里可能有一张论文截图、一份扫描版实验报告、一张示波器屏幕照片上面画着一条或几条曲线但你没有原始数据。重新做实验成本太高找作者要数据不现实手动描点又慢又容易出错。HaoCurve 这类工具的核心价值就是把“看图”变成“取数”把像素坐标映射回物理坐标最终输出一份能直接丢进 Excel、Python 或 MATLAB 里继续处理的数值表。它适合谁做实验数据分析的研究生、需要复现别人图表结果的一线工程师、经常处理历史数据档案的技术人员以及任何需要从静态图像中提取定量信息的人。你不需要精通图像处理但需要理解一个基本前提曲线提取的精度取决于你对坐标轴和图像质量的控制而不是工具本身有多智能。这篇笔记会从最小可复现流程讲起把参数怎么调、坑在哪里、什么情况下该放弃自动提取改用半自动全部拆开说清楚。2. 从像素到数值HaoCurve 的坐标映射逻辑与最小跑通流程2.1 为什么不能直接“点两个点就完事”很多人以为曲线提取就是“点一下左上角点一下右下角然后自动识别曲线”。这个思路在理想情况下成立但实际图像里充满了干扰坐标轴刻度线、网格线、图例、文字标注、曲线交叉、颜色渐变、压缩伪影。HaoCurve 的底层逻辑通常分三步图像预处理 → 坐标轴标定 → 曲线像素追踪与映射。预处理负责把彩色图转灰度、二值化、去噪坐标轴标定负责建立像素坐标与物理坐标的仿射变换关系曲线追踪负责从二值图中分离出属于曲线的像素并按列或按行扫描得到中心点。这里的关键参数是二值化阈值和坐标轴标定点数量。阈值设高了浅色曲线会断设低了背景噪声会被当成曲线。标定点只选两个角点在图像有透视畸变或非线性缩放时会产生系统性误差。常见做法是至少选四个点X 轴两个刻度、Y 轴两个刻度用最小二乘法拟合映射关系。2.2 用 Python 复现 HaoCurve 的核心步骤下面这段代码不依赖任何特定 GUI 工具直接用 OpenCV 和 NumPy 实现从图像到数值的最小闭环。你可以把它当成 HaoCurve 类工具的“手动挡”版本理解每一步在做什么。import cv2 import numpy as np # 1. 读取图像并转灰度 img cv2.imread(curve.png) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 2. 二值化阈值需要根据图像对比度调整 # 如果曲线是深色、背景是浅色用 THRESH_BINARY_INV _, binary cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY_INV) # 3. 坐标轴标定假设你已知四个参考点的像素坐标和物理坐标 # 像素坐标 (px, py) 从图像上读取物理坐标 (x, y) 从刻度标签读取 pixel_points np.array([[50, 400], [550, 400], [50, 50], [550, 50]], dtypenp.float32) physical_points np.array([[0, 0], [10, 0], [0, 5], [10, 5]], dtypenp.float32) # 4. 计算仿射变换矩阵这里用透视变换适应更多场景 M cv2.getPerspectiveTransform(pixel_points, physical_points) # 5. 提取曲线像素按列扫描取每列中二值化后白色像素的均值位置 height, width binary.shape curve_points [] for col in range(width): rows np.where(binary[:, col] 0)[0] if len(rows) 0: # 取中位数比均值更抗噪 row_center int(np.median(rows)) curve_points.append((col, row_center)) # 6. 将像素坐标映射到物理坐标 curve_pixels np.array(curve_points, dtypenp.float32).reshape(-1, 1, 2) curve_physical cv2.perspectiveTransform(curve_pixels, M).reshape(-1, 2) # 7. 按 X 物理坐标排序并输出 curve_physical curve_physical[np.argsort(curve_physical[:, 0])] np.savetxt(curve_data.csv, curve_physical, delimiter,, headerx,y, comments)这段代码的逻辑说明第一步转灰度是为了降低计算维度第二步二值化把曲线从背景中分离出来阈值 120 是经验值实际需要根据图像直方图调整第三步的四个标定点必须覆盖整个坐标轴范围不能只选局部第四步用透视变换而不是简单线性缩放是因为拍照或扫描图像往往存在梯形畸变第五步按列扫描取中位数比取均值更能抵抗孤立噪声点第六步把像素坐标批量映射到物理坐标第七步排序后保存为 CSV。参数调整建议如果曲线有断裂降低阈值或先做形态学闭运算如果背景噪声多提高阈值或先做高斯模糊如果曲线是彩色的而背景是灰白的可以转到 HSV 空间按颜色通道提取比灰度二值化更干净。2.3 标定点的选择比算法本身更重要我见过太多人把时间花在换算法、调参数上结果标定点选错了整条曲线整体偏移。标定点的选取原则只有一条选你确定知道物理坐标的、清晰的、孤立的点。坐标轴上的刻度线交叉点是最佳选择不要选文字标注的中心不要选网格线的交点除非你能确认网格线对应的物理值。如果图像有旋转至少选三个不共线的点如果有透视畸变选四个角点。标定完成后一定要用已知点做验证比如取一个刻度点看映射后的物理坐标是否等于标签值误差超过 1% 就要重新选点。3. 参数怎么设二值化、扫描方向与平滑策略的取舍3.1 二值化阈值自适应比固定值更稳固定阈值在光照均匀的扫描件上表现很好但遇到拍照图像、屏幕截图、老旧文档全局阈值就会翻车。常见做法是改用自适应阈值或 Otsu 方法。Otsu 适合双峰直方图的图像自适应阈值适合光照不均的场景。下面这段代码展示了三种方式的对比。# 全局固定阈值 _, binary_fixed cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY_INV) # Otsu 自动阈值 _, binary_otsu cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU) # 自适应阈值适合光照不均 binary_adaptive cv2.adaptiveThreshold( gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 31, 10 )逻辑说明Otsu 方法会遍历所有可能阈值找到使类间方差最大的那个值适合背景和前景对比明显的图。自适应阈值对每个像素邻域单独计算阈值邻域大小 31 和常数 10 需要根据图像分辨率调整——分辨率越高邻域越大。如果曲线本身有粗细变化自适应阈值可能会把粗的地方内部挖空这时候需要先做闭运算填充。3.2 扫描方向按列还是按行取决于曲线单调性按列扫描适合 X 轴单调递增的曲线比如时间序列、频率响应。按行扫描适合 Y 轴单调的曲线比如深度剖面。如果曲线有回环、自交、垂直段单一方向扫描会丢失信息。这时候需要改用轮廓追踪或骨架化方法。常见做法是先用cv2.findContours找到所有连通区域再根据面积和长宽比筛选出曲线轮廓最后用cv2.ximgproc.thinning做骨架化得到单像素宽度的曲线。参数上轮廓筛选的面积阈值建议设为图像总像素的 0.1% 到 1% 之间太小会把噪声当曲线太大会把断裂的曲线段丢掉。骨架化之后按列扫描取中位数依然适用但要注意骨架化可能会在交叉点产生分支需要做剪枝。3.3 平滑策略什么时候该平滑什么时候不该提取出来的原始数据往往有锯齿这是像素离散化的必然结果。平滑能改善视觉效果但会引入偏差。我的经验是如果后续要做微分或峰值检测不要平滑如果只是做趋势展示可以用 Savitzky-Golay 滤波。移动平均会把峰值削平高斯滤波会改变峰位Savitzky-Golay 在保留峰形方面表现更好。窗口长度建议取曲线总点数的 1% 到 3%多项式阶数取 2 或 3。平滑前先保存一份原始数据这是后悔药。4. 避坑与排查曲线提取翻车的五个典型场景4.1 曲线断裂成多段提取结果跳变现象输出的 CSV 里 X 坐标不连续中间缺了一段或者 Y 值突然跳到另一个位置。原因二值化阈值过高浅色曲线段被当成背景或者曲线被坐标轴、网格线切断。解决降低阈值或者先做形态学闭运算把断口连上。如果曲线被网格线切断可以先用霍夫线检测找出网格线并擦除再提取曲线。擦除网格线时注意不要擦到曲线本身可以用颜色区分——网格线通常是灰色曲线是彩色或深色。4.2 坐标轴标定后整体偏移现象提取的曲线形状对但所有数值整体偏大或偏小。原因标定点选在了刻度线的边缘而不是中心或者标定点的物理坐标读错了。解决放大图像把标定点选在刻度线中心物理坐标要读刻度标签不要自己估算。标定完成后用另一个已知刻度点做验证误差超过 1% 就重新标定。如果图像有透视畸变四个标定点要尽量分散在图像四角不要集中在某一侧。4.3 多条曲线混在一起无法分离现象图中有多条曲线提取结果把几条线连成了一条。原因二值化后所有曲线都是白色按列扫描时取了所有白色像素的中位数导致结果落在几条曲线中间。解决按颜色分离。如果曲线颜色不同转到 HSV 空间对每种颜色单独做掩膜。如果曲线颜色相同但线型不同实线、虚线可以先用形态学操作区分——实线连通域大虚线连通域小。如果都不行只能手动框选每条曲线的区域分区域提取。4.4 图像分辨率太低导致精度不够现象提取的曲线阶梯感明显相邻点之间跳变很大。原因图像分辨率不足一个像素对应的物理量太大。解决如果可能重新获取更高分辨率的图像。如果不行可以用插值方法在像素之间估算但要注意插值不会增加真实信息只是让曲线看起来平滑。更诚实的做法是在输出数据里保留原始像素精度并在文档里注明“每个像素对应 X 物理单位”。一般来说曲线提取的相对误差不应低于图像中曲线线宽的一半。4.5 输出数据在 Excel 里打开乱码或错列现象CSV 文件用 Excel 打开后所有数据挤在一列或者中文注释变成乱码。原因分隔符不匹配或编码问题。解决保存时用delimiter,并指定encodingutf-8-sig这样 Excel 能正确识别 UTF-8 编码。如果还是错列检查数据里是否有逗号被当成了分隔符——物理坐标通常不会但如果你在 header 里写了中文逗号就会出问题。最稳妥的方式是保存为.xlsx格式用 openpyxl 或 pandas 直接写。5. 进阶技巧批量处理与精度验证的实操方法5.1 批量处理同格式图像的脚本框架如果你有几十张同类型的曲线图每张都手动标定会疯掉。批量处理的前提是图像格式高度一致坐标轴位置固定、刻度范围相同、曲线颜色一致。这种情况下可以把标定点写成配置文件循环读取图像并套用同一套参数。下面是一个批量处理的骨架代码。import os import cv2 import numpy as np import pandas as pd # 配置所有图像共用的标定点和阈值 CONFIG { pixel_points: [[50, 400], [550, 400], [50, 50], [550, 50]], physical_points: [[0, 0], [10, 0], [0, 5], [10, 5]], threshold: 120, output_dir: output } os.makedirs(CONFIG[output_dir], exist_okTrue) M cv2.getPerspectiveTransform( np.array(CONFIG[pixel_points], dtypenp.float32), np.array(CONFIG[physical_points], dtypenp.float32) ) for fname in os.listdir(images): if not fname.lower().endswith((.png, .jpg, .jpeg)): continue img cv2.imread(os.path.join(images, fname)) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) _, binary cv2.threshold(gray, CONFIG[threshold], 255, cv2.THRESH_BINARY_INV) points [] for col in range(binary.shape[1]): rows np.where(binary[:, col] 0)[0] if len(rows) 0: points.append((col, int(np.median(rows)))) if not points: print(f{fname}: 未检测到曲线跳过) continue pixels np.array(points, dtypenp.float32).reshape(-1, 1, 2) physical cv2.perspectiveTransform(pixels, M).reshape(-1, 2) physical physical[np.argsort(physical[:, 0])] df pd.DataFrame(physical, columns[x, y]) out_path os.path.join(CONFIG[output_dir], fname.rsplit(., 1)[0] .csv) df.to_csv(out_path, indexFalse, encodingutf-8-sig) print(f{fname}: 输出 {len(df)} 个点)这段代码的关键在于配置与数据分离。标定点和阈值写在 CONFIG 里换一批图只需要改配置。实际使用中建议先拿一张图跑通确认输出正确后再批量跑。批量跑的时候要加异常捕获某张图失败不应该中断整个流程。输出文件名保持和输入一致方便追溯。5.2 精度验证用已知曲线做闭环测试你怎么知道提取结果是准的最可靠的方法是自己生成一张测试图用代码画一条已知函数的曲线保存为图片再用提取脚本跑一遍对比提取结果和原始函数。下面是一个闭环测试的例子。import numpy as np import matplotlib.pyplot as plt # 生成已知曲线y sin(x) 0.1x x_true np.linspace(0, 10, 500) y_true np.sin(x_true) 0.1 * x_true # 画图并保存不显示坐标轴标签和刻度模拟干净图像 fig, ax plt.subplots(figsize(6, 4), dpi100) ax.plot(x_true, y_true, k-, linewidth2) ax.set_xlim(0, 10) ax.set_ylim(-1.5, 2.5) ax.axis(off) # 关闭坐标轴方便后续手动标定 plt.savefig(test_curve.png, bbox_inchestight, pad_inches0) plt.close() # 然后用前面的提取脚本处理 test_curve.png # 标定点需要根据实际图像像素位置手动读取 # 对比提取结果和 y_true计算均方根误差这个测试的意义在于你能精确知道每个 X 对应的真实 Y 值提取误差一目了然。如果均方根误差在可接受范围内比如小于曲线幅度的 1%说明整个流程是可靠的。如果误差大逐步排查是二值化问题、标定问题还是扫描方向问题。我一般会把这个测试图跑三遍原图、加高斯噪声、加 JPEG 压缩看看算法在退化条件下的表现。这样你心里就有底了知道什么质量的图能接什么质量的图该拒。5.3 一个容易被忽略的细节曲线线宽的影响曲线在图像里是有宽度的按列扫描取中位数得到的是线宽中心。如果线宽是 3 个像素中心点的误差最多 1.5 个像素。在标定精度高的情况下这个误差可以接受。但如果线宽是 10 个像素误差就大了。这时候需要做亚像素级中心提取常见做法是对每列的灰度分布做高斯拟合取拟合峰值位置。OpenCV 没有直接提供这个功能需要自己写。我的习惯是线宽小于 5 像素用中位数大于 5 像素用灰度加权重心。灰度加权重心的公式是sum(y * I(y)) / sum(I(y))其中 I(y) 是灰度值。这个方法比中位数更抗噪但要求曲线和背景的对比度足够高。最后说一个我自己的教训早期做曲线提取时我总想把所有参数都自动化结果在异常图像上反复翻车。后来我改成“自动提取 人工抽检”的模式每批数据随机抽 10% 用肉眼对比原图发现偏差就回退到半自动模式。这个习惯帮我省了很多返工时间。希望帮到你。本文还有配套的精品资源点击获取