OpenCV-字典法实现数字识别(尺寸归一化+图像差值)

简介: OpenCV-字典法实现数字识别(尺寸归一化+图像差值)

实现原理

      数字识别是图像处理学在现实生活中较多的应用之一,比如车牌识别、银行卡号识别、执照识别、文档文字OCR识别等等。如何实现数字识别呢?目前最为流行的就是运用AI、机器学习等技术结合图像处理学,大量训练数据集,以实现智能且精确的识别。说到人工智能,很多人可能觉得它非常深奥和复杂,其实说白了它最底层的识别逻辑还是基于普通的图像分析,像特征提取、轮廓分析、比对分析等等,再在庞大的数据集中按照相似程度,分析出一个最可能的结果。


      本文提供了一种相对简单的思路来实现数字识别,适合初学图像处理的新人研究参考。该方法为字典法:首先对图像进行阈值二值化处理,并适当清理微小噪声区;再结合轮廓分析,分割识别图像中的图形区域(不一定全是数字,也可能有其他的图形),并将图形区域尺寸归一化;对字典数字图同样进行分割和尺寸归一化;运用图像差值法,将识别图像的子区域分别比对字典中所有的数字子图,取其中差值最低的数字作为识别数字,若识别完成所有的差值均高于阈值,则识别错误;最后,在原图中给识别成功的子区域绘制最小包围矩形,并在上方书写所识别的数字。

      下方介绍具体流程。

具体流程

1)读取识别图像的原图和灰度图,读取字典图(灰度图)。将灰度图和字典图进行二值化处理。

cv::Mat src = imread("number.jpg");
cv::Mat gray = imread("number.jpg",0);
cv::Mat Template= imread("num.jpg", 0);
// 二值化
cv::Mat thresh,thresh_t;
cv::threshold(gray, thresh, 200, 255, THRESH_BINARY);
cv::threshold(Template, thresh_t,200, 255, THRESH_BINARY);

图1 识别图像二值图

图2 字典二值图


2)若识别图像中数字为深色,背景为白色,则反相;若同字典类似,数字为白,背景为黑,则不动。

// 反相
vector<Point> points;
findNonZero(thresh, points);
if (points.size() > (gray.rows*gray.cols / 2))
{
  thresh = 255 - thresh;
}

3)Clear_MicroConnected_Areas函数清除二值图中的微小噪声区,函数具体介绍见:

OpenCV-清除小面积连通域_翟天保的博客-CSDN博客

4)确认识别图像和字典图的轮廓区。

// 寻找轮廓
vector<vector<Point>> contour,contour_t;
vector<Vec4i> hierarchy, hierarchy_t;
findContours(thresh, contour, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_NONE);
findContours(thresh_t, contour_t, hierarchy_t, RETR_EXTERNAL, CHAIN_APPROX_NONE);

5)用GetSubarea函数实现子区域提取。

// 获取子区域集合
vector<cv::Mat> GetSubarea(cv::Mat src, vector<vector<Point>> contour, vector<Vec4i> hierarchy)
{
  vector<cv::Mat> result;
  if (!contour.empty() && !hierarchy.empty())
  {
    std::vector<std::vector<cv::Point> >::const_iterator itc = contour.begin();
    // 遍历所有轮廓
    while (itc != contour.end())
    {
      // 定位当前轮廓所在位置
      cv::Rect rect = cv::boundingRect(cv::Mat(*itc));
      cv::Mat temp = src(rect).clone();
      result.push_back(temp);
      itc++;
    }
  }
  return result;
}

6)用UniformSize函数实现子区域尺寸归一化。

// 统一尺寸
vector<cv::Mat> UniformSize(vector<cv::Mat> in)
{
  vector<cv::Mat> result;
  for (auto it = in.begin(); it != in.end(); ++it)
  {
    cv::Mat temp;
    resize(*it,temp,Size(48,48));
    result.push_back(temp);
  }
  return result;
}

7)用NumberRecognition函数实现数字识别。该函数输入四个参数,分别为识别图像原图、识别图像尺寸归一后子区域容器、字典图尺寸归一后子区域容器和阈值参数。其中阈值参数用来判断两图的差异性。

// 数字识别
cv::Mat NumberRecognition(cv::Mat Thresh, vector<cv::Mat> sub_size, vector<cv::Mat> sub_t_size, vector<vector<Point>> contour,int thresh)
{
  cv::Mat result = Thresh.clone();
  for (int i = 0; i < sub_size.size(); ++i)
  {
    int min = 999999;
    int idx = 0;
    for (int j = 0; j < sub_t_size.size(); ++j)
    {
      int d = calcDiff(sub_size[i], sub_t_size[j]);
      if (d < min)
      {
        min = d;
        idx = j;
      }
    }
    if (min < thresh)
    {
      cout << "第" << i + 1 << "个轮廓数字识别为:" << a[idx] << endl;
      cv::Rect rect = cv::boundingRect(contour[i]);
      rectangle(result, rect, Scalar(255,0,0), 1, 8);
      putText(result, to_string(a[idx]), Point(rect.x, rect.y - 20), 1, 2, Scalar(0,0,255));
    }
    else {
      cout << "第" << i + 1 << "个轮廓数字识别为: 空" << endl;
    }
  }
  return result;
}
// 对比图像差值
int calcDiff(cv::Mat src1, cv::Mat src2)
{
  CV_Assert(src1.size() == src2.size());
  cv::Mat dif;
  cv::absdiff(src1, src2, dif);
  Scalar Sum=cv::sum(dif);
  return int(Sum[0] / 255);
}

C++测试代码

#include <opencv2/opencv.hpp>
#include <iostream>
#include <algorithm>
#include <time.h>
using namespace cv;
using namespace std; 
vector<cv::Mat> UniformSize(vector<cv::Mat> in);
vector<cv::Mat> GetSubarea(cv::Mat src,vector<vector<Point>> contour, vector<Vec4i> hierarchy);
int calcDiff(cv::Mat src1, cv::Mat src2);
void Clear_MicroConnected_Areas(cv::Mat src, cv::Mat &dst, double min_area);
cv::Mat NumberRecognition(cv::Mat Thresh, vector<cv::Mat> sub_size, vector<cv::Mat> sub_t_size, vector<vector<Point>> contour, int thresh);
int a[10] = { 7,0,9,8,6,5,4,3,2,1 };
int main()
{
  cv::Mat src = imread("number.jpg");
  cv::Mat gray = imread("number.jpg",0);
  cv::Mat Template= imread("num.jpg", 0);
  // 二值化
  cv::Mat thresh,thresh_t;
  cv::threshold(gray, thresh, 200, 255, THRESH_BINARY);
  cv::threshold(Template, thresh_t,200, 255, THRESH_BINARY);
  // 反相
  vector<Point> points;
  findNonZero(thresh, points);
  if (points.size() > (gray.rows*gray.cols / 2))
  {
    thresh = 255 - thresh;
  }
  // 清除噪声区
  Clear_MicroConnected_Areas(thresh, thresh, 200);
  // 寻找轮廓
  vector<vector<Point>> contour,contour_t;
  vector<Vec4i> hierarchy, hierarchy_t;
  findContours(thresh, contour, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_NONE);
  findContours(thresh_t, contour_t, hierarchy_t, RETR_EXTERNAL, CHAIN_APPROX_NONE);
  // 分割子区域
  vector<cv::Mat> sub = GetSubarea(thresh, contour, hierarchy);
  vector<cv::Mat> sub_t = GetSubarea(thresh_t, contour_t, hierarchy_t);
  // 子区域尺寸统一化
  vector<cv::Mat> sub_size = UniformSize(sub);
  vector<cv::Mat> sub_t_size = UniformSize(sub_t);
  // 数字识别(字典法比对)
  int t = 400;
  cv::Mat result = NumberRecognition(src, sub_size, sub_t_size, contour, t);
  imshow("original", src);
  imshow("thresh", thresh);
  imshow("result", result);
  waitKey(0);
  return 0;
}
// 获取子区域集合
vector<cv::Mat> GetSubarea(cv::Mat src, vector<vector<Point>> contour, vector<Vec4i> hierarchy)
{
  vector<cv::Mat> result;
  if (!contour.empty() && !hierarchy.empty())
  {
    std::vector<std::vector<cv::Point> >::const_iterator itc = contour.begin();
    // 遍历所有轮廓
    while (itc != contour.end())
    {
      // 定位当前轮廓所在位置
      cv::Rect rect = cv::boundingRect(cv::Mat(*itc));
      cv::Mat temp = src(rect).clone();
      result.push_back(temp);
      itc++;
    }
  }
  return result;
}
// 统一尺寸
vector<cv::Mat> UniformSize(vector<cv::Mat> in)
{
  vector<cv::Mat> result;
  for (auto it = in.begin(); it != in.end(); ++it)
  {
    cv::Mat temp;
    resize(*it,temp,Size(48,48));
    result.push_back(temp);
  }
  return result;
}
// 对比图像差值
int calcDiff(cv::Mat src1, cv::Mat src2)
{
  CV_Assert(src1.size() == src2.size());
  cv::Mat dif;
  cv::absdiff(src1, src2, dif);
  Scalar Sum=cv::sum(dif);
  return int(Sum[0] / 255);
}
// 清除小区域
void Clear_MicroConnected_Areas(cv::Mat src, cv::Mat &dst, double min_area)
{
  // 备份复制
  dst = src.clone();
  std::vector<std::vector<cv::Point> > contours;  // 创建轮廓容器
  std::vector<cv::Vec4i>  hierarchy;
  // 寻找轮廓的函数
  // 第四个参数CV_RETR_EXTERNAL,表示寻找最外围轮廓
  // 第五个参数CV_CHAIN_APPROX_NONE,表示保存物体边界上所有连续的轮廓点到contours向量内
  cv::findContours(src, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE, cv::Point());
  if (!contours.empty() && !hierarchy.empty())
  {
    std::vector<std::vector<cv::Point> >::const_iterator itc = contours.begin();
    // 遍历所有轮廓
    while (itc != contours.end())
    {
      // 定位当前轮廓所在位置
      cv::Rect rect = cv::boundingRect(cv::Mat(*itc));
      // contourArea函数计算连通区面积
      double area = contourArea(*itc);
      // 若面积小于设置的阈值
      if (area < min_area)
      {
        // 遍历轮廓所在位置所有像素点
        for (int i = rect.y; i < rect.y + rect.height; i++)
        {
          uchar *output_data = dst.ptr<uchar>(i);
          for (int j = rect.x; j < rect.x + rect.width; j++)
          {
            // 将连通区的值置0
            if (output_data[j] == 255)
            {
              output_data[j] = 0;
            }
          }
        }
      }
      itc++;
    }
  }
}
// 数字识别
cv::Mat NumberRecognition(cv::Mat Thresh, vector<cv::Mat> sub_size, vector<cv::Mat> sub_t_size, vector<vector<Point>> contour,int thresh)
{
  cv::Mat result = Thresh.clone();
  for (int i = 0; i < sub_size.size(); ++i)
  {
    int min = 999999;
    int idx = 0;
    for (int j = 0; j < sub_t_size.size(); ++j)
    {
      int d = calcDiff(sub_size[i], sub_t_size[j]);
      if (d < min)
      {
        min = d;
        idx = j;
      }
    }
    if (min < thresh)
    {
      cout << "第" << i + 1 << "个轮廓数字识别为:" << a[idx] << endl;
      cv::Rect rect = cv::boundingRect(contour[i]);
      rectangle(result, rect, Scalar(255,0,0), 1, 8);
      putText(result, to_string(a[idx]), Point(rect.x, rect.y - 20), 1, 2, Scalar(0,0,255));
    }
    else {
      cout << "第" << i + 1 << "个轮廓数字识别为: 空" << endl;
    }
  }
  return result;
}

测试效果

图3 原图

图4 阈值图

图5 识别结果

      不难看出,字典的丰富性影响了识别的准确程度,当字体同字典数字字体类似,即使数字压缩变形也能识别出来,反之则不行。画了个圆识别为了0,emm哈哈哈。手写体用字典法很难做到,毕竟千人千样,字典量过大,程序计算时间就飞上去了。所以,该方法适合标准数字类型,即使是车牌或者信用卡卡号这种,只要提前做好字典都是可以实现的。


      如果函数有什么可以改进完善的地方,非常欢迎大家指出,一同进步何乐而不为呢~


      如果文章帮助到你了,可以点个赞让我知道,我会很快乐~加油!

相关文章
|
6天前
|
算法 计算机视觉
【Qt&OpenCV 图像的感兴趣区域ROI】
【Qt&OpenCV 图像的感兴趣区域ROI】
9 1
|
6天前
|
运维 算法 计算机视觉
【Qt&OpenCV 图像的模板匹配 matchTemplate/minMaxLoc】
【Qt&OpenCV 图像的模板匹配 matchTemplate/minMaxLoc】
7 1
|
18天前
|
计算机视觉
OpenCV中图像算术操作与逻辑操作
OpenCV中图像算术操作与逻辑操作
14 1
|
19天前
|
计算机视觉
OpenCV图像二值化
OpenCV图像二值化
|
19天前
|
存储 Cloud Native Linux
OpenCV图像翻转和旋转
OpenCV图像翻转和旋转
|
19天前
|
存储 Cloud Native Linux
OpenCV鼠标操作(画红色方框截取图像)
OpenCV鼠标操作(画红色方框截取图像)
|
19天前
|
计算机视觉
OpencV图像几何形状绘制
OpencV图像几何形状绘制
|
19天前
|
计算机视觉
OpenCV图像像素值统计
OpenCV图像像素值统计
|
19天前
|
计算机视觉
OpenCV图像色彩空间转换
OpenCV图像色彩空间转换
|
19天前
|
计算机视觉
OpenCV图像像素逻辑操作
OpenCV图像像素逻辑操作