目录

 

倾斜矫正具体实现

calcSafeRect()计算安全矩阵

rotation()矩形角度旋转矫正

isdeflection()“车牌”(白色)区域偏斜判断

affine()仿射变换,扭正“车牌”


倾斜矫正具体实现

具体实现代码如下,其中调用了包含了4个功能函数:calcSafeRect(),rotation(),isdeflection(),affine()。

int CPlateLocate::deskew(const Mat &src, const Mat &src_b,
                         vector<RotatedRect> &inRects,
                         vector<CPlate> &outPlates, bool useDeteleArea, Color color) {
  Mat mat_debug;
  src.copyTo(mat_debug);

  for (size_t i = 0; i < inRects.size(); i++) {
    RotatedRect roi_rect = inRects[i];  //矩形块

    float r = (float) roi_rect.size.width / (float) roi_rect.size.height; //宽高比
    float roi_angle = roi_rect.angle; //矩形角度

    Size roi_rect_size = roi_rect.size; 
    if (r < 1) {
      roi_angle = 90 + roi_angle;
      swap(roi_rect_size.width, roi_rect_size.height);//交换函数
    }

    if (m_debug) {  
      Point2f rect_points[4]; 
      roi_rect.points(rect_points);//返回矩形4个顶点
      for (int j = 0; j < 4; j++)
        line(mat_debug, rect_points[j], rect_points[(j + 1) % 4], //画直线
             Scalar(0, 255, 255), 1, 8); 
    }

    // changed
    // rotation = 90 - abs(roi_angle);
    // rotation < m_angel;

    // m_angle=60
    if (roi_angle - m_angle < 0 && roi_angle + m_angle > 0) {  //偏离60°之内
      Rect_<float> safeBoundRect;
      bool isFormRect = calcSafeRect(roi_rect, src, safeBoundRect);//计算安全矩阵
      if (!isFormRect) continue;

      Mat bound_mat = src(safeBoundRect); //边界,获取感兴趣区域(原彩色图)
      Mat bound_mat_b = src_b(safeBoundRect);边界,获取感兴趣区域(二值图)

      if (0) {
        imshow("bound_mat_b", bound_mat_b);
        waitKey(0);
        destroyWindow("bound_mat_b");
      }

      Point2f roi_ref_center = roi_rect.center - safeBoundRect.tl();//感兴趣区域的中心点

      Mat deskew_mat;
      if ((roi_angle - 5 < 0 && roi_angle + 5 > 0) || 90.0 == roi_angle ||
          -90.0 == roi_angle) {   //5°或者无偏离情况,不矫正
        deskew_mat = bound_mat;
      } else {
        Mat rotated_mat;
        Mat rotated_mat_b;

        if (!rotation(bound_mat, rotated_mat, roi_rect_size, roi_ref_center, roi_angle))
          continue;  //旋转

        if (!rotation(bound_mat_b, rotated_mat_b, roi_rect_size, roi_ref_center, roi_angle))
          continue;

        // we need affine for rotatioed image
        double roi_slope = 0;
        // imshow("1roated_mat",rotated_mat);
        // imshow("rotated_mat_b",rotated_mat_b);
        if (isdeflection(rotated_mat_b, roi_angle, roi_slope)) {  //偏斜判断
          affine(rotated_mat, deskew_mat, roi_slope);//偏斜纠正
        } else
          deskew_mat = rotated_mat;
      }

      Mat plate_mat;
      plate_mat.create(HEIGHT, WIDTH, TYPE);

      // haitungaga add锛宎ffect 25% to full recognition.
      if (useDeteleArea)
        deleteNotArea(deskew_mat, color);//color为对应的输入的颜色

      if (deskew_mat.cols * 1.0 / deskew_mat.rows > 2.3 && deskew_mat.cols * 1.0 / deskew_mat.rows < 6) {
        if (deskew_mat.cols >= WIDTH || deskew_mat.rows >= HEIGHT)
          resize(deskew_mat, plate_mat, plate_mat.size(), 0, 0, INTER_AREA);
        else
          resize(deskew_mat, plate_mat, plate_mat.size(), 0, 0, INTER_CUBIC);

        CPlate plate;
        plate.setPlatePos(roi_rect);
        plate.setPlateMat(plate_mat);
        if (color != UNKNOWN) plate.setPlateColor(color);
        outPlates.push_back(plate);
      }
    }
  }
  return 0;
}

实现流程:

step1:判断颜色匹配的旋转矩形区域倾斜角度是否在60°之内,是则进入下一步,否则不予考虑。(因为假设了拍摄的图像倾斜不是特别大)

step2:调用calcSafeRect()计算安全矩阵,看矩阵是否超过图像边界,没超过则进行下一步,否则,该矩形块不予考虑(拍摄的车牌不完全)

step3:再判断颜色匹配的矩形区域倾斜角度是否在5°之内,是则直接输出,不矫正,否则进行下一步;

step4:调用rotation(),进行矩形角度旋转矫正;

step5:调用isdeflection(),对矩形中“车牌”(白色)区域进行偏斜判断,是正视角,则直接输出,否则进行下一步仿射变换;

step6:调用affine(),对上一步的矩形进行仿射变换,扭正“车牌”。


下面详细说明上述几个功能函数:

calcSafeRect()计算安全矩阵

代码如下:

bool calcSafeRect(const RotatedRect &roi_rect, const Mat &src,
                    Rect_<float> &safeBoundRect) {
    Rect_<float> boudRect = roi_rect.boundingRect(); //返回最小外接矩形

    float tl_x = boudRect.x > 0 ? boudRect.x : 0;//左上角
    float tl_y = boudRect.y > 0 ? boudRect.y : 0;//

    float br_x = boudRect.x + boudRect.width < src.cols //右下角
                 ? boudRect.x + boudRect.width - 1
                 : src.cols - 1;
    float br_y = boudRect.y + boudRect.height < src.rows
                 ? boudRect.y + boudRect.height - 1
                 : src.rows - 1;

    float roi_width = br_x - tl_x;   //感兴趣区域宽度
    float roi_height = br_y - tl_y;  //感兴趣区域高度

    if (roi_width <= 0 || roi_height <= 0) return false;

    //  a new rect not out the range of mat

    safeBoundRect = Rect_<float>(tl_x, tl_y, roi_width, roi_height);

    return true;
  }

这部分比较简单,看代码注释即可。


rotation()矩形角度旋转矫正

bool CPlateLocate::rotation(Mat &in, Mat &out, const Size rect_size,
                            const Point2f center, const double angle) {
  if (0) {
    imshow("in", in);
    waitKey(0);
    destroyWindow("in");
  }

  Mat in_large;
  in_large.create(int(in.rows * 1.5), int(in.cols * 1.5), in.type());//创建将原图放大1.5倍的图像

  float x = in_large.cols / 2 - center.x > 0 ? in_large.cols / 2 - center.x : 0;//左上
  float y = in_large.rows / 2 - center.y > 0 ? in_large.rows / 2 - center.y : 0;

  float width = x + in.cols < in_large.cols ? in.cols : in_large.cols - x;//宽度
  float height = y + in.rows < in_large.rows ? in.rows : in_large.rows - y;

  /*assert(width == in.cols);
  assert(height == in.rows);*/

  if (width != in.cols || height != in.rows) return false;

  Mat imageRoi = in_large(Rect_<float>(x, y, width, height));//放大后的感兴趣区域
  addWeighted(imageRoi, 0, in, 1, 0, imageRoi);//图像融合

  Point2f center_diff(in.cols / 2.f, in.rows / 2.f);//
  Point2f new_center(in_large.cols / 2.f, in_large.rows / 2.f);

  Mat rot_mat = getRotationMatrix2D(new_center, angle, 1);//获取旋转矩阵

  /*imshow("in_copy", in_large);
  waitKey(0);*/

  Mat mat_rotated;
  warpAffine(in_large, mat_rotated, rot_mat, Size(in_large.cols, in_large.rows),
             CV_INTER_CUBIC);//仿射变换

  /*imshow("mat_rotated", mat_rotated);
  waitKey(0);*/

  Mat img_crop;
  getRectSubPix(mat_rotated, Size(rect_size.width, rect_size.height),
                new_center, img_crop);//获取旋转后的感兴趣的图像

  out = img_crop;

  if (0) {
    imshow("out", out);
    waitKey(0);
    destroyWindow("out");
  }

  /*imshow("img_crop", img_crop);
  waitKey(0);*/

  return true;
}

在旋转的过程当中,遇到一个问题,就是旋转后的图像被截断了,如下图所示:

python 车牌倾斜矫正_python 车牌倾斜矫正

仔细分析下代码可以发现,getRotationMatrix2D()  函数主要根据旋转中心和角度进行旋转,当旋转角度还小时,一切都还好,但当角度变大时,明显我们看到的外接矩形的大小也在扩增。在这里,外接矩形被称为视框,也就是我需要旋转的正方形所需要的最小区域。随着旋转角度的变大,视框明显增大。 如下图所示:

python 车牌倾斜矫正_二值化_02

EasyPR使用了一个极为简单的策略,它将原始图像与目标图像都进行了扩大化。首先新建一个尺寸为原始图像 1.5 倍的新图像,接着把原始图像映射到新图像上,于是我们得到了一个显示区域(视框)扩大化后的原始图像。显示区域扩大以后,那些在原图像中没有值的像素被置了一个初值。接着调用 warpAffine 函数,使用新图像的大小作为目标图像的大小。warpAffine 函数会将新图像旋转,并用目标图像尺寸的视框去显示它。于是我们得到了一个所有感兴趣区域都被完整显示的旋转后图像,这样,我们再使用 getRectSubPix()函数就可以获得想要的车牌区域了。


isdeflection()“车牌”(白色)区域偏斜判断

bool CPlateLocate::isdeflection(const Mat &in, const double angle, //判断平行四边形倾斜值
                                double &slope) { /*imshow("in",in);
                                                waitKey(0);*/
  if (0) {
    imshow("in", in);
    waitKey(0);
    destroyWindow("in");
  }
  
  int nRows = in.rows;
  int nCols = in.cols;

  assert(in.channels() == 1);//断言检查

  int comp_index[3];
  int len[3];

  comp_index[0] = nRows / 4;//二值图像高度的 1/4 处
  comp_index[1] = nRows / 4 * 2;//二值图像高度的 2/4 处 
  comp_index[2] = nRows / 4 * 3;//二值图像高度的 3/4 处

  const uchar* p;

  for (int i = 0; i < 3; i++) {
    int index = comp_index[i];
    p = in.ptr<uchar>(index);

    int j = 0;
    int value = 0;
    while (0 == value && j < nCols)
		value = int(p[j++]);

    len[i] = j;//各个高度处的列数
  }

  // cout << "len[0]:" << len[0] << endl;
  // cout << "len[1]:" << len[1] << endl;
  // cout << "len[2]:" << len[2] << endl;

  // len[0]/len[1]/len[2] are used to calc the slope***计算斜率

  double maxlen = max(len[2], len[0]); //列数最大值
  double minlen = min(len[2], len[0]);//列数最小值
  double difflen = abs(len[2] - len[0]);

  double PI = 3.14159265;

  double g = tan(angle * PI / 180.0);

  if (maxlen - len[1] > nCols / 32 || len[1] - minlen > nCols / 32) { //倾斜较大??

    double slope_can_1 =double(len[2] - len[0]) / double(comp_index[1]); //三个斜率               
    double slope_can_2 = double(len[1] - len[0]) / double(comp_index[0]);
    double slope_can_3 = double(len[2] - len[1]) / double(comp_index[0]);
    // cout<<"angle:"<<angle<<endl;
    // cout<<"g:"<<g<<endl;
    // cout << "slope_can_1:" << slope_can_1 << endl;
    // cout << "slope_can_2:" << slope_can_2 << endl;
    // cout << "slope_can_3:" << slope_can_3 << endl;
    // if(g>=0)
    slope = abs(slope_can_1 - g) <= abs(slope_can_2 - g) ? slope_can_1
                                                         : slope_can_2; //???
    // cout << "slope:" << slope << endl;
    return true;
  } else {
    slope = 0;
  }
  //倾斜较小
  return false;
}

上述代码就是分析截取后的车牌区域,判断车牌偏斜的程度,并且计算偏斜的值。车牌区域里的车牌分为正角度和偏斜角度两种。对于正的角度而言,可以看出车牌区域就是车牌,因此直接输出即可。而对于偏斜角度而言,车牌是平行四边形,与矩形的车牌区域不重合。如何判断一个图像中的图形是否是平行四边形?

一种简单的思路就是对图像二值化,然后根据二值化图像进行判断。为了判断二值化图像中白色的部分是平行四边形。一种简单的做法就是从图像中选择一些特定的行。计算在这个行中,第一个全为0的串的长度。从几何意义上来看, 这就是平行四边形斜边上某个点距离外接矩形的长度。假设我们选择的这些行位于二值化图像高度的 1/4,2/4,3/4 处的话,如果是白色图形是矩形的话, 这些串的大小应该是相等或者相差很小的,相反如果是平行四边形的话,那么这些串的大小应该不等,并 且呈现一个递增或递减的关系。通过这种不同,我们就可以判断车牌区域里的图形,究竟是矩形还是平行 四边形。

偏斜判断的另一个重要作用就是,计算平行四边形倾斜的斜率,这个斜率值用来在下面的仿射变换中 发挥作用。我们使用一个简单的公式去计算这个斜率,那就是利用上面判断过程中使用的串大小,假设二值化图像高度的 1/4,2/4,3/4 处对应的串的大小分别为 len1,len2,len3,车牌区域的高度为 Height。 一个计算斜率 slope 的计算公式就是:(len3-len1)/Height*2。     

python 车牌倾斜矫正_仿射变换_03


affine()仿射变换,扭正“车牌”

代码如下:

void CPlateLocate::affine(const Mat &in, Mat &out, const double slope) {   //仿射变换
  // imshow("in", in);
  // waitKey(0);

  Point2f dstTri[3];
  Point2f plTri[3];

  float height = (float) in.rows;
  float width = (float) in.cols;
  float xiff = (float) abs(slope) * height; //

  if (slope > 0) {  //右倾斜

    // right, new position is xiff/2

    plTri[0] = Point2f(0, 0);
    plTri[1] = Point2f(width - xiff - 1, 0);
    plTri[2] = Point2f(0 + xiff, height - 1);//输入三角形

    dstTri[0] = Point2f(xiff / 2, 0);
    dstTri[1] = Point2f(width - 1 - xiff / 2, 0);
    dstTri[2] = Point2f(xiff / 2, height - 1);//输出三角形
  } else {      //左倾斜

    // left, new position is -xiff/2

    plTri[0] = Point2f(0 + xiff, 0);
    plTri[1] = Point2f(width - 1, 0);
    plTri[2] = Point2f(0, height - 1);

    dstTri[0] = Point2f(xiff / 2, 0);
    dstTri[1] = Point2f(width - 1 - xiff + xiff / 2, 0);
    dstTri[2] = Point2f(xiff / 2, height - 1);
  }

  Mat warp_mat = getAffineTransform(plTri, dstTri); //生成仿射变换矩阵

  Mat affine_mat;
  affine_mat.create((int) height, (int) width, TYPE);

  if (in.rows > HEIGHT || in.cols > WIDTH)

    warpAffine(in, affine_mat, warp_mat, affine_mat.size(),
               CV_INTER_AREA); //仿射变换-区域插值
  else
    warpAffine(in, affine_mat, warp_mat, affine_mat.size(), CV_INTER_CUBIC);//三次样条插值

  out = affine_mat;
}

 函数 affine() 的主要功能是对图像进行根据偏斜角度,进行仿射变换,如下:

                                                 

python 车牌倾斜矫正_仿射变换_04

注:以下情况为左偏斜

                                            

python 车牌倾斜矫正_仿射变换_03

以上用到了仿射变换的原理:

仿射变换(Affine Transformation 或 Affine Map),又称仿射映射,是指在几何中,一个向量空间进行一次线性变换并接上一个平移,变换为另一个向量空间的过程。它保持了二维图形的“平直性”(即:直线经过变换之后依然是直线)和“平行性”(即:二维图形之间的相对位置关系保持不变,平行线依然是平行线,且直线上点的位置顺序不变)。

一个任意的仿射变换都能表示为乘以一个矩阵(线性变换)接着再加上一个向量(平移)的形式。

那么, 我们能够用仿射变换来表示如下三种常见的变换形式:

  • 旋转,rotation (线性变换)
  • 平移,translation(向量加)
  • 缩放,scale(线性变换)

如果进行更深层次的理解,仿射变换代表的是两幅图之间的一种映射关系。这类变换可以用一个3*3的矩阵M来表示,其最后一行为(0,0,1)。该变换矩阵将原坐标为(x,y)变换为新坐标(x',y')。

                                                      

python 车牌倾斜矫正_仿射变换_06

对应的opencv函数如下:

1、getRotationMatrix2D

  Mat getRotationMatrix2D(Point2f center,  angle,  scale) 
       已知旋转中心坐标(坐标原点为图像左上端点)、旋转角度(单位为度°,顺时针为负,逆时针为正)、放缩比例,返回旋转/放缩矩阵。

2、warpAffine

void warpAffine(InputArray src, 
        OutputArray dst,
           InputArray M, 
        Size dsize,
           int flags=INTER_LINEAR,
           int borderMode=BORDER_CONSTANT, 
        const Scalar& borderValue=Scalar())

   根据getRotationMatrix2D得到的变换矩阵,计算变换后的图像。warpAffine 方法要求输入的参数是原始图像的左上点,右上点,左下点,以及输出图像的左上点,右上点,左下点。注意,必须保证这些点的对应顺序,否则仿射的效果跟你预想的不一样。因此 opencv 需要的是三个点对(共六个点)的坐标,然后建立一个映射关系,通过这个映射关系将原始图像的所有点映射到目标图像上。 如下图所示:

                                                    

python 车牌倾斜矫正_斜率_07

 

最后使用 resize 函数将车牌区域统一化为 EasyPR 的车牌大小,大小为136*36。