斜向数字的KNN字符识别:需旋转图像还是指定读取轴?
解决倾斜数字的KNN字符识别问题
嗨,这个问题我做字符识别项目时也碰到过,咱们直接说核心结论:KNN字符识别没办法直接指定“读取轴”——它是基于像素的全局特征匹配,你训练用的是垂直的数字样本,倾斜后的数字像素分布和训练集完全不一致,自然会识别错误。所以必须先把倾斜的数字校正到垂直状态再识别,两种校正思路都可行:要么旋转整个图像,要么单独旋转每个字符区域,后者精度通常更高。
一、先搞清楚怎么检测倾斜角度
针对单行倾斜数字,常用两种检测方法:
- 方法1:整体行倾斜检测:对预处理后的二值图用霍夫变换(
cv::HoughLinesP)检测直线,计算所有直线的平均角度,以此作为图像的倾斜角度,再旋转整个图像。 - 方法2:单个字符轮廓校正:对每个数字的轮廓计算最小外接矩形(
cv::minAreaRect),这个矩形的角度就是字符的倾斜角度,单独旋转每个字符的ROI区域,确保字符垂直。
推荐用方法2,因为如果图像里有其他干扰元素,单行整体旋转可能不如单个字符校正精准。
二、结合你的代码修改实现校正
我先补全你截断的代码,再加入旋转校正的核心逻辑:
#include<opencv2/core/core.hpp> #include<opencv2/highgui/highgui.hpp> #include<opencv2/imgproc/imgproc.hpp> #include<opencv2/ml/ml.hpp> #include<stdio.h> #include<opencv2/opencv.hpp> #include<iostream> #include<sstream> // global variables /////////////////////////////////////////////////////////////////////////////// const int MIN_CONTOUR_AREA = 60; const int RESIZED_IMAGE_WIDTH = 20; const int RESIZED_IMAGE_HEIGHT = 30; /////////////////////////////////////////////////////////////////////////////////////////////////// class ContourWithData { public: std::vector<cv::Point> ptContour; // 轮廓点集 cv::Rect boundingRect; // 轴对齐外接矩形 cv::RotatedRect rotatedRect; // 新增:最小外接旋转矩形(用于获取倾斜角度) float fltArea; // 轮廓面积 bool checkIfContourIsValid() { if (fltArea < MIN_CONTOUR_AREA) return false; return true; } static bool sortByBoundingRectXPosition(const ContourWithData& cwdLeft, const ContourWithData& cwdRight) { return(cwdLeft.boundingRect.x < cwdRight.boundingRect.x); } }; /////////////////////////////////////////////////////////////////////////////////////////////////// int main() { std::vector<ContourWithData> allContoursWithData; std::vector<ContourWithData> validContoursWithData; // 读取训练分类数据(原有代码保留) cv::Mat matClassificationInts; cv::FileStorage fsClassifications("classifications.xml", cv::FileStorage::READ); if (fsClassifications.isOpened() == false) { std::cout << "error, unable to open training classifications file, exiting program\n\n"; return(0); } fsClassifications["classifications"] >> matClassificationInts; fsClassifications.release(); // 读取训练图像数据(原有代码保留) cv::Mat matTrainingImagesAsFlattenedFloats; cv::FileStorage fsTrainingImages("images.xml", cv::FileStorage::READ); if (fsTrainingImages.isOpened() == false) { std::cout << "error, unable to open training images file, exiting program\n\n"; return(0); } fsTrainingImages["images"] >> matTrainingImagesAsFlattenedFloats; fsTrainingImages.release(); // 训练KNN模型(原有代码保留) cv::Ptr<cv::ml::KNearest> kNearest(cv::ml::KNearest::create()); kNearest->train(matTrainingImagesAsFlattenedFloats, cv::ml::ROW_SAMPLE, matClassificationInts); // 读取测试图像(原有代码保留) cv::Mat matTestingNumbers = cv::imread("bc_sick_12_c.jpg"); if (matTestingNumbers.empty()) { std::cout << "error: image not read from file\n\n"; return(0); } // 图像预处理(补全你截断的代码) cv::Mat matGrayscale; cv::Mat matBlurred; cv::Mat matThresh; cv::Mat matThreshCopy; cv::cvtColor(matTestingNumbers, matGrayscale, cv::COLOR_BGR2GRAY); cv::GaussianBlur(matGrayscale, matBlurred, cv::Size(5, 5), 0); // 用自适应阈值处理,对光照不均的场景更友好 cv::adaptiveThreshold(matBlurred, matThresh, 255, cv::ADAPTIVE_THRESH_GAUSSIAN_C, cv::THRESH_BINARY_INV, 11, 2); // 复制阈值图用于轮廓检测(避免修改原图) matThreshCopy = matThresh.clone(); // 查找轮廓(补全代码) std::vector<std::vector<cv::Point> > ptContours; std::vector<cv::Vec4i> v4iHierarchy; cv::findContours(matThreshCopy, ptContours, v4iHierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); // 填充轮廓数据(新增旋转矩形记录) for (int i = 0; i < ptContours.size(); i++) { ContourWithData contourWithData; contourWithData.ptContour = ptContours[i]; contourWithData.boundingRect = cv::boundingRect(contourWithData.ptContour); contourWithData.rotatedRect = cv::minAreaRect(contourWithData.ptContour); // 计算最小外接旋转矩形 contourWithData.fltArea = cv::contourArea(contourWithData.ptContour); allContoursWithData.push_back(contourWithData); } // 筛选有效轮廓(原有逻辑保留) for (int i = 0; i < allContoursWithData.size(); i++) { if (allContoursWithData[i].checkIfContourIsValid()) { validContoursWithData.push_back(allContoursWithData[i]); } } // 按X坐标从左到右排序(原有逻辑保留) std::sort(validContoursWithData.begin(), validContoursWithData.end(), ContourWithData::sortByBoundingRectXPosition); // 识别每个字符(加入旋转校正核心逻辑) std::string strFinalString; for (int i = 0; i < validContoursWithData.size(); i++) { // 计算需要旋转的角度:处理minAreaRect的角度特殊情况 float angle = validContoursWithData[i].rotatedRect.angle; if (validContoursWithData[i].rotatedRect.size.width > validContoursWithData[i].rotatedRect.size.height) { angle += 90.0; // 当矩形宽>高时,说明字符是横向的,需要加90度校正 } // 获取旋转矩阵 cv::Mat matRotation = cv::getRotationMatrix2D(validContoursWithData[i].rotatedRect.center, angle, 1.0); // 旋转阈值图,得到校正后的图像 cv::Mat matRotated; cv::warpAffine(matThresh, matRotated, matRotation, matThresh.size(), cv::INTER_CUBIC); // 从旋转后的图像中提取字符ROI cv::Rect roiRect = cv::boundingRect(validContoursWithData[i].rotatedRect.points()); cv::Mat matROI = matRotated(roiRect); // 调整为训练样本的尺寸(保证特征一致性) cv::Mat matROIResized; cv::resize(matROI, matROIResized, cv::Size(RESIZED_IMAGE_WIDTH, RESIZED_IMAGE_HEIGHT)); // 转换为KNN需要的格式 cv::Mat matROIFlattenedFloat; matROIResized.convertTo(matROIFlattenedFloat, CV_32FC1); matROIFlattenedFloat = matROIFlattenedFloat.reshape(1, 1); // KNN预测 cv::Mat matCurrentChar; kNearest->findNearest(matROIFlattenedFloat, 1, matCurrentChar); char charCurrentChar = (char)matCurrentChar.at<float>(0, 0); strFinalString += charCurrentChar; } // 输出识别结果 std::cout << "识别结果:" << strFinalString << "\n"; // 显示图像(可选) cv::imshow("Original Image", matTestingNumbers); cv::imshow("Threshold Image", matThresh); cv::waitKey(0); return(0); }
三、关键修改点说明
- 新增旋转矩形记录:在
ContourWithData类中加入cv::RotatedRect,用于获取每个字符的倾斜角度。 - 角度校正逻辑:
minAreaRect返回的角度范围是[-90,0),当矩形宽度大于高度时,需要加90度才能让字符垂直。 - 单字符旋转校正:对每个字符的ROI单独旋转,避免其他区域干扰,保证校正精度。
- 特征一致性保证:校正后的字符resize成和训练样本一致的20x30尺寸,确保KNN接收到的特征向量格式匹配。
为什么不能直接给KNN指定读取轴?
KNN的核心是对比特征向量的欧氏距离,你训练时的样本是垂直数字,像素是上下排列;倾斜后的数字像素是斜向分布,flatten后的特征向量顺序完全打乱,KNN根本无法匹配到正确的样本。所以必须先把字符校正到和训练样本相同的姿态,才能保证识别准确率。
内容的提问来源于stack exchange,提问作者mcally
相关产品推荐
相关产品推荐

