function recognize_math_expression_4()
% 创建主窗口
fig = figure('Name', '鲁棒公式识别系统', 'NumberTitle', 'off', ...
'Position', [100, 100, 1200, 800], 'MenuBar', 'none', ...
'Color', [0.95 0.95 0.95]);
% 创建UI控件
uicontrol('Style', 'pushbutton', 'String', '上传图像', ...
'Position', [30, 750, 100, 30], 'Callback', @uploadImage, ...
'FontSize', 11, 'BackgroundColor', [0.3 0.6 0.9], 'ForegroundColor', 'white');
uicontrol('Style', 'pushbutton', 'String', '识别公式', ...
'Position', [150, 750, 100, 30], 'Callback', @recognizeFormula, ...
'FontSize', 11, 'BackgroundColor', [0.1 0.7 0.3], 'ForegroundColor', 'white');
uicontrol('Style', 'pushbutton', 'String', '清除结果', ...
'Position', [270, 750, 100, 30], 'Callback', @clearResults, ...
'FontSize', 11, 'BackgroundColor', [0.9 0.5 0.1], 'ForegroundColor', 'white');
% 进度条
progress_ax = axes('Parent', fig, 'Position', [0.4, 0.78, 0.2, 0.02], ...
'XLim', [0 100], 'YLim', [0 1], 'Box', 'on', 'XTick', [], 'YTick', []);
progress_bar = rectangle('Parent', progress_ax, 'Position', [0,0,0,1], ...
'FaceColor', [0.1 0.8 0.1], 'EdgeColor', 'none');
progress_text = uicontrol('Style', 'text', 'String', '就绪', ...
'Position', [580, 770, 200, 20], 'FontSize', 10, ...
'HorizontalAlignment', 'center', 'BackgroundColor', [0.95 0.95 0.95]);
% 创建图像显示区域
ax_original = axes('Parent', fig, 'Position', [0.05, 0.5, 0.4, 0.35]);
title(ax_original, '原始图像');
axis(ax_original, 'off');
ax_processed = axes('Parent', fig, 'Position', [0.55, 0.5, 0.4, 0.35]);
title(ax_processed, '处理图像');
axis(ax_processed, 'off');
ax_equal = axes('Parent', fig, 'Position', [0.05, 0.1, 0.4, 0.35]);
title(ax_equal, '等号检测');
axis(ax_equal, 'off');
% 创建结果展示区域
result_panel = uipanel('Title', '识别结果', 'FontSize', 12, ...
'BackgroundColor', 'white', 'Position', [0.55, 0.1, 0.4, 0.35]);
uicontrol('Parent', result_panel, 'Style', 'text', 'String', '识别公式:', ...
'Position', [20, 200, 80, 20], 'FontSize', 11, 'HorizontalAlignment', 'left');
formula_text = uicontrol('Parent', result_panel, 'Style', 'text', 'String', '', ...
'Position', [110, 200, 300, 20], 'FontSize', 11, 'HorizontalAlignment', 'left', ...
'BackgroundColor', 'white');
uicontrol('Parent', result_panel, 'Style', 'text', 'String', '计算结果:', ...
'Position', [20, 170, 80, 20], 'FontSize', 11, 'HorizontalAlignment', 'left');
calc_text = uicontrol('Parent', result_panel, 'Style', 'text', 'String', '', ...
'Position', [110, 170, 300, 20], 'FontSize', 11, 'HorizontalAlignment', 'left', ...
'BackgroundColor', 'white');
uicontrol('Parent', result_panel, 'Style', 'text', 'String', '用户答案:', ...
'Position', [20, 140, 80, 20], 'FontSize', 11, 'HorizontalAlignment', 'left');
answer_text = uicontrol('Parent', result_panel, 'Style', 'text', 'String', '', ...
'Position', [110, 140, 300, 20], 'FontSize', 11, 'HorizontalAlignment', 'left', ...
'BackgroundColor', 'white');
uicontrol('Parent', result_panel, 'Style', 'text', 'String', '验证结果:', ...
'Position', [20, 110, 80, 20], 'FontSize', 11, 'HorizontalAlignment', 'left');
result_text = uicontrol('Parent', result_panel, 'Style', 'text', 'String', '', ...
'Position', [110, 110, 300, 20], 'FontSize', 11, 'HorizontalAlignment', 'left', ...
'BackgroundColor', 'white');
% 等号检测日志
equal_log = uicontrol('Parent', result_panel, 'Style', 'listbox', ...
'String', {}, 'Position', [20, 30, 360, 70], 'FontSize', 10, ...
'BackgroundColor', 'white');
% 存储GUI句柄
handles = struct();
handles.ax_original = ax_original;
handles.ax_processed = ax_processed;
handles.ax_equal = ax_equal;
handles.formula_text = formula_text;
handles.calc_text = calc_text;
handles.answer_text = answer_text;
handles.result_text = result_text;
handles.equal_log = equal_log;
handles.progress_bar = progress_bar;
handles.progress_text = progress_text;
handles.progress_ax = progress_ax;
handles.current_image = [];
handles.binary_image = [];
handles.region_stats = [];
handles.region_bboxes = [];
handles.equal_sign_index = [];
handles.ocr_results = [];
handles.fig = fig; % 存储figure句柄
guidata(fig, handles);
%% ======================== 回调函数 ========================
function uploadImage(~, ~)
% 获取全局 handles 结构体
handles = guidata(gcbf); % 使用 gcbf 获取当前回调的 figure 句柄
updateProgress(0, '准备上传图像...');
[filename, pathname] = uigetfile({'*.jpg;*.png;*.bmp', '图像文件 (*.jpg, *.png, *.bmp)'}, ...
'选择公式图像');
if isequal(filename, 0)
updateProgress(0, '取消上传');
return;
end
updateProgress(10, '读取图像...');
img_path = fullfile(pathname, filename);
img = imread(img_path);
% 更新 handles
handles.current_image = img;
% 显示原始图像
axes(handles.ax_original);
imshow(img);
title('原始图像');
% 预处理图像
updateProgress(20, '图像预处理...');
if size(img, 3) == 3 % 判断是否为彩色图像
gray_img = rgb2gray(img); % 将彩色图像转换为灰度图像
else
gray_img = img; % 如果已经是灰度图像,则直接使用
end
% 显示灰度图像
axes(handles.ax_processed);
imshow(gray_img);
title('灰度图像');
% 图像锐化 - 增强边缘
sharpened_img = imsharpen(gray_img, 'Radius', 1.5, 'Amount', 1.2, 'Threshold', 0.1);
% 使用大律法计算全局阈值
global_level = graythresh(sharpened_img);
% 计算图像的标准差(衡量对比度)
image_contrast = std(double(sharpened_img(:))) / 255;
% 动态调整敏感度
if image_contrast > 0.1 % 对比度较好
sensitivity = global_level * 0.9; % 较低的敏感度
if sensitivity<0.7
sensitivity=0.7;
end
else % 对比度较差
sensitivity = global_level * 1.05; % 较高的敏感度
end
% 自适应二值化结合敏感度
binary_img = imbinarize(sharpened_img, 'adaptive', 'Sensitivity', sensitivity);
inverted_img = ~binary_img;
% 显示结果
axes(handles.ax_processed);
imshow(inverted_img);
title('结合大律法与自适应二值化的图像');
% 形态学去噪 - 定义结构元素
se = strel('disk', 2); % 创建一个半径为 2 的圆形结构元素
% 开运算:先腐蚀后膨胀,去除小的噪声点
cleaned_img = imopen(inverted_img, se);
% 可选:闭运算(先膨胀后腐蚀),填补小的孔洞
%cleaned_img = imclose(cleaned_img, se);
%inverted_img = ~binary_img;
% 更新 handles
handles.binary_image = cleaned_img;
% 显示处理图像
axes(handles.ax_processed);
imshow(cleaned_img);
title('大律法二值化图像');
% 重置结果
set(handles.formula_text, 'String', '');
set(handles.calc_text, 'String', '');
set(handles.answer_text, 'String', '');
set(handles.result_text, 'String', '');
set(handles.equal_log, 'String', {});
axes(handles.ax_equal);
cla;
title('等号检测');
% 保存更新后的 handles
guidata(gcbf, handles); % 关键修复:更新全局 handles
updateProgress(100, '图像上传完成');
end
%% 回调函数
function recognizeFormula(~, ~)
handles = guidata(gcbf); % 使用 gcbf 获取当前句柄
if isempty(handles.binary_image)
errordlg('请先上传图像', '错误');
return;
end
try
updateProgress(0, '开始公式识别...');
inverted_img = handles.binary_image;
[img_h, img_w] = size(inverted_img);
% ================= 第一步:区域分割 =================
axes(handles.ax_processed);
imshow(inverted_img);
title('区域分割');
drawnow;
updateProgress(10, '区域分割...');
% 动态计算膨胀参数
se_radius = calculateDilationRadius(inverted_img, img_h, img_w);
se = strel('disk', se_radius); % 创建圆形结构元素
dilated_img = imdilate(inverted_img, se);
% 区域分割 - 使用 8 连通性
cc = bwconncomp(dilated_img, 8); % 使用 8 连通性
stats = regionprops(cc, 'BoundingBox', 'Area', 'Eccentricity', 'Orientation', 'Solidity');
% 添加IsEqual字段(默认为false)
[stats.IsEqual] = deal(0);
[stats.Row] = deal(0);
% 过滤噪点
min_area = max(5, img_h*img_w*0.0005); % 动态最小面积
valid_idx = [stats.Area] > min_area;
stats = stats(valid_idx);
% 按垂直位置排序
bboxes = vertcat(stats.BoundingBox);
[~, order] = sort(bboxes(:,2));
bboxes = bboxes(order, :);
stats = stats(order);
% 存储区域信息
handles.region_stats = stats;
handles.region_bboxes = bboxes;
% 显示分割结果
imshow(inverted_img); hold on;
for i = 1:size(bboxes,1)
rectangle('Position', bboxes(i,:), 'EdgeColor', [0.8 0.2 0.2], 'LineWidth', 1);
end
hold off;
title(sprintf('分割出 %d 个区域', length(stats)));
drawnow;
updateProgress(20, '按行分组...');
row_threshold = mean([bboxes(:,4)]) * 1; % 行间距阈值为平均字符高度的1倍
%rows = groupRegionsByRows(stats, row_threshold);
% 更新 stats.Row 字段
current_row = 0;
prev_center_y = -Inf;
for i = 1:length(stats)
bbox = stats(i).BoundingBox;
center_y = bbox(2) + bbox(4)/2; % 当前字符的垂直中心坐标
% 如果垂直间距大于阈值,则开始新行
if center_y - prev_center_y > row_threshold
current_row = current_row + 1;
end
% 更新 stats.Row 字段
stats(i).Row = current_row;
prev_center_y = center_y;
end
%创建副本元胞数组
all_row_stats = {};
all_row_bboxes = {};
log_entries = {};
all_row_ocr_results={};
for row_num = 1:max([stats.Row]) % 根据 Row 字段的最大值确定行数
% 提取当前行的字符索引
row_indices = [stats.Row] == row_num;
% 创建当前行的 stats 副本
row_stats = stats(row_indices);
row_bboxes = vertcat(row_stats.BoundingBox);
% 按垂直位置排序
[~, order] = sort(row_bboxes(:,1));
row_bboxes = row_bboxes(order, :);
row_stats = row_stats(order);
% ================= 新增:过滤除号小圆点 =================
updateProgress(30, '过滤除号小圆点...');
filtered_stats = []; % 初始化过滤后的 stats
filtered_bboxes = []; % 初始化过滤后的边界框
% 计算所有区域的平均面积
all_areas = [row_stats.Area]; % 提取所有区域的面积
mean_area = mean(all_areas); % 计算平均面积
% 遍历每个区域,标记需要移除的区域索引
remove_indices = [];
for i = 1:length(row_stats)
try
bbox = row_stats(i).BoundingBox; % 提取边界框
area = row_stats(i).Area; % 提取面积
width = bbox(3); % 宽度
height = bbox(4); % 高度
% 检查宽度和高度是否有效
if width <= 0 || height <= 0
log_entries{end+1} = sprintf('跳过区域 %d: 无效尺寸 (宽 %.2f, 高 %.2f)', ...
i, width, height);
remove_indices = [remove_indices, i];
continue;
end
% 判断条件:
% 1. 面积小于平均面积的某个比例(例如 10%)
% 2. 高宽比接近 1(例如在 0.8 到 1.2 范围内)
aspect_ratio = abs(width - height) / max(width, height); % 计算高宽比偏差
if area < 0.2 * mean_area && aspect_ratio < 0.2
log_entries{end+1} = sprintf('过滤掉区域 %d: 小圆点 (面积 %.2f, 高宽比 %.2f)', ...
i, area, aspect_ratio);
remove_indices = [remove_indices, i]; % 标记为移除
end
catch ME
% 捕获异常并记录日志
log_entries{end+1} = sprintf('处理区域 %d 时发生错误: %s', i, ME.message);
remove_indices = [remove_indices, i]; % 标记为移除
end
end
% 移除标记的区域
if ~isempty(remove_indices)
remaining_indices = setdiff(1:length(row_stats), remove_indices); % 剩余区域索引
% 如果有剩余区域,则更新 stats 和 bboxes
if ~isempty(remaining_indices)
row_stats = row_stats(remaining_indices); % 更新 stats
row_bboxes = row_bboxes(remaining_indices, :);
else
% 如果没有剩余区域,初始化为空
row_stats = [];
row_bboxes = [];
end
else
% 如果没有移除任何区域,直接提取边界框
% bboxes = vertcat([stats.BoundingBox]);
end
% 按水平位置重新排序
if ~isempty(row_bboxes)
[~, row_order] = sort(row_bboxes(:,1)); % 按 x 坐标排序
row_bboxes = row_bboxes(row_order, :);
row_stats = row_stats(row_order);
end
hold off;
title(sprintf('算式 %d 过滤后分割出 %d 个区域', row_num,size(row_bboxes, 1)));
drawnow;
% % 存储更新后的区域信息
% handles.region_stats = stats;
% handles.region_bboxes = bboxes;
% guidata(gcbf, handles);
% ================= 第二步:等号检测与合并 =================
updateProgress(30+row_num*10, '检测等号...');
axes(handles.ax_equal);
if row_num==1
imshow(inverted_img);
end
title('等号检测过程');
hold on;
% log_entries = {};
candidate_pairs = [];
% 找出所有类似减号的区域
dash_like_indices = [];
for i = 1:length(row_stats)
bbox = row_bboxes(i,:);
w = bbox(3);
h = bbox(4);
% 减号特征:高宽比大,离心率高
aspect_ratio = w / h;
eccentricity = row_stats(i).Eccentricity;
orientation = row_stats(i).Orientation; % 获取方向属性
if aspect_ratio > 1.5 && eccentricity > 0.85 && abs(orientation) < 10
dash_like_indices = [dash_like_indices; i];
rectangle('Position', bbox, 'EdgeColor', [0 0.8 0.8], 'LineWidth', 1.5);
text(bbox(1)+5, bbox(2)-10, sprintf('%d', i), ...
'Color', [0 0.5 0.5], 'FontSize', 10, 'FontWeight', 'bold');
end
end
log_entries{end+1} = sprintf('找到 %d 个减号样区域', length(dash_like_indices));
% 垂直探索寻找等号对
for i = 1:length(dash_like_indices)
idx1 = dash_like_indices(i);
bbox1 = row_bboxes(idx1,:);
center_x1 = bbox1(1) + bbox1(3)/2;
center_y1 = bbox1(2) + bbox1(4)/2;
% 在垂直方向探索
for j = 1:length(dash_like_indices)
if i == j, continue; end % 跳过自身
idx2 = dash_like_indices(j);
bbox2 = row_bboxes(idx2,:);
center_x2 = bbox2(1) + bbox2(3)/2;
center_y2 = bbox2(2) + bbox2(4)/2;
% 检查垂直位置关系
vertical_gap = abs(center_y1 - center_y2);
avg_height = (bbox1(4) + bbox2(4))/2;
% 检查水平对齐
horizontal_diff = abs(center_x1 - center_x2);
avg_width = (bbox1(3) + bbox2(3))/2;
% 检查尺寸相似性
width_ratio = max(bbox1(3), bbox2(3)) / min(bbox1(3), bbox2(3));
height_ratio = max(bbox1(4), bbox2(4)) / min(bbox1(4), bbox2(4));
% 等号对条件:
% 1. 垂直距离在合理范围(1-3倍平均高度)
% 2. 水平位置对齐(中心偏差小于宽度的一半)
% 3. 尺寸相似(宽高比小于1.5)
if vertical_gap > 0.5*avg_height && vertical_gap < 5*avg_height && ...
horizontal_diff < avg_width/2 && ...
width_ratio < 2 && height_ratio < 2
% 检查是否已经配对
if ~isempty(candidate_pairs) && any(ismember([idx1, idx2], candidate_pairs(:)))
continue;
end
% 标记候选对
candidate_pairs = [candidate_pairs; idx1, idx2];
% 绘制连接线
plot([center_x1, center_x2], [center_y1, center_y2], 'g-', 'LineWidth', 1.5);
log_entry = sprintf('发现等号候选对: %d 和 %d (垂直距离: %.1f, 水平偏差: %.1f)', ...
idx1, idx2, vertical_gap, horizontal_diff);
log_entries{end+1} = log_entry;
end
end
end
% 处理等号候选对
equal_sign_index = [];
if ~isempty(candidate_pairs)
% 选择最可能的一对(水平对齐最好)
[~, best_idx] = min(abs(candidate_pairs(:,1) - candidate_pairs(:,2)));
best_pair = candidate_pairs(best_idx, :);
% 计算合并后的边界框
bbox1 = row_bboxes(best_pair(1), :);
bbox2 = row_bboxes(best_pair(2), :);
min_x = min(bbox1(1), bbox2(1));
max_x = max(bbox1(1)+bbox1(3), bbox2(1)+bbox2(3));
min_y = min(bbox1(2), bbox2(2));
max_y = max(bbox1(2)+bbox1(4), bbox2(2)+bbox2(4));
equal_bbox = [min_x, min_y, max_x-min_x, max_y-min_y];
% 创建完整的等号区域属性结构体
equal_region = struct(...
'BoundingBox', equal_bbox, ...
'Area', equal_bbox(3)*equal_bbox(4), ...
'Eccentricity', 0.9, ... % 等号通常是细长的
'Orientation', 0, ... % 水平方向
'Solidity', 0.7, ... % 中等实心度
'IsEqual', 1 ,...
'Row',row_num); % 标记为等号区域
% 确保字段顺序一致
if ~isempty(row_stats)
equal_region = orderfields(equal_region, fieldnames(row_stats));
end
% 标记等号区域
rectangle('Position', equal_bbox, 'EdgeColor', [0.2 0.8 0.2], 'LineWidth', 3);
text(equal_bbox(1)+5, equal_bbox(2)-15, '等号', ...
'Color', [0 0.6 0], 'FontSize', 12, 'FontWeight', 'bold');
% 更新区域列表 - 移除原始的两个区域
remaining_indices = setdiff(1:length(row_stats), best_pair);
row_bboxes = row_bboxes(remaining_indices, :);
row_stats = row_stats(remaining_indices);
% 安全连接结构体
if isempty(row_stats)
row_stats = equal_region; % 当没有剩余区域时
else
% 确保列向量结构
if size(row_stats, 2) > 1
row_stats = row_stats';
end
% 连接结构体
row_stats = [row_stats; equal_region];
end
% 添加合并后的等号区域
row_bboxes = [row_bboxes; equal_bbox];
% 重新排序并记录等号位置
[~, row_order] = sort(row_bboxes(:,1));
original_positions = 1:size(row_bboxes,1);
new_order_indices = original_positions(row_order);
% 关键修复:正确计算等号位置
equal_position_in_original = size(row_bboxes,1); % 等号添加在末尾
equal_sign_index = find(new_order_indices == equal_position_in_original);
% 应用排序
row_bboxes = row_bboxes(row_order, :);
row_stats = row_stats(row_order);
log_entries{end+1} = sprintf('确定等号: 区域 %d 和 %d 合并为区域 %d', ...
best_pair(1), best_pair(2), equal_sign_index);
else
log_entries{end+1} = '未找到有效的等号对';
equal_sign_index = [];
end
hold off;
% 更新日志
set(handles.equal_log, 'String', log_entries);
% % 存储更新后的区域信息
% handles.region_stats = stats;
% handles.region_bboxes = bboxes;
% handles.equal_sign_index = equal_sign_index;
% % 关键修复:保存更新后的handles
% guidata(gcbf, handles);
updateProgress(30+row_num*20, '减号二次识别...');
% 找出所有类似减号的区域(排除已标记为等号的区域)
dash_like_indices = [];
for i = 1:length(row_stats)
if row_stats(i).IsEqual
continue; % 跳过等号区域
end
bbox = row_bboxes(i,:);
w = bbox(3);
h = bbox(4);
% 减号特征:高宽比大,离心率高
aspect_ratio = w / h;
eccentricity = row_stats(i).Eccentricity;
orientation = row_stats(i).Orientation; % 获取方向属性
if aspect_ratio > 1.5 && eccentricity > 0.85 && abs(orientation) < 10
dash_like_indices = [dash_like_indices; i];
row_stats(i).IsEqual = 3;
handles.region_row_stats = row_stats;
guidata(gcbf, handles); % 保存更新后的 handles
rectangle('Position', bbox, 'EdgeColor', [0 0.8 0.8], 'LineWidth', 1.5);
text(bbox(1)+5, bbox(2)-10, sprintf('%d', i), ...
'Color', [0 0.5 0.5], 'FontSize', 10, 'FontWeight', 'bold');
end
end
log_entries{end+1} = sprintf('找到 %d 个减号样区域', length(dash_like_indices));
%% 对每个疑似减号区域进行二次识别
division_sign_indices = [];
for i = 1:length(dash_like_indices)
idx = dash_like_indices(i);
bbox = row_bboxes(idx,:);
center_x = bbox(1) + bbox(3)/2;
center_y = bbox(2) + bbox(4)/2;
bbox = row_stats(idx).BoundingBox;
%center_x = bbox(1) + bbox(3)/2; % 区域中心的 x 坐标
top_y = bbox(2); % 区域顶部 y 坐标
bottom_y = bbox(2) + bbox(4); % 区域底部 y 坐标
% 定义搜索范围 (从上下边界向外扩展)
search_radius = round(max(bbox(3), bbox(4)) * 1); % 搜索半径增大到 1.5 倍
% 搜索上方区域
top_start = max(1, top_y - search_radius); % 上方搜索起始位置
top_end = top_y - 1; % 上方搜索结束位置(不包含当前区域)
if top_start <= top_end % 确保搜索范围有效
top_region = inverted_img(top_start:top_end, ...
max(1, center_x - search_radius):min(img_w, center_x + search_radius));
else
top_region = []; % 如果搜索范围无效,则置为空
end
% 搜索下方区域
bottom_start = bottom_y + 1; % 下方搜索起始位置(不包含当前区域)
bottom_end = min(img_h, bottom_y + search_radius); % 下方搜索结束位置
if bottom_start <= bottom_end % 确保搜索范围有效
bottom_region = inverted_img(bottom_start:bottom_end, ...
max(1, center_x - search_radius):min(img_w, center_x + search_radius));
else
bottom_region = []; % 如果搜索范围无效,则置为空
end
% 检查上下是否存在白色的小方块
top_white_pixels = sum(top_region(:)) > numel(top_region)*0.01; % 至少1%为白色
bottom_white_pixels = sum(bottom_region(:)) > numel(bottom_region)*0.01;
if top_white_pixels && bottom_white_pixels
division_sign_indices = [division_sign_indices; idx];
% 扩展边界框以包含上下白色区域 (扩展倍数增加到3.0)
extended_top = max(1, bbox(2) - round(bbox(4)*3.0)); % 增加到3.0倍
extended_bottom = min(img_h, bbox(2) + bbox(4) + round(bbox(4)*3.0)); % 增加到3.0倍
extended_bbox = [bbox(1), extended_top, bbox(3), extended_bottom - extended_top];
% 更新区域信息
row_stats(idx).BoundingBox = extended_bbox;
row_bboxes(idx,:) = extended_bbox;
% 创建新的除号区域属性结构体
division_region = struct(...
'BoundingBox', extended_bbox, ...
'Area', extended_bbox(3)*extended_bbox(4), ...
'Eccentricity', 0.8, ... % 除号通常是细长的
'Orientation', 0, ... % 水平方向
'Solidity', 0.6, ... % 中等实心度
'IsEqual', 2 ,...
'Row',row_num); % 标记为非等号区域
% 确保字段顺序一致
if ~isempty(row_stats)
division_region = orderfields(division_region, fieldnames(row_stats));
end
% 替换原始减号区域
row_stats(idx) = division_region;
% 标记为除号
rectangle('Position', extended_bbox, 'EdgeColor', [0.2 0.8 0.2], 'LineWidth', 1.5);
text(bbox(1)+5, bbox(2)-10, '÷', ...
'Color', [0 0.6 0], 'FontSize', 12, 'FontWeight', 'bold');
log_entries{end+1} = sprintf('区域 %d: 被标记为除号', idx);
end
end
if isempty(division_sign_indices)
log_entries{end+1} = '未找到有效的除号';
else
% % 同步更新 handles 中的区域信息
% handles.region_row_stats = row_stats;
% handles.region_row_bboxes = row_bboxes;
% guidata(gcbf, handles); % 保存更新后的 handles
end
% 更新日志
set(handles.equal_log, 'String', log_entries);
handles.equal_sign_index = equal_sign_index;
guidata(gcbf, handles);
% ================= 第三步:字符识别 =================
updateProgress(30+row_num*30, '字符识别...');
% % 关键修复:使用更新后的区域列表
% row_stats = handles.region_row_stats;
% row_bboxes = handles.region_row_bboxes;
ocr_results = cell(1, length(row_stats));
for i = 1:length(row_stats)
% 更新进度
updateProgress(30+row_num*30 + round(40*i/length(row_stats)), sprintf('识别字符 %d/%d', i, length(row_stats)));
% 根据 IsEqual 字段直接读取符号
switch row_stats(i).IsEqual
case 1 % 等号
ocr_results{i} = '=';
log_entries{end+1} = sprintf('区域 %d: 已标记为等号', i);
case 2 % 除号
ocr_results{i} = '÷';
log_entries{end+1} = sprintf('区域 %d: 已标记为除号', i);
case 3 % 减号
ocr_results{i} = '-';
log_entries{end+1} = sprintf('区域 %d: 已标记为减号', i);
otherwise % 其他字符
% 提取字符区域
bbox = row_stats(i).BoundingBox;
% 放大裁剪框(增加5%的边距)
pad_ratio = 0.05; % 边距比例
x = floor(max(1, bbox(1) - bbox(3)*pad_ratio));
y = floor(max(1, bbox(2) - bbox(4)*pad_ratio));
w = ceil(min(img_w - x, bbox(3)*(1 + 2*pad_ratio)));
h = ceil(min(img_h - y, bbox(4)*(1 + 2*pad_ratio)));
% 安全提取区域图像
y1 = max(1, y);
y2 = min(img_h, y+h-1);
x1 = max(1, x);
x2 = min(img_w, x+w-1);
if y2 < y1 || x2 < x1
ocr_results{i} = '?';
continue;
end
region_img = inverted_img(y1:y2, x1:x2);
% 调用改进的 OCR 函数
[char, ~] = improvedOCR(region_img);
ocr_results{i} = char;
% 实时更新日志和进度提示
log_entries{end+1} = sprintf('区域 %d: 识别结果为 "%s"', i, char);
end
% 更新进度条下方的文本提示
set(handles.progress_text, 'String', sprintf('识别字符 %d/%d: 结果为 "%s"', i, length(row_stats), ocr_results{i}));
drawnow; % 立即刷新界面
end
% ================= 第四步:构建表达式 =================
updateProgress(30+row_num*65, '构建表达式...');
% 分割表达式和答案
expr_str = '';
answer_str = '';
% 找到等号位置
eq_idx = [];
if ~isempty(handles.equal_sign_index)
eq_idx = handles.equal_sign_index;
% 验证索引有效性
if eq_idx < 1 || eq_idx > length(ocr_results)
log_entries = get(handles.equal_log, 'String');
log_entries{end+1} = sprintf('警告: 等号索引 %d 超出范围 [1-%d]', eq_idx, length(ocr_results));
set(handles.equal_log, 'String', log_entries);
eq_idx = round(length(ocr_results)/2); % 使用备用方案
end
else
% 如果没有找到等号,使用位置最中间的区域作为等号
[~, eq_idx] = min(abs(1:length(row_stats) - length(row_stats)/2));
log_entries = get(handles.equal_log, 'String');
log_entries{end+1} = sprintf('未找到等号,使用区域 %d 作为等号', eq_idx);
set(handles.equal_log, 'String', log_entries);
ocr_results{eq_idx} = '=';
end
% 关键修复:确保等号区域被正确识别
if ~strcmp(ocr_results{eq_idx}, '=')
log_entries = get(handles.equal_log, 'String');
log_entries{end+1} = sprintf('警告: 区域 %d 不是等号,强制设为等号', eq_idx);
set(handles.equal_log, 'String', log_entries);
ocr_results{eq_idx} = '=';
end
for i = 1:length(ocr_results)
if i < eq_idx
expr_str = [expr_str ocr_results{i}];
elseif i > eq_idx
answer_str = [answer_str ocr_results{i}];
end
end
% 特殊字符转换
expr_str = strrep(expr_str, 'x', '*');
expr_str = strrep(expr_str, 'X', '*');
expr_str = strrep(expr_str, '÷', '/');
expr_str = strrep(expr_str, ' ', '');
% 尝试转换为数值
try
user_answer = str2double(answer_str);
catch
user_answer = NaN;
end
% 安全计算表达式
try
correct_result = eval(expr_str);
catch
% 修正常见OCR错误
expr_str = strrep(expr_str, 'O', '0');
expr_str = strrep(expr_str, 'o', '0');
expr_str = strrep(expr_str, 'l', '1');
expr_str = strrep(expr_str, 'I', '1');
expr_str = strrep(expr_str, 's', '5');
expr_str = strrep(expr_str, 'z', '2');
try
correct_result = eval(expr_str);
catch ME
log_entries = get(handles.equal_log, 'String');
log_entries{end+1} = sprintf('计算错误: %s', ME.message);
set(handles.equal_log, 'String', log_entries);
correct_result = NaN;
end
end
% 验证结果
is_correct = abs(user_answer - correct_result) < 1e-6;
% 更新结果展示
set(handles.formula_text, 'String', [expr_str '=' answer_str]);
set(handles.calc_text, 'String', num2str(correct_result));
set(handles.answer_text, 'String', num2str(user_answer));
if is_correct
set(handles.result_text, 'String', '✓ 答案正确', 'ForegroundColor', [0 0.6 0]);
else
set(handles.result_text, 'String', '✗ 答案错误', 'ForegroundColor', [0.8 0 0]);
end
% 显示最终识别结果
axes(handles.ax_processed);
if row_num==1
imshow(inverted_img);
end
hold on;
for i = 1:length(row_stats)
bbox = row_stats(i).BoundingBox;
% 等号区域用绿色标注
if row_stats(i).IsEqual
switch row_stats(i).IsEqual
case 1 % 等号
rect_color = [0.2 0.8 0.2]; % 绿色
text_color = [0 0.5 0]; % 深绿色
symbol = '=';
case 2 % 除号
rect_color = [0.8 0.2 0.5]; % 粉色
text_color = [0.6 0 0.3]; % 深粉色
symbol = '÷';
case 3 % 减号
rect_color = [0.8 0.5 0.2]; % 橙色
text_color = [0.6 0.3 0]; % 深橙色
symbol = '-';
otherwise % 其他字符
rect_color = [1 0 0]; % 红色
text_color = [0 0 1]; % 蓝色
symbol = ocr_results{i};
end
% 绘制边界框
rectangle('Position', bbox, 'EdgeColor', rect_color, 'LineWidth', 1.5);
% 添加文本标注
if ~isempty(symbol)
text(bbox(1)+5, bbox(2)-10, symbol, ...
'Color', text_color, 'FontSize', 10, 'FontWeight', 'bold');
end
else
% 运算符区域用不同颜色标注
if strcmp(ocr_results{i}, '+')
rect_color = [0.8 0.2 0.8]; % 紫色
text_color = [0.6 0 0.6]; % 深紫色
elseif strcmp(ocr_results{i}, '-')
rect_color = [0.8 0.5 0.2]; % 橙色
text_color = [0.6 0.3 0]; % 深橙色
elseif strcmp(ocr_results{i}, '*') || strcmp(ocr_results{i}, '×')
rect_color = [0.2 0.5 0.8]; % 蓝色
text_color = [0.1 0.3 0.6]; % 深蓝色
elseif strcmp(ocr_results{i}, '/') || strcmp(ocr_results{i}, '÷')
rect_color = [0.8 0.2 0.5]; % 粉色
text_color = [0.6 0 0.3]; % 深粉色
else
rect_color = [1 0 0]; % 红色
text_color = [0 0 1]; % 蓝色
end
rectangle('Position', bbox, 'EdgeColor', rect_color, 'LineWidth', 1.5);
if ~isempty(ocr_results{i})
text(bbox(1)+5, bbox(2)-10, ocr_results{i}, ...
'Color', text_color, 'FontSize', 10, 'FontWeight', 'bold');
end
end
end
hold off;
% 将当前行的 stats 副本存入 all_row_stats
all_row_stats{end+1} = row_stats;
all_row_bboxes{end+1} = row_bboxes;
all_row_ocr_results{end+1} = ocr_results;
end
title('最终识别结果');
% 正确合并所有行 - 创建新的结构体数组
if ~isempty(all_row_stats)
% 方法1: 使用 vertcat 直接合并
try
stats_combined = vertcat(all_row_stats{:});
catch
% 方法2: 处理不同尺寸结构体的替代方案
stats_combined = struct();
for i = 1:numel(all_row_stats)
if ~isempty(all_row_stats{i})
stats_combined = [stats_combined; all_row_stats{i}(:)];
end
end
end
else
stats_combined = struct([]); % 空结构体
end
% 替换原始 stats
stats = stats_combined;
% 如果需要将所有行的 Bboxes 副本按顺序连接起来
reconstructed_ocr_results = [];
for i = 1:length(all_row_bboxes)
bboxes = [bboxes; all_row_bboxes{i}];
end
for i = 1:length(all_row_ocr_results)
reconstructed_ocr_results = [reconstructed_ocr_results, all_row_ocr_results{i}];
end
% 保存处理后的数据
handles.ocr_results = ocr_results;
guidata(gcbf, handles);
updateProgress(30+row_num*100, '公式识别完成');
catch ME
updateProgress(0, sprintf('错误: %s', ME.message));
errordlg(sprintf('识别失败: %s', ME.message), '错误');
end
end
function clearResults(~, ~)
handles = guidata(gcbf);
% 清除结果文本
set(handles.formula_text, 'String', '');
set(handles.calc_text, 'String', '');
set(handles.answer_text, 'String', '');
set(handles.result_text, 'String', '');
set(handles.equal_log, 'String', {});
% 清除图像
axes(handles.ax_processed);
cla;
title('处理图像');
axes(handles.ax_equal);
cla;
title('等号检测');
% 清除进度条
set(handles.progress_bar, 'Position', [0,0,0,1]);
set(handles.progress_text, 'String', '就绪');
% 如果有原始图像,重新显示
if ~isempty(handles.current_image)
axes(handles.ax_original);
imshow(handles.current_image);
title('原始图像');
end
% 保存handles状态
guidata(gcbf, handles);
end
% 更新进度条函数
function updateProgress(percent, message)
handles = guidata(gcbf); % 获取当前 figure 的 handles
% 确保进度条存在
if isfield(handles, 'progress_bar') && ishandle(handles.progress_bar)
set(handles.progress_bar, 'Position', [0,0,percent,1]);
end
if isfield(handles, 'progress_text') && ishandle(handles.progress_text)
set(handles.progress_text, 'String', message);
end
drawnow; % 立即更新显示
% 保存 handles 状态
guidata(gcbf, handles);
end
end
% ======================== 运算符检测函数 ========================
function [is_operator, operator_type] = detectOperator(region_img)
[h, w] = size(region_img);
% 计算形状特征
aspect_ratio = w / h;
eccentricity = regionprops(region_img, 'Eccentricity');
if ~isempty(eccentricity)
eccentricity = eccentricity.Eccentricity;
else
eccentricity = 0;
end
is_operator = false;
operator_type = '';
% 减号检测
if aspect_ratio > 3 && eccentricity > 0.9
is_operator = true;
operator_type = '-';
return;
end
% 加号检测
if aspect_ratio > 1.2 && aspect_ratio < 2.5
% 中心区域分析
center_y = round(h/2);
center_x = round(w/2);
% 检查水平和垂直线段
horizontal_line = sum(region_img(center_y, :)) > w*0.7;
vertical_line = sum(region_img(:, center_x)) > h*0.7;
if horizontal_line && vertical_line
is_operator = true;
operator_type = '+';
return;
end
end
% 乘号检测(斜线)
if aspect_ratio > 0.8 && aspect_ratio < 1.2
% 使用Hough变换检测斜线
[H, theta, rho] = hough(region_img, 'Theta', -45:5:45);
peaks = houghpeaks(H, 2);
if size(peaks,1) >= 2
angles = theta(peaks(:,2));
angle_diff = abs(diff(angles));
% 检查是否接近垂直的斜线对
if abs(angle_diff) > 80 && abs(angle_diff) < 100
is_operator = true;
operator_type = '*';
return;
end
end
end
% 除号检测(点)
if aspect_ratio > 0.8 && aspect_ratio < 1.2
% 检测中心点
center_y = round(h/2);
center_x = round(w/2);
if center_x > 0 && center_y > 0 && center_x <= w && center_y <= h
if region_img(center_y, center_x)
is_operator = true;
operator_type = '/';
return;
end
end
end
% 等号检测(特殊情况)
if aspect_ratio > 2.5 && eccentricity > 0.95
is_operator = true;
operator_type = '=';
end
end
% ======================== 数字校正函数 ========================
function char = correctDigit(region_img, original_char)
[h, w] = size(region_img);
% 1的特征:高宽比大,顶部无横线,底部较宽
aspect_ratio = h / w;
top_region = region_img(1:round(h*0.3), :);
bottom_region = region_img(round(h*0.7):end, :);
top_pixels = sum(top_region(:));
bottom_pixels = sum(bottom_region(:));
% 7的特征:顶部有横线,右上角有折角
top_line = sum(region_img(1, :)) > w*0.6;
right_top_corner = false;
if w > 1 && h > 1
right_top_corner = region_img(1, end) && region_img(2, end);
end
% 0的特征:封闭轮廓
filled_area = bwarea(region_img);
convex_area = regionprops(region_img, 'ConvexArea');
if ~isempty(convex_area)
convex_area = convex_area.ConvexArea;
solidity = filled_area / convex_area;
else
solidity = 0;
end
% 8的特征:两个封闭区域
holes = regionprops(region_img, 'EulerNumber');
if ~isempty(holes)
num_holes = 1 - holes.EulerNumber; % 欧拉数 = 1 - 孔洞数
else
num_holes = 0;
end
switch original_char
case '7'
% 检查是否为1的特征
if aspect_ratio > 3 && top_pixels < numel(top_region)*0.2 && bottom_pixels > numel(bottom_region)*0.5
char = '1';
return;
end
case '1'
% 检查是否为7的特征
if aspect_ratio < 2 && top_line && right_top_corner
char = '7';
return;
end
case '0'
% 检查是否为8的特征(有孔洞)
if num_holes > 0
char = '8';
return;
end
case '8'
% 检查是否为0的特征(无孔洞)
if num_holes == 0 && solidity > 0.9
char = '0';
return;
end
case '5'
% 检查是否为6的特征(底部封闭)
bottom_closed = sum(region_img(end, :)) > w*0.6;
if bottom_closed
char = '6';
return;
end
case '6'
% 检查是否为5的特征(底部开放)
bottom_open = sum(region_img(end, :)) < w*0.4;
if bottom_open
char = '5';
return;
end
end
char = original_char; % 保持原识别结果
end
function [char, confidence] = improvedOCR(region_img)
% 确保输入图像为二值图像
if ~islogical(region_img)
error('输入图像必须是二值图像');
end
% 获取图像尺寸
[height, width] = size(region_img);
% 定义目标尺寸
target_size = 200;
% 计算填充量
pad_top = max(0, floor((target_size - height) / 2));
pad_bottom = max(0, ceil((target_size - height) / 2));
pad_left = max(0, floor((target_size - width) / 2));
pad_right = max(0, ceil((target_size - width) / 2));
% 对图像进行填充
region_img_padded = padarray(region_img, [pad_top, pad_left], 'post');
region_img_padded = padarray(region_img_padded, [pad_bottom, pad_right], 'pre');
% 确保填充后的图像大小符合要求
if size(region_img_padded, 1) < target_size || size(region_img_padded, 2) < target_size
error('填充后的图像大小不符合要求');
end
%region_img_processed = thinningPreprocessing(region_img_padded);
region_img_processed = edgeEnhancement(region_img_padded);
% 定义扩展字符集;
char_set = '0123456789+-*/=Xx';
% 设置置信度阈值
confidence_threshold = 0.3;
% 使用 OCR 函数
try
ocr_result = ocr(region_img_processed, 'CharacterSet', char_set, 'TextLayout', 'Block' );
% 检查 OCR 返回值是否为有效的 ocrText 对象
if ~isa(ocr_result, 'ocrText') || isempty(ocr_result.Text)
% 如果 OCR 未能识别出任何字符,则尝试基于几何特征进行校正
char = detectCharacterByGeometry(region_img);
confidence = 0;
else
% 提取 Text 的第一个字符
first_char = ocr_result.Text(1); % 直接提取第一个字符
% 检查第一个字符是否在定义的字符集中
if contains(char_set, first_char)
char = first_char; % 如果有效,则记录该字符
else
char = '?'; % 如果无效,则返回问号
end
% 优先使用 WordConfidence
word_confidence = ocr_result.WordConfidences;
if word_confidence >= confidence_threshold
confidence = word_confidence;
else
char = '?'; % 如果无法获取任何置信度信息,返回问号
confidence = 0;
end
end
catch ME
% 记录错误日志
fprintf('OCR 错误: %s\n', ME.message);
fprintf('OCR 返回类型: %s\n', class(ocr_result));
char = '?'; % 如果 OCR 失败,返回问号
confidence = 0;
end
% 特殊字符校正
char = strrep(char, 'x', '*');
char = strrep(char, 'X', '*');
char = strrep(char, '÷', '/');
char = strrep(char, ' ', '');
% 针对 '6' 的检测与校正逻辑
if strcmp(char, '?')
% 如果 OCR 未能识别出字符,则尝试基于几何特征进行校正
char = detectCharacterByGeometry(region_img);
end
end
% ======================== 字符检测函数 ========================
function detected_char = detectCharacterByGeometry(region_img)
% 计算形状特征
stats = regionprops(region_img, 'BoundingBox', 'Eccentricity', 'Solidity', 'Orientation', 'EulerNumber', 'Extent');
if isempty(stats)
detected_char = '?';
return;
end
bbox = stats.BoundingBox;
aspect_ratio = bbox(4) / bbox(3); % 高宽比
eccentricity = stats.Eccentricity; % 离心率
solidity = stats.Solidity; % 实心度
orientation = stats.Orientation; % 方向
euler_number = stats.EulerNumber; % 欧拉数
extent = stats.Extent; % 填充比例
% 判断是否为 '6'
if aspect_ratio > 1.2 && aspect_ratio < 2.5 && ...
eccentricity < 0.8 && solidity > 0.4 && ...
abs(orientation) > 80 && euler_number ~= 1
detected_char = '6';
% 判断是否为 '3'
elseif aspect_ratio > 0.8 && aspect_ratio < 1.8 && ...
eccentricity > 0.7 &&eccentricity < 0.9...
&& solidity > 0.4 && ...
euler_number == 1 && ... % '3' 的欧拉数通常为 1(有两个连通区域)
abs(orientation) > 80&&...
extent > 0.35 &&extent < 0.40
detected_char = '3';
% 判断是否为 '7'
elseif aspect_ratio > 1.5 && ... % '7' 的高宽比较大
eccentricity > 0.9 && ... % '7' 的离心率较高
solidity > 0.45 && ... % '7' 的实心度较高
extent < 0.35 && ... % '7' 的填充比例较高
abs(orientation) > 75 &&abs(orientation) < 85 % '7' 的方向接近水平
detected_char = '7';
% 判断是否为 '5'
elseif aspect_ratio > 1.0 && aspect_ratio < 2.0 && ...
eccentricity < 0.85 && solidity > 0.45 && ...
euler_number == 1 && ... % '5' 的欧拉数通常为 1(有两个连通区域)
abs(orientation) > 85 &&...% '5' 的方向接近水平
extent > 0.40
detected_char = '5';
else
detected_char = '?';
end
end
function [thinned_img] = thinningPreprocessing(img)
% 确保输入为二值图像
if ~islogical(img)
img = imbinarize(rgb2gray(im2double(img)));
end
% 字符瘦身(Thinning)
thinned_img = bwmorph(img, 'thin', Inf);
% 确保输出为二值图像
thinned_img = imbinarize(thinned_img);
end
function [sharpened_img] = edgeEnhancement(img)
% 确保输入为二值图像
if ~islogical(img)
img = imbinarize(rgb2gray(im2double(img)));
end
% 定义结构元素
se = strel('disk', 1);
% 腐蚀操作
eroded_img = imerode(img, se);
% 提取边缘
edge_img = img - eroded_img;
% 边缘增强:将边缘信息加回到原始图像中
sharpened_img = img + edge_img;
% 确保输出为二值图像
sharpened_img = imbinarize(sharpened_img);
end
function se_radius = calculateDilationRadius(inverted_img, img_h, img_w)
% 计算连通域
cc = bwconncomp(inverted_img, 4); % 使用 4 连通性
stats = regionprops(cc, 'Area', 'BoundingBox');
% 提取所有连通域的面积
areas = [stats.Area];
bboxes = vertcat(stats.BoundingBox);
% 去除小面积噪点
min_area_threshold = img_h * img_w * 0.0005; % 动态最小面积阈值
valid_idx = areas > min_area_threshold;
valid_areas = areas(valid_idx); % 有效区域的面积 (1xN 数组)
valid_bboxes = bboxes(valid_idx, :); % 有效区域的包围盒 (Nx4 数组)
% 如果没有有效区域,则设置默认半径
if isempty(valid_areas)
se_radius = 2; % 默认半径
return;
end
% 计算平均宽度和高度
widths = valid_bboxes(:,3); % 宽度 (Nx1 数组)
heights = valid_bboxes(:,4); % 高度 (Nx1 数组)
avg_width = mean(widths); % 平均宽度
avg_height = mean(heights); % 平均高度
% 计算有效区域的平均分辨率
avg_res = sqrt(avg_width * avg_height); % 平均分辨率
% 调整维度以确保逐元素运算正确
compactness = valid_areas ./ (widths' .* heights'); % 每个连通域的紧凑性
avg_compactness = 20 * mean(compactness(:)); % 紧凑性的平均值
% 边缘检测以获取字符边缘分布
edge_img = edge(inverted_img, 'Canny');
edge_density = 20 * sum(edge_img(:)) / numel(edge_img); % 边缘密度
% 综合计算字符粗细
char_thickness = sqrt(avg_width * avg_height * avg_compactness * edge_density);
% 定义幂次参数 p
p = 1.8; % 幂次参数,可以调整以控制非线性程度
% 使用对数函数结合幂次增强非线性度
adjusted_radius = (log(1 + avg_res / (char_thickness + 1e-6)))^p * 5;
% 根据字符粗细动态调整膨胀半径
se_radius = round(max(1, adjusted_radius));
% 根据字符粗细动态调整膨胀半径
% se_radius = round(max(1, 20 * char_thickness / avg_res));
% 限制膨胀半径范围
if se_radius > 10
se_radius = 10; % 最大半径限制
elseif se_radius < 1
se_radius = 1; % 最小半径限制
end
end
请参考我这里面使用的方法,预处理图像部分原样保留到二值化处理,之后的图像分割全部不要,然后把这种预处理放到刚才的无预处理代码中
最新发布