关闭

文字检测与识别1-MSER

标签: CVScene Text
10666人阅读 评论(17) 收藏 举报
分类:

导语

文字识别在现实场景中的用途非常广泛,现在已经有很多公司将这项技术用于实际中。比如车牌识别,图片转换成文档,拍照搜题,拍照翻译等。这让很多人有了错觉,感觉文字识别的技术已经炉火纯青,可以广泛应用。其实不然,车牌识别里面字体和字的类型比较单一,并且有一些矩形等辅助的特征。而拍照翻译的图片一般是文档类型,较容易识别,但也有不小的错误率。文字识别的首要问题是找到文字,其次才是识别。而在自然场景下找文字的难度比一般情况下难度要高很多,因为自然场景背景十分复杂,字的类型大小多种多样,视角,污渍,反光等也是需要考虑的问题。谷歌曾经想推出谷歌翻译眼镜,但据个人所知,这几年已经没有多少宣传,应该是技术上遇到了一些困难。


这系列文章主要介绍一些自然场景下文字检测(Scene Text Textin the wild,ICDAR会议2上有很多SceneText论文,大家有兴趣可以去主页上看卡,它里面也有数据集可以供你训练和测试。主要介绍三篇文章[1,2,3]里面的技术“Detectingtext in natural scenes with stroke width transform”,“Robust text detection in natural scene images”,和“Efficient Scene Text Localization and Recognition with LocalCharacter Refinement”,简单起见,分别称之为SWT,RST,EST,还有其他一些本人看过的可以查阅下面的参考文献。

自然场景下文字检测一般分为以下这么几步,产生候选(candidate,字符过滤,字符合并成文本行,文本行过滤和后处理。需要注意的是有些论文采用字符和文本行双重过滤,有些论文则只采用其中一种过滤。评价一个算法的好坏一般有两个指标,一个是精准度precision,一个是召回率recall,这名字有点拗口,不过非常简单。比如说一幅图片中有100个字,但是算法找出了120个框,其中80是个是字,20个不是字,那么精准度就是80/120,召回率是80/100,可以用f这个单一的指标去描述PRf = 2/(1/p+1/r),从中可以看出,PR只要有一个很低,f值都会很低。

 

产生候选(Extract candidates

现在很多文章都采用连通域类方案,如SWT采用的连通域,文献[12]采用的ERextremal region),RSTEST采用的MSER(maximally stable extremal regions),还有的采用了MSER的衍生版,如[7]采用的是CER,还有的比较“另类”,[5]用的是对称特征检测器,[8]用的是edge box,用于物体检测的,[16]为了召回率利用一些特征合并产生非常非常的候选,并通过word级别的识别来过滤,因此速度特别慢(Titan下一到两分钟)。但是大部分用的还是MSER类的,虽然在ICDAR的数据库中还有一些字母MSER检测不出来,但是从性能和效果上说,MSER还是具有一些优势(请注意以下所讲的都是灰度图的MSER,彩色图的MSER用的是不同的算法)。因此下面主要以opencv的实现讲MSER,为了照顾到后面的过滤步骤,会提取出MSER的树形结构。首先给两张图片让大家有个直观的感受。左图是在灰色通道上的MSER结果,右图是经过文献[2]MSER Tree过滤的结果


opencv的代码参考的是文献[15],最开始一般去看代码和文献都会有点晕,个人感觉最好的理解的方法就是找个例子,然后按照论文和代码的流程一步步去推演。opencv的代码做了一些优化,对c++和算法不是很熟悉的话可能要看很久,另外一个版本会更好理解,但是请注意它是GNU license

 

ER

opencv里并没有提取出树的信息,所以先依照opencv的代码介绍ERER代表着是图片中一个连通(比如4连通或8连通)区域的集合,此集合内所有的像素值都小于等于某一值,而这个区域内的边界都大于这个值。我们可以把像素的值想象成地势,而把一个ER想象成一个填满水的坑洼的水坑(在这里我们采用4连通)。在这个水坑里,有一个水位淹没了所里面所有的像素但,也就是说这个区域里所有的地势(像素值)都要低于这个水位,并且水也流不出去,因为水盆有个边缘(边缘像素值要高于这个水位)。虽然水流的方式跟现实中有些区别,但是大体意思是一致的,后面会提到。

考虑如下一个简单的3*3的一个图片

3

2

2

2

3

1

1

2

3

它的提取方式如下图,为了方便讲解,在每个操作上都打了ID(上方的红色数字),参考流程图和代码,详细过程和流程如下

注意一般在最开始会放一个水位最高的256dummy component作为根节点,因为图像的最高值在255.另外开始点从(1,0)开始(坐标行在前,列在后),可以稍微节省点时间。边界存储的是与当前ER连接的边界坐标,也就是水盆边界的位置。GrowHistory存的是一个ER从低水位到高水位的过程,所有的ER(除了全图)都会存于这个history中,另外需要说明两件事情,一个是opencvhistory中代表parent的是shortcut,这在计算MSER的时候就不应是父节点,但在我们这里是一样的,另外一个是historyparent child变量跟MSER中是不一致的,不然opencv的代码就已经提取出树的信息。

1)执行'1' ' 2' ' 3'红色位置代表当前像素,如果某个位置被黄色填充,代表这个像素已经被访问。这部分主要是些初始化的工作。主要的意思是我们在(1,0)的像素点上放充分量的水,水位的值也就等于当前的像素值2.

 

2)现在有水停留在红色位置(1,0),并且水位为2。人往高处走,水往低处流。在这里唯一的不同是水每次只流向一个方向,而不能同时扩散。我们跟opencv的代码保持一致采用右下左右的顺序。首先执行'7'->'8'->'10',水尝试往右流到(11),发现那里的地势为3,比我们当前的水位要高,自然流不过去,因此应该是个边缘,所以把(1,1)加入到地势为3的边缘中。同理执行'7'->'8'->'4'->'3'现在水尝试往下流,发现坐标为(2,0)地势为1的像素。很显然我们的水位可以流向那,这时我们的水位降低为1,先增一个ER区,而地势为2的(1,0)成了边界。

 


 

3)现在我们在(2,0),水位为1,。执行'7'->'8'->'10',水尝试流向地势为2的(2,1),流不通,将坐标(2,1)压入边界中。


4)执行'7'->'6'->'5',这时发现(2,0)处的周围全都尝试流通过了,我们确认当前的像素是属于当前的ER,因此将此像素压入ER栈顶的点集中。并且我们找到地势最低的边界点,作为当前点。

 


5)执行'9'->'12'->'13'.刚刚的水位是1,没道理说现在就流到2了。让我们回顾一下刚才的情况,刚才的水位是1,然后发现边界的最低的地势为2,说明我们已经找到了一个ER,在这个区域已经没有邻域的地势小于等于1,并且边界都大于1.因此我们现在能做的就是提高水位。而且根据ER的定义,高地势的区域会包含连通的低地势区域,因此我们要将其合并。为了方便,grow historyID10开始


 

(6)现在又来到了熟悉的流程,执行'14'->'7'->'8'->'10',将(2,2)压入边界。执行'7'->'6'->'5',发现当前位置已经都访问过了,将该点压入栈顶的er,因此弹出边界(1,0),发现边界的地势跟当前的水位是一样的,因此直接将其作为该当前点。


7)执行'7'->'8'->'10',继续探索,还有未访问邻域(0,0)压入边界。


 

8)所有的邻域都已访问,执行'7'->'6'->'5',将当前的点压入ER栈顶,并弹出边界(0,0


9)又到了要提高水位的时候,发现栈的第二个水位是256,如果提高到256,水位太大了。因此我们将当期的er保存的history,并把它的水位提高到当前位置的地势值3。而且到了这一步我们可以检查地势为1ER是否为MSER了,依旧是Grow History ID 10保存的内容。

 


10)执行'7'->'8'->'4"->'3',访问到地势为2的(0,1),因此水位再次下降


(11)继续往外探索,执行'7'->'8'->'10',将(0,2)压入边界

 


12)执行'7'->'6'->'5'->'9',没有未访问的邻域点,将(0,1)压入ER栈,并弹出边界,发现发现当前的像素还在一个水位上,因此不需要合并或者升水位


 

 

13)继续探索,发现低地势的(1,2),水位下降,将当前点压入边界


 

(14)现在所有的点都已访问了,将坐标(1,2)压入ER栈,并弹出边界

 


15)上一步中的边界水位比我们的要高,并观察ER栈的gray level,因此现合并栈顶的两个ER

 


 

16)与上面的情况类似,压入当前点到ER栈,弹出边界,并合并栈顶量ER

 


17)按照之前的过程,连续压入对角线上的3,已经没有边界了,推出。自此我们找出了所有的ER.

 


 

MSER Tree

按照上面的流程,我们提取了所有的ER,他们的ID分别为110111213.要构建树,需要定义父子关系,我们把合并过程中高地势的为父,低水位的为子,因此构建树如下

 


那怎么判断一个ER是不是MSER呢?对于单通道图像来说主要有五个参数,delta, maxVariation,minDiversity, minArea, maxArea.其中minArea,maxAreaopencv中代表的点数,如ID11的点数是3ID10的点数是10

delta是为了计算variation.不管是MSER还是特征点或者BING,Edge box而言,它的核心思想都是要找到一块区域,能跟周围的有明显的变化。在MSER里,这个是通过variation定义的。打个不恰当的比方,一个脸盆和一个水桶,脸盆底部是个ER,水桶的底部也是一个ER。但是脸盆的底部跟边缘的高度相差不大,我只要把水位增加一点,水就溢出来,脸盆的边缘和底部合成了一个新的ER。但如果是水桶,你需要加很多水才能行成新的ER。因此水桶的ER更稳定,它跟周围的对比度更强。一个正式的定义是


其中S代表的ER的点数,在Opencv中简化为


比如delta= 2,要计算ID1vatiation,可以看出S(ERlevel) =9,ID1gray level3,因此要找到3-2 =1gray levelER,我们去点数最多的,都是1,因此按照上面的公式是(9-1/1variation8.opencv默认的是0.25.另外还有个限制是当前ERvariation要小于父和子的variation,对于这一点,也简化了。

 

minDiversity是为了解决两个MSER靠的很近的问题。公式如下,MSERson代表的是子节点代数最近的已经确认是MSER的区域。如果有个子MSER,而且两个点数比较接近,我们认为两个ER相隔太近,父的ER就不能当成MSER.Openv默认的值是0.2


 

最后如果我们对上面的ER tree做假设,只有ID101113MSER,那么我们的MSER tree就分成了两个Tree.





另外很重要的一点,如字有黑底白字和白底黑字,要把原来的图像像素反转一下img=255-img,按照流程再算一遍。

至此,这部分就已讲完。由于本人水平有限,难免疏漏与错误,恳请批评与指正。

附opencv这部分的核心代码

static void extractMSER_8UC1_Pass( int* ioptr,
              int* imgptr,
              int*** heap_cur,
              LinkedPoint* ptsptr,
              MSERGrowHistory* histptr,
              MSERConnectedComp* comptr,
              int step,
              int stepmask,
              int stepgap,
              MSERParams params,
              int color,
              CvSeq* contours,
              CvMemStorage* storage )
{
    comptr->grey_level = 256;
    comptr++;
    comptr->grey_level = (*imgptr)&0xff;
    initMSERComp( comptr );
    *imgptr |= 0x80000000;
    heap_cur += (*imgptr)&0xff;
    int dir[] = { 1, step, -1, -step };
#ifdef __INTRIN_ENABLED__
    unsigned long heapbit[] = { 0, 0, 0, 0, 0, 0, 0, 0 };
    unsigned long* bit_cur = heapbit+(((*imgptr)&0x700)>>8);
#endif
    for ( ; ; )
    {
        // take tour of all the 4 directions
        while ( ((*imgptr)&0x70000) < 0x40000 )
        {
            // get the neighbor
            int* imgptr_nbr = imgptr+dir[((*imgptr)&0x70000)>>16];
            if ( *imgptr_nbr >= 0 ) // if the neighbor is not visited yet
            {
                *imgptr_nbr |= 0x80000000; // mark it as visited
                if ( ((*imgptr_nbr)&0xff) < ((*imgptr)&0xff) )
                {
                    // when the value of neighbor smaller than current
                    // push current to boundary heap and make the neighbor to be the current one
                    // create an empty comp
                    (*heap_cur)++;
                    **heap_cur = imgptr;
                    *imgptr += 0x10000;
                    heap_cur += ((*imgptr_nbr)&0xff)-((*imgptr)&0xff);
#ifdef __INTRIN_ENABLED__
                    _bitset( bit_cur, (*imgptr)&0x1f );
                    bit_cur += (((*imgptr_nbr)&0x700)-((*imgptr)&0x700))>>8;
#endif
                    imgptr = imgptr_nbr;
                    comptr++;
                    initMSERComp( comptr );
                    comptr->grey_level = (*imgptr)&0xff;
                    continue;
                } else {
                    // otherwise, push the neighbor to boundary heap
                    heap_cur[((*imgptr_nbr)&0xff)-((*imgptr)&0xff)]++;
                    *heap_cur[((*imgptr_nbr)&0xff)-((*imgptr)&0xff)] = imgptr_nbr;
#ifdef __INTRIN_ENABLED__
                    _bitset( bit_cur+((((*imgptr_nbr)&0x700)-((*imgptr)&0x700))>>8), (*imgptr_nbr)&0x1f );
#endif
                }
            }
            *imgptr += 0x10000;
        }
        int imsk = (int)(imgptr-ioptr);
        ptsptr->pt = cvPoint( imsk&stepmask, imsk>>stepgap );
        // get the current location
        accumulateMSERComp( comptr, ptsptr );
        ptsptr++;
        // get the next pixel from boundary heap
        if ( **heap_cur )
        {
            imgptr = **heap_cur;
            (*heap_cur)--;
#ifdef __INTRIN_ENABLED__
            if ( !**heap_cur )
                _bitreset( bit_cur, (*imgptr)&0x1f );
#endif
        } else {
#ifdef __INTRIN_ENABLED__
            bool found_pixel = 0;
            unsigned long pixel_val;
            for ( int i = ((*imgptr)&0x700)>>8; i < 8; i++ )
            {
                if ( _BitScanForward( &pixel_val, *bit_cur ) )
                {
                    found_pixel = 1;
                    pixel_val += i<<5;
                    heap_cur += pixel_val-((*imgptr)&0xff);
                    break;
                }
                bit_cur++;
            }
            if ( found_pixel )
#else
            heap_cur++;
            unsigned long pixel_val = 0;
            for ( unsigned long i = ((*imgptr)&0xff)+1; i < 256; i++ )
            {
                if ( **heap_cur )
                {
                    pixel_val = i;
                    break;
                }
                heap_cur++;
            }
            if ( pixel_val )
#endif
            {
                imgptr = **heap_cur;
                (*heap_cur)--;
#ifdef __INTRIN_ENABLED__
                if ( !**heap_cur )
                    _bitreset( bit_cur, pixel_val&0x1f );
#endif
                if ( pixel_val < comptr[-1].grey_level )
                {
                    // check the stablity and push a new history, increase the grey level
                    if ( MSERStableCheck( comptr, params ) )
                    {
                        CvContour* contour = MSERToContour( comptr, storage );
                        contour->color = color;
                        cvSeqPush( contours, &contour );
                    }
                    MSERNewHistory( comptr, histptr );
                    comptr[0].grey_level = pixel_val;
                    histptr++;
                } else {
                    // keep merging top two comp in stack until the grey level >= pixel_val
                    for ( ; ; )
                    {
                        comptr--;
                        MSERMergeComp( comptr+1, comptr, comptr, histptr );
                        histptr++;
                        if ( pixel_val <= comptr[0].grey_level )
                            break;
                        if ( pixel_val < comptr[-1].grey_level )
                        {
                            // check the stablity here otherwise it wouldn't be an ER
                            if ( MSERStableCheck( comptr, params ) )
                            {
                                CvContour* contour = MSERToContour( comptr, storage );
                                contour->color = color;
                                cvSeqPush( contours, &contour );
                            }
                            MSERNewHistory( comptr, histptr );
                            comptr[0].grey_level = pixel_val;
                            histptr++;
                            break;
                        }
                    }
                }
            } else
                break;
        }
    }
}


参考文献

[1]Epshtein B, Ofek E, WexlerY. Detecting text in natural scenes with stroke width transform[C]//ComputerVision and Pattern Recognition (CVPR), 2010 IEEE Conference on. IEEE, 2010:2963-2970.

[2]Yin X C, Yin X, Huang K, etal. Robust text detection in natural scene images[J]. Pattern Analysis andMachine Intelligence, IEEE Transactions on, 2014, 36(5): 970-983.

[3]Neumann L, Matas J.Efficient Scene Text Localization and Recognition with Local CharacterRefinement[J]. arXiv preprint arXiv:1504.03522, 2015.

[4]Zhu Y, Yao C, Bai X. Scenetext detection and recognition: Recent advances and future trends[J]. Frontiersof Computer Science, 2015.

[5]Zhang Z, Shen W, Yao C, etal. Symmetry-Based Text Line Detection in Natural Scenes[C]//Proceedings of theIEEE Conference on Computer Vision and Pattern Recognition. 2015: 2558-2567.

[6]Huang W, Qiao Y,Tang X. Robust scene text detection with convolution neural network inducedmser trees[M]//Computer VisionECCV 2014.Springer International Publishing, 2014: 497-511.

[7]Sun L, Huo Q, Jia W, et al.Robust Text Detection in Natural Scene Images by Generalized Color-EnhancedContrasting Extremal Region and Neural Networks[C]//Pattern Recognition (ICPR),2014 22nd International Conference on. IEEE, 2014: 2715-2720.

[8]Jaderberg M, Simonyan K,Vedaldi A, et al. Reading text in the wild with convolutional neuralnetworks[J]. International Journal of Computer Vision, 2014: 1-20.

[9]Jaderberg M,Vedaldi A, Zisserman A. Deep features for text spotting[M]//Computer VisionECCV 2014. Springer International Publishing, 2014: 512-528.

[10]Gomez L, Karatzas D. A fasthierarchical method for multi-script and arbitrary oriented scene textextraction[J]. arXiv preprint arXiv:1407.7504, 2014.

[11]Coates A, Carpenter B, CaseC, et al. Text detection and character recognition in scene images withunsupervised feature learning[C]//Document Analysis and Recognition (ICDAR),2011 International Conference on. IEEE, 2011: 440-445.

[12]Neumann L, Matas J.Real-time scene text localization and recognition[C]//Computer Vision andPattern Recognition (CVPR), 2012 IEEE Conference on. IEEE, 2012: 3538-3545.

[13]Shi B, Yao C, Zhang C, etal. Automatic Script Identification in the Wild[J]. arXiv preprintarXiv:1505.02982, 2015.

[14]Wang T, Wu D J, Coates A,et al. End-to-end text recognition with convolutional neuralnetworks[C]//Pattern Recognition (ICPR), 2012 21st International Conference on.IEEE, 2012: 3304-3308.

[15]Nistér D, Stewénius H.Linear time maximally stable extremal regions[M]//Computer Vision–ECCV 2008.Springer Berlin Heidelberg, 2008: 183-196.

[16]Gomez-Bigorda, Lluis, and Dimosthenis Karatzas. "TextProposals: a Text-specific Selective Search Algorithm for Word Spotting in the Wild." arXiv preprint arXiv:1604.02619 (2016).

[17]Lee C Y, Osindero S. Recursive Recurrent Nets with Attention Modeling for OCR in the Wild[J]. arXiv preprint arXiv:1603.03101, 2016.


6
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:81245次
    • 积分:1058
    • 等级:
    • 排名:千里之外
    • 原创:24篇
    • 转载:6篇
    • 译文:0篇
    • 评论:98条
    博客专栏
    文章分类
    最新评论