菜鸟小筑

一只平凡的菜鸟,一片浩瀚的天空,我自翱翔…

【OpenCV3.3】检测图像中的身份证区域

        假设现有一些含身份证前景以及不确定背景的图像,想通过计算机将身份证区域检测出来,实现诸如用户拍照定位提示、背景分离等业务,用OpenCV该如何做呢?如果输入图像前景和背景同时具有一定区分度,并且没有光照污染(即均匀光照),那么本文介绍的方法似乎是一种较快的检测方案(因为对输入图像要求比较严格, 故计算量少)。

        首先我们来看看一些例子(来自互联网):


        可以看到,这些图像都有一些共同点,就是干净(几乎没有噪声)、清晰(前后背景区分度大)。我们按照如下几个步骤来一步步实现检测身份证区域:

  1. 灰度化。同其它图像检测预处理步骤一样,这一步目的在于将图像转为单通道灰度图,通过丢掉一些影响不大的颜色信息来加快计算。OpenCV提供了几种转灰度图的方法,一个是在读取时(cv::imread、cv::imdecode)指定cv::IMREAD_GRAYSCALE,另一个是颜色空间转换(cv::cvtColor)指定cv::COLOR_BGR2GRAY,还有就是cv::split等。考虑到身份证的颜色特征,我们使用分量法,即提取原图的B分量作为灰度图(在本例中B相比G、R分量得到的结果前后背景区分度会大一些)。
    cv::Mat mvs[3];
    cv::split(image_color, mvs);
    
    cv::Mat &r = mvs[0]; // roi
  2. 二值化。OpenCV支持全局阀值(cv::threshold)和自适应阀值(cv::adaptiveThreshold),前面说了本文的方法会比较快,部分原因就是这里使用全局阀值而不是自适应阀值。关于全局阀值,OpenCV提供了两种确定算法,最大类间方差法cv::THRESH_OTSU和三角形算法cv::THRESH_TRIANGLE,我们这里使用了简化版的OTSU:在计算图像直方图时不对整幅图像进行计算,而是仅仅提取图像上下左右和中间五个小区域进行计算。
    // 计算指定区域直方图峰值所在的范围
    static int calcDistrib_Rel(const cv::Mat &image_gray, const cv::Rect &roi)
    {
    	int maxv = 0;
    	int histogram[UCHAR_MAX + 1] = { 0 };
    
    	// TODO: loop unrolling & optimization
    	const size_t image_step       = image_gray.step;
    	const uchar *__restrict pdata = image_gray.data + roi.y * image_step;
    	const int_fast32_t y_upper    = roi.y + roi.height;
    	const int_fast32_t x_upper    = roi.x + roi.width;
    	for (int_fast32_t i = roi.y; i < y_upper; ++i) {
    		auto pline_data = pdata + roi.x;
    		for (int_fast32_t j = roi.x; j < x_upper; ++j) {
    			int_fast32_t m  = *pline_data++;
    			int_fast32_t v  = ++histogram[m];
    			if (v > histogram[maxv]) maxv = m;
    		}
    		pdata += image_step;
    	}
    
    	int mid = histogram[maxv] / 2;
    	int low = maxv, high = maxv;
    	while (low > 0 && histogram[--low] >= mid) {
    	}
    	while (high < UCHAR_MAX && histogram[++high] >= mid) {
    	}
    
    	return MAKELONG(low, high);
    }
    
    // 假设身份证位于图像中部
    // 通过直方图计算上下左右和中间块区域的颜色区分度
    static constexpr int block_scale = 10;
    int block_width  = r.cols / block_scale;
    int block_height = r.rows / block_scale;
    int e = calcDistrib_Rel(r, cv::Rect((r.cols - block_width) / 2 - 1, (r.rows - block_height) / 2 - 1, block_width, block_height));
    int a = calcDistrib_Rel(r, cv::Rect(0, 0, block_width, block_height));
    int m = HIWORD(a) <= LOWORD(e), n = LOWORD(a) >= HIWORD(e); // 背景颜色分布在前景前、后还是有重叠,从直方图很容易看出来
    int b = m || n ? calcDistrib_Rel(r, cv::Rect(r.cols - block_width - 1, 0, block_width, block_height)) : a;
    int o = (m && HIWORD(b) <= LOWORD(e)) || (n && LOWORD(b) >= HIWORD(e));
    int c = !o ? b : calcDistrib_Rel(r, cv::Rect(0, r.rows - block_height - 1, block_width, block_height));
    int q = (m && HIWORD(c) <= LOWORD(e)) || (n && LOWORD(c) >= HIWORD(e));
    int d = !q ? c : calcDistrib_Rel(r, cv::Rect(r.cols - block_width - 1, r.rows - block_height - 1, block_width, block_height));
    int s = (m && HIWORD(d) <= LOWORD(e)) || (n && LOWORD(d) >= HIWORD(e));
    // 上述m、n、o、q、s都是区域颜色是否重叠的标志,之所以这样做是为了快速判断输入图像能否使用当前方案,避免多余计算
    
    if (s) {
    	int threshold = m ?
    		Utility::select_max(HIWORD(a), HIWORD(b), HIWORD(c), HIWORD(d)) :
    		Utility::select_min(LOWORD(a), LOWORD(b), LOWORD(c), LOWORD(d));
    	threshold = m ?
    		threshold + (LOWORD(e) - threshold) * 0.45 :
    		HIWORD(e) + (threshold - HIWORD(e)) * 0.60; // 让阀值尽可能靠近背景
    	cv::Mat r_threshold;
    	cv::threshold(r, r_threshold, threshold, UCHAR_MAX, m ? cv::THRESH_BINARY : cv::THRESH_BINARY_INV);
    } else {
    	// 这里表明前、背景有颜色重叠,可能是背景颜色和身份证近似,或者光照污染,此时全局阀值二值化的结果肯定不好,会丢失轮廓,可以考虑自适应阀值+形态学滤波cv::MORPH_GRADIENT等来得到轮廓明显的图像,这里就不贴了。
    }
    
  3. 轮廓提取。通过上述二值化处理之后身份证轮廓已经比较清晰了,所以省去调用各种算子(如Canny、Sobel等)进行边缘检测的步骤,直接进行轮廓信息提取。
    std::vector< std::vector<cv::Point> > contours_list;
    std::vector<cv::Vec4i> hierarchy;
    // Since opencv 3.2 source image is not modified by this function
    cv::findContours(r_threshold, contours_list, hierarchy,
    		 cv::RetrievalModes::RETR_EXTERNAL, cv::ContourApproximationModes::CHAIN_APPROX_NONE);
    
  4. 把结果画回原图看看。
    for (auto &contour : contours_list) {
    	cv::Rect &&rect = cv::boundingRect(contour);
    	if (rect.width > (r.cols / 2) && rect.height > (r.rows / 2)) {
    		//cv::rectangle(image_color, rect, cv::Scalar(0, 255, 0), 5);
    		std::vector<cv::Point2f> poly;
    		cv::approxPolyDP(contour, poly, 2, true);
    		for (uint32_t i = 0; i < poly.size() - 1; ++i) {
    			cv::line(image_color, poly[i], poly[i + 1], cv::Scalar(0, 255, 0), 6);
    		} //for
    		cv::line(image_color, poly[poly.size() - 1], poly[0], cv::Scalar(0, 255, 0), 6);
    		break;
    	} //if
    }

        效果截图(里面的r是ROI不是r分量;最后一张不是很完美):



阅读更多
文章标签: opencv c++
个人分类: C/C++ 图像处理
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

不良信息举报

【OpenCV3.3】检测图像中的身份证区域

最多只允许输入30个字

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭