简介:这是一份面向科研人员与图像处理学习者的MATLAB曲线识别工具包,用于从论文截图或实验图像中自动提取曲线坐标,省去手工描点或重写绘图代码的麻烦。压缩包共14个文件,约459KB,包含12个png示例图片、1个mat数据文件和1个m脚本文件,图片用于测试识别效果,脚本与数据文件则承载核心算法与结果存储。程序覆盖图像预处理、边缘检测、轮廓追踪、曲线拟合、数据绘图、文件读写及GUI交互等环节,并涉及算法优化思路,适合具备MATLAB基础、希望快速上手图像曲线提取的读者。目前已有2019人学习下载,可借助示例图片与脚本对照调试,理解从灰度化、二值化到坐标拟合的完整流程,并在此基础上改造为适配自身课题的识别方案。
1. 曲线图片识别程序:从像素到数据的完整还原路径
手里有一张论文截图、一份扫描版实验报告,或者别人发来的折线图,想把上面的曲线重新变成可以计算、可以拟合的数值——这件事听起来简单,做起来全是细节。matlab 曲线图片识别程序就是干这个的:输入一张带坐标轴的曲线图,输出曲线上一系列 (x, y) 数据点。它解决的核心问题是「图里有数据,但数据拿不出来」。适合谁用?做实验对比时找不到原始数据的研究生、需要复现别人图表的一线工程师、以及经常要处理历史纸质图表的数据分析人员。我拆过不少这类源码包,大部分都卡在三个地方:坐标轴自动定位不准、曲线与网格线粘连、多曲线交叉时提取串线。下面按实际复现流程,把这份 matlab 曲线图片识别程序从头到尾走一遍。
2. 曲线识别程序的整体流程与核心模块拆解
2.1 从图像预处理到坐标映射的五个阶段
一份完整的曲线图片识别程序,流程可以拆成五步:图像读取与灰度化、坐标轴区域检测、曲线像素提取、像素坐标到数据坐标的映射、数据平滑与输出。每一步都有对应的 matlab 函数和参数需要调。
先看图像读取和预处理。常见做法是用imread读入,然后rgb2gray转灰度。如果原图是彩色曲线,灰度化之后不同颜色的曲线可能灰度值接近,导致后续二值化时粘连。这时候不要急着转灰度,可以先在 RGB 空间按颜色通道分离。
% 读取图像并查看基本信息 img = imread('curve_sample.png'); info = imfinfo('curve_sample.png'); fprintf('尺寸: %d x %d, 位深: %d\n', info.Width, info.Height, info.BitDepth); % 如果是彩色图,先分离通道看看曲线颜色分布 if size(img, 3) == 3 r = img(:,:,1); g = img(:,:,2); b = img(:,:,3); % 红色曲线在 R 通道亮、G/B 通道暗,用差值增强 red_enhanced = r - max(g, b); figure; imshow(red_enhanced); title('红色曲线增强'); end % 灰度化作为备用 gray = rgb2gray(img);这段代码的逻辑是先判断图像类型,彩色图不要盲目转灰度。参数上,imfinfo返回的BitDepth决定了后续二值化阈值的选取范围:8 位图像素值 0-255,16 位则是 0-65535。如果曲线是红色而背景是白色,r - max(g,b)这种通道差值能有效抑制背景,比直接灰度化再阈值分割稳得多。
第二步是坐标轴区域检测。这是整个流程里最容易翻车的地方。常见做法是用边缘检测加霍夫变换找直线,或者用投影法找坐标轴的长直线。matlab 里可以用edge配合hough,但更稳的方式是先二值化,然后对行和列做像素求和,峰值位置就是坐标轴候选。
% 二值化:Otsu 方法自动选阈值 bw = imbinarize(gray, 'adaptive', 'Sensitivity', 0.5); % 投影法找坐标轴:行投影和列投影 row_sum = sum(bw, 2); col_sum = sum(bw, 1); % 找行投影的峰值(横轴位置) [min_row, max_row] = findpeaks(row_sum, 'MinPeakHeight', size(bw,2)*0.3); % 找列投影的峰值(纵轴位置) [min_col, max_col] = findpeaks(col_sum, 'MinPeakHeight', size(bw,1)*0.3); fprintf('横轴候选行: %s\n', mat2str(max_row)); fprintf('纵轴候选列: %s\n', mat2str(max_col));这里imbinarize的Sensitivity参数很关键,默认 0.5 适合背景干净的图,如果图上有噪声或者扫描件有污渍,调到 0.4-0.45 能减少伪影。findpeaks的MinPeakHeight设成图像宽或高的 30%,是为了过滤掉曲线本身的投影峰值——曲线在行方向上的投影通常不会超过整幅图宽度的 30%,而坐标轴是贯穿整幅图的。
第三步是曲线像素提取。坐标轴定位之后,把坐标轴区域裁掉,剩下的就是曲线和可能的网格线、图例、文字标注。网格线通常是浅色细线,可以用形态学操作去掉:先腐蚀再膨胀,或者直接用bwareaopen去掉小面积连通域。
% 裁掉坐标轴区域,只保留绘图区 plot_area = bw(axis_top+2:axis_bottom-2, axis_left+2:axis_right-2); % 去掉网格线:网格线通常比曲线细,用面积过滤 plot_area = bwareaopen(plot_area, 30); % 去掉小于 30 像素的连通域 % 如果还有网格线残留,用形态学开运算 se = strel('disk', 1); plot_area = imopen(plot_area, se); % 提取曲线骨架,保证单像素宽 curve_skeleton = bwmorph(plot_area, 'skel', Inf);bwareaopen的面积阈值 30 是个经验值,取决于图像分辨率。高分辨率图(2000px 以上)可以设到 50-80,低分辨率图(500px 以下)设 10-15。bwmorph的'skel'操作把曲线细化成单像素宽,方便后续逐列取点。
第四步是坐标映射。这一步需要知道坐标轴的实际数值范围。如果原图上有刻度标签,可以用 OCR 识别;如果没有,就得手动输入。常见做法是在程序里留一个配置区,让用户填 x 轴和 y 轴的起止值。
% 坐标映射参数(手动配置区) x_data_min = 0; % x 轴起始数值 x_data_max = 10; % x 轴结束数值 y_data_min = -5; % y 轴起始数值 y_data_max = 5; % y 轴结束数值 % 像素坐标到数据坐标的线性映射 [rows, cols] = find(curve_skeleton); x_pixel_min = min(cols); x_pixel_max = max(cols); y_pixel_min = min(rows); y_pixel_max = max(rows); x_data = x_data_min + (cols - x_pixel_min) / (x_pixel_max - x_pixel_min) * (x_data_max - x_data_min); y_data = y_data_max - (rows - y_pixel_min) / (y_pixel_max - y_pixel_min) * (y_data_max - y_data_min); % 按 x 排序并去重(同一列可能有多个像素) [x_sorted, idx] = sort(x_data); y_sorted = y_data(idx); [x_unique, ia] = unique(round(x_sorted, 4), 'first'); y_unique = y_sorted(ia); % 输出结果 result = [x_unique, y_unique]; save('curve_data.mat', 'result'); writematrix(result, 'curve_data.csv');映射逻辑里 y 轴要反过来减,因为图像行号从上往下增大,而数据坐标 y 轴从下往上增大。unique去重时用round到四位小数,是为了处理浮点误差导致的重复 x 值。最后同时存 mat 和 csv 两种格式,方便后续在 matlab 或其他工具里用。
2.2 多曲线分离与颜色通道策略
一张图上有两条以上曲线时,提取难度直接翻倍。核心思路是按颜色分离。如果曲线颜色区分明显(红、蓝、绿),在 RGB 空间做通道运算就能分开。如果颜色接近,就得转到 HSV 空间按色调分离。
% 多曲线按颜色分离 img_hsv = rgb2hsv(img); hue = img_hsv(:,:,1); % 色调通道 sat = img_hsv(:,:,2); % 饱和度通道 val = img_hsv(:,:,3); % 明度通道 % 红色曲线:色调在 0 或 1 附近 red_mask = (hue < 0.05 | hue > 0.95) & sat > 0.3 & val > 0.3; % 蓝色曲线:色调在 0.55-0.75 之间 blue_mask = (hue > 0.55 & hue < 0.75) & sat > 0.3 & val > 0.3; % 分别提取 red_curve = bwskel(red_mask); blue_curve = bwskel(blue_mask);HSV 分离的关键参数是饱和度阈值sat > 0.3。这个值太低会把浅色背景算进来,太高会漏掉浅色曲线。如果曲线是半透明的,饱和度可能只有 0.15-0.25,这时候要配合明度通道一起判断。bwskel是 matlab 较新版本里的骨架化函数,比bwmorph更快,老版本用bwmorph(..., 'skel', Inf)替代。
多曲线交叉的地方是另一个坑。两条曲线在像素上重叠时,颜色会混合,按颜色分离后交叉点会出现断裂。常见做法是对每条曲线分别做形态学闭运算,把断裂处连上,然后再骨架化。
% 对交叉断裂处做闭运算修复 se_line = strel('line', 5, 0); % 水平方向闭运算 red_fixed = imclose(red_mask, se_line); red_fixed = imclose(red_fixed, strel('line', 5, 90)); % 垂直方向 % 再骨架化 red_curve = bwskel(red_fixed);闭运算的结构元素长度 5 是个起点,交叉断裂 gap 大的话加到 10-15。但注意闭运算太强会把两条平行靠近的曲线粘成一条,所以修复后要检查连通域数量,如果从两条变成一条,说明结构元素太大了。
3. 参数调优与批量处理:让识别结果可复现
3.1 二值化阈值与形态学参数的联动调整
曲线识别里最玄学的就是二值化阈值。同一张图,阈值差 0.05,提取出来的曲线可能从「光滑」变成「锯齿」。我一般不会只调一个参数,而是把二值化阈值和形态学参数放在一起联动调。
先看二值化。imbinarize的自适应方法有两个关键参数:Sensitivity和NeighborhoodSize。前者控制阈值偏移,后者控制局部窗口大小。
% 参数扫描:不同 Sensitivity 下的曲线提取效果 sensitivity_list = [0.35, 0.40, 0.45, 0.50, 0.55]; figure; for i = 1:length(sensitivity_list) bw_temp = imbinarize(gray, 'adaptive', ... 'Sensitivity', sensitivity_list(i), ... 'NeighborhoodSize', 2*floor(size(gray,1)/16)+1); % 统计前景像素占比 fg_ratio = sum(bw_temp(:)) / numel(bw_temp); subplot(2, 3, i); imshow(bw_temp); title(sprintf('Sens=%.2f, FG=%.1f%%', sensitivity_list(i), fg_ratio*100)); endNeighborhoodSize必须是奇数,常见做法是取图像高度的 1/8 到 1/16,然后取最近的奇数。前景像素占比fg_ratio是个很好的量化指标:曲线图的前景占比通常在 2%-8% 之间,低于 2% 说明阈值太高漏掉了曲线,高于 8% 说明把背景噪声也算进来了。
形态学参数方面,bwareaopen的面积阈值和imopen的结构元素大小要配合着调。我的习惯是先固定bwareaopen的面积阈值,扫描结构元素大小,看曲线连通域数量的变化。
% 形态学参数扫描 area_thresh = 30; se_sizes = [1, 2, 3, 4, 5]; for i = 1:length(se_sizes) bw_temp = bwareaopen(bw, area_thresh); se = strel('disk', se_sizes(i)); bw_temp = imopen(bw_temp, se); % 统计连通域数量和最大连通域面积 cc = bwconncomp(bw_temp); stats = regionprops(cc, 'Area'); areas = [stats.Area]; fprintf('SE=%d: 连通域数=%d, 最大面积=%d, 总面积=%d\n', ... se_sizes(i), cc.NumObjects, max(areas), sum(areas)); end判断标准:连通域数量应该等于曲线条数(单条曲线就是 1),最大连通域面积应该占总面积的 80% 以上。如果连通域数量突然从 1 变成 5,说明结构元素太大把曲线打断了;如果从 3 变成 1,说明把多条曲线粘在一起了。
3.2 批量处理多张图片的脚本框架
实际工作中很少只处理一张图。批量处理的核心是把上面的流程封装成函数,然后用循环调用。这里给一个可复用的框架。
function batch_curve_extract(input_dir, output_dir, config) % 批量曲线提取 % input_dir: 输入图片目录 % output_dir: 输出数据目录 % config: 配置结构体,包含坐标范围、颜色阈值等 if ~exist(output_dir, 'dir') mkdir(output_dir); end img_files = dir(fullfile(input_dir, '*.png')); img_files = [img_files; dir(fullfile(input_dir, '*.jpg'))]; log_file = fopen(fullfile(output_dir, 'extract_log.txt'), 'w'); for i = 1:length(img_files) img_path = fullfile(input_dir, img_files(i).name); fprintf('处理 [%d/%d]: %s\n', i, length(img_files), img_files(i).name); try result = extract_single_curve(img_path, config); % 保存结果 [~, name, ~] = fileparts(img_files(i).name); csv_path = fullfile(output_dir, [name, '_data.csv']); writematrix(result, csv_path); fprintf(log_file, 'OK: %s, 点数=%d\n', img_files(i).name, size(result,1)); catch ME fprintf(log_file, 'FAIL: %s, 原因=%s\n', img_files(i).name, ME.message); end end fclose(log_file); end function result = extract_single_curve(img_path, config) % 单张图片的曲线提取流程 img = imread(img_path); % 按配置做颜色分离或灰度化 if isfield(config, 'color_mode') && strcmp(config.color_mode, 'hsv') img_hsv = rgb2hsv(img); mask = (img_hsv(:,:,1) > config.hue_min & img_hsv(:,:,1) < config.hue_max) & ... (img_hsv(:,:,2) > config.sat_min); else gray = rgb2gray(img); mask = imbinarize(gray, 'adaptive', 'Sensitivity', config.sensitivity); end % 形态学清理 mask = bwareaopen(mask, config.area_thresh); mask = imopen(mask, strel('disk', config.se_size)); mask = bwskel(mask); % 坐标映射 [rows, cols] = find(mask); x_data = config.x_min + (cols - min(cols)) / (max(cols) - min(cols)) * (config.x_max - config.x_min); y_data = config.y_max - (rows - min(rows)) / (max(rows) - min(rows)) * (config.y_max - config.y_min); [x_sorted, idx] = sort(x_data); y_sorted = y_data(idx); [x_unique, ia] = unique(round(x_sorted, 4), 'first'); result = [x_unique, y_sorted(ia)]; end这个框架里config结构体把所有可调参数集中管理,批量处理时只需要改配置,不用改代码。日志文件记录每张图的处理结果和失败原因,方便排查。extract_single_curve里用try-catch包住,单张图失败不会中断整个批次。
配置结构体的典型设置:
config = struct(); config.color_mode = 'hsv'; % 'hsv' 或 'gray' config.hue_min = 0.0; % 红色曲线 config.hue_max = 0.05; config.sat_min = 0.3; config.sensitivity = 0.45; % 灰度模式下的二值化灵敏度 config.area_thresh = 30; % 面积过滤阈值 config.se_size = 2; % 形态学结构元素大小 config.x_min = 0; config.x_max = 10; config.y_min = -5; config.y_max = 5;批量处理时最容易忽略的是图像尺寸不一致。不同来源的图片分辨率可能从 500px 到 4000px 不等,area_thresh和se_size需要按图像尺寸缩放。常见做法是在extract_single_curve开头根据图像高度动态调整这两个参数。
% 按图像尺寸自适应调整形态学参数 img_height = size(img, 1); scale_factor = img_height / 1000; % 以 1000px 高度为基准 config.area_thresh = round(config.area_thresh * scale_factor); config.se_size = max(1, round(config.se_size * scale_factor));4. 避坑与排查:曲线识别里最常见的五类翻车
4.1 坐标轴定位偏移导致数据整体平移
现象:提取出来的曲线形状对,但数值整体偏大或偏小,和原图对不上。
原因:坐标轴检测时把曲线的一部分误判成了坐标轴,或者坐标轴有箭头、刻度线伸出,导致检测到的轴位置比实际偏了几个像素。
解决:在坐标轴检测后加一步人工确认。程序里把检测到的轴位置画在原图上,让用户看一眼。如果偏了,允许手动输入轴位置的像素坐标。常见做法是在配置里加axis_override字段,用户填了就跳过自动检测。
% 坐标轴位置人工覆盖 if isfield(config, 'axis_override') && ~isempty(config.axis_override) axis_top = config.axis_override(1); axis_bottom = config.axis_override(2); axis_left = config.axis_override(3); axis_right = config.axis_override(4); else % 自动检测逻辑 [axis_top, axis_bottom, axis_left, axis_right] = auto_detect_axis(bw); end4.2 网格线与曲线粘连导致提取出多余数据点
现象:提取的数据里出现大量水平或垂直的直线段,曲线本身反而被淹没了。
原因:网格线和曲线颜色接近,二值化后连在一起,形态学操作没能分开。
解决:先判断网格线方向。如果网格线是水平线,用水平方向的长结构元素做开运算,把水平线去掉;垂直线同理。注意结构元素长度要大于网格线间距,但小于曲线的最小弯曲半径。
% 去掉水平网格线 se_h = strel('line', 50, 0); % 长度 50 的水平线 no_grid_h = imopen(bw, se_h); bw_clean = bw & ~no_grid_h; % 从原图中减去网格线 % 再去掉垂直网格线 se_v = strel('line', 50, 90); no_grid_v = imopen(bw, se_v); bw_clean = bw_clean & ~no_grid_v;4.3 多曲线交叉处提取串线
现象:两条曲线交叉后,提取的数据点从一条曲线跳到了另一条上。
原因:按颜色分离后交叉点颜色混合,或者骨架化时交叉点被合并成一个节点。
解决:对每条曲线分别做连通域标记,交叉点处断开成多个连通域,然后按「方向连续性」原则重新连接。具体做法是记录每个连通域的端点和方向,距离近且方向夹角小于 30 度的端点连起来。
% 连通域端点分析 cc = bwconncomp(curve_mask); stats = regionprops(cc, 'Endpoint', 'Orientation'); % 找距离近且方向一致的端点对 endpoints = cat(1, stats.Endpoint); orientations = cat(1, stats.Orientation); for i = 1:size(endpoints, 1) for j = i+1:size(endpoints, 1) dist = norm(endpoints(i,:) - endpoints(j,:)); angle_diff = abs(orientations(i) - orientations(j)); if dist < 20 && angle_diff < 30 % 连接这两个端点 % 在两点之间画线 end end end4.4 扫描件噪声导致二值化后满屏噪点
现象:扫描版图片二值化后背景全是小黑点,曲线被噪点包围。
解决:扫描件噪声通常是椒盐噪声,用中值滤波先处理。medfilt2窗口大小 3 或 5,太大把曲线也模糊了。滤波后再二值化,然后bwareaopen去掉小噪点。
% 扫描件去噪流程 gray_denoised = medfilt2(gray, [3 3]); % 中值滤波 bw = imbinarize(gray_denoised, 'adaptive', 'Sensitivity', 0.4); bw = bwareaopen(bw, 20); % 去掉小噪点 bw = imclose(bw, strel('disk', 1)); % 闭运算连接断裂4.5 坐标映射时 y 轴方向搞反
现象:提取的曲线形状上下颠倒,或者数值正负号反了。
原因:图像行号从上往下增大,而数据坐标 y 轴从下往上增大,映射时忘了翻转。
解决:映射公式里 y 轴用y_max - (row - row_min) / (row_max - row_min) * (y_max - y_min)。如果原图 y 轴是反向的(比如某些频谱图),把y_max和y_min对调即可。验证方法:取曲线最高点,看映射后的 y 值是否接近y_max。
5. 进阶技巧:用插值和平滑让提取数据直接可用
提取出来的原始数据点往往有锯齿,直接拿去拟合或求导会放大噪声。我一般会做两步后处理:先按 x 均匀重采样,再做平滑。
% 后处理:均匀重采样 + 平滑 x_raw = result(:, 1); y_raw = result(:, 2); % 按 x 均匀重采样,点数设为原始点数的 2 倍 x_uniform = linspace(min(x_raw), max(x_raw), length(x_raw) * 2)'; y_uniform = interp1(x_raw, y_raw, x_uniform, 'pchip'); % Savitzky-Golay 平滑 window_size = 11; % 窗口大小,必须是奇数 poly_order = 3; % 多项式阶数 y_smooth = sgolayfilt(y_uniform, poly_order, window_size); % 输出最终结果 final_result = [x_uniform, y_smooth]; writematrix(final_result, 'curve_data_smooth.csv'); % 对比图 figure; plot(x_raw, y_raw, 'b.', 'MarkerSize', 4); hold on; plot(x_uniform, y_smooth, 'r-', 'LineWidth', 1.5); legend('原始提取点', '重采样+平滑'); xlabel('X'); ylabel('Y');interp1用'pchip'而不是'spline',是因为 pchip 保形插值不会在数据点之间产生过冲,spline 在曲线拐弯处容易冒出虚假的峰。sgolayfilt的窗口大小 11 和阶数 3 是通用起点:窗口越大越平滑但细节丢失越多,阶数越高越贴近原始数据但噪声保留越多。判断标准是看平滑后的曲线是否还保留原图的特征峰——如果峰被抹平了,窗口调小;如果还有明显锯齿,窗口调大。
验证提取精度的一个实用方法:把提取的数据用plot画出来,和原图叠在一起对比。matlab 里可以用imshow显示原图,然后用hold on把数据曲线画在同一个坐标系里,调整坐标轴范围让两者对齐。
% 叠加验证 figure; imshow(img); hold on; % 把数据坐标映射回像素坐标 x_pixel = (x_uniform - config.x_min) / (config.x_max - config.x_min) * (axis_right - axis_left) + axis_left; y_pixel = axis_bottom - (y_smooth - config.y_min) / (config.y_max - config.y_min) * (axis_bottom - axis_top); plot(x_pixel, y_pixel, 'g-', 'LineWidth', 2); title('绿色为提取曲线,应与原图曲线重合');如果绿色线和原图曲线重合度高,说明提取准确;如果整体偏移,检查坐标轴检测;如果局部偏离,检查该区域的二值化和形态学参数。从那以后我每次提取完曲线,都强制走一遍这个叠加验证,确认无误再进入后续分析。希望帮到你。
本文还有配套的精品资源,点击获取