You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

斜向数字的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);
}

三、关键修改点说明

  1. 新增旋转矩形记录:在ContourWithData类中加入cv::RotatedRect,用于获取每个字符的倾斜角度。
  2. 角度校正逻辑:minAreaRect返回的角度范围是[-90,0),当矩形宽度大于高度时,需要加90度才能让字符垂直。
  3. 单字符旋转校正:对每个字符的ROI单独旋转,避免其他区域干扰,保证校正精度。
  4. 特征一致性保证:校正后的字符resize成和训练样本一致的20x30尺寸,确保KNN接收到的特征向量格式匹配。

为什么不能直接给KNN指定读取轴?

KNN的核心是对比特征向量的欧氏距离,你训练时的样本是垂直数字,像素是上下排列;倾斜后的数字像素是斜向分布,flatten后的特征向量顺序完全打乱,KNN根本无法匹配到正确的样本。所以必须先把字符校正到和训练样本相同的姿态,才能保证识别准确率。

内容的提问来源于stack exchange,提问作者mcally

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 08:14:28