精华内容
下载资源
问答
  • 一、问题描述 同学偶然问我问题,怎么画出一个轮廓的中心轮廓。...细化算法干的事呢就是画出轮廓的中心线~ 得到图中虚线的那一条。 二、学习链接 https://answers.opencv.org/question/31908/ho...

    一、问题描述

    同学偶然问我问题,怎么画出一个轮廓的中心轮廓。然后我回去查了一下,其实发现这个其实有专门的算法叫做细化算法。在OCR文字提取并描绘出来中非常常见。现在大致分享一下我查到的一些比较关键的内容及放上几个我参考的链接。
    细化算法干的事呢就是画出轮廓的中心线~ 得到图中虚线的那一条。
    在这里插入图片描述
    在这里插入图片描述

    二、学习链接

    https://answers.opencv.org/question/31908/how-do-i-detect-the-centerline-of-an-object/ 这个是一个大神源代码实现

    https://docs.opencv.org/3.2.0/df/d2d/group__ximgproc.html 这个呢是OpenCV的拓展库实现

    https://www.youtube.com/watch?v=iOtodmhfMfU 这个是YouTube上的大神实例,下面有附赠代码链接

    三、总结

    在实际工程项目中呢,我们总会遇到很多问题是OpenCV基础算法没有的。除了在想能不能把复杂问题变为一般问题外(例如找曲线能不能变成找直线用霍夫变换等)还可以多上谷歌查一下。切记上谷歌一定要用英文关键字去搜索。其实我们遇到的很多问题GOOGLE上都有大神做到了,或者已经有了挺多成熟的算法,但是只是自己知识面比较浅薄而已~ 所以大家在工程中如果实在想不到怎么解决的时候可以多Google,解锁自己的搜索技能~ 因为想要准确的用英文表达自己的问题还是需要一定锻炼的。ヾ(◍°∇°◍)ノ゙

    四、算法详解

    这是我对该算法的具体分析:

    https://blog.csdn.net/weixin_40977054/article/details/96888371
    希望能帮助到各位~

    展开全文
  • 图像内文字定位算法

    万次阅读 2016-08-14 13:12:09
    图像文字识别是替代键盘输入的方式之一,可以使用户获得更好...为了能让文字识别达到预期效果,你必须首先能从前景与背景混杂的图像中提取到有用的文字图像数据,那么可以姑且把这一过程称之为“图像内文字切割算法”。

    前言
    图像文字识别是替代键盘输入的方式之一,可以使用户获得更好的信息输入体验。但有别于一般性的光学字符识别(OCR)过程,我们今天要讨论的算法,其图像来源相对复杂——多样的拍摄角度;多样的光照条件;多样的印刷背景;多样的拍照设备。为了能让文字识别达到预期效果,你必须首先能从前景与背景混杂的图像中提取到有用的文字图像数据,那么可以姑且把这一过程称之为“图像内文字定位算法“。
    在此之前,先简略介绍一下OPENCV。这是一款开源的跨平台计算机视觉库,应用领域为机器图像视觉以及在此基础上延伸的人工智能。Intel公司是这个项目的最大赞助商。其中的神经网络算法(基于人工神经网络理论的实现),可以训练程序如何从纷杂的数据中自动收集预定的数据特征,从而实现对大批量原始数据的筛选与匹配。例如从视频存储资料中定位某个物体或者是具有某种自我学习能力的人机交互。

    应用场景
    以手机相机或其他拍摄设备所拍摄的图像作为输入源,识别其图像内标识性字符串。(由字母和数字构成,并拥有特殊定义结构,不需要进行词汇联想)

    算法概述
    首先对文件进行裁剪、灰度与滤波处理,在此基础上使用Canny算法进行多轮边缘查找,再将其查找结果(阈化图像)进行像素邻域计算以得到分布在图像内大大小小多个像素连通区域,然后将这些以矩形标识的分散区域根据识别对象字符横向等距分布的特性进行多次区域兼并运算,之后将多轮处理后得到的多个兼并对象作整体布局上的比较分析,从而淘汰不良结果。最后根据最优兼并结果内字符单元区域集合提取图像内待识别文字。

    实现步骤(代码将采用java语言来展示)
    注:
    1、注释中出现注重符“•”表示其所针对的代码行中出现的常量及其计算表达式在实际应用中应取自或依赖于预设的配置文件,因为这些定义都是根据当前应用场景得到的试验结果,为了程序能够适应不同的需求或拥有良好扩展特性,比较好的方式便是做到运行参数可配置。
    2、“…”符号表示代码有某些细节被省略(代码来源于真实项目)

    import org.opencv.highgui.Highgui;
    import org.opencv.imgproc.*;
    import java.awt.image.BufferedImage;
    import java.awt.image.DataBufferByte;
    
    import javax.imageio.ImageIO;
    
    import org.opencv.core.CvException;
    import org.opencv.core.CvType;
    import org.opencv.core.Mat;
    ...
    //本文后续所介绍的主要方法都在此类中定义
    public class MrzOcrIdentify {
    ...
    private static boolean isLibraryLoaded = true;
    //加载opencv库文件,在这里此文件应位于JVM执行目录下。
    static{
    	try{
    		System.loadLibrary("opencv_java248");
    	}catch(Exception e){
    		e.printStackTrace();
    		isLibraryLoaded = false;
    	}
    }
    //区域计算时高度和宽度的参考上限
    private int maxh, maxw;
    //原始图像裁剪后区域
    private MrzOcrRect scanRect;
    //类似于point结构,在计算像素连通区域时用到
    static class Pair{
    	public int first, second;
    	
    	public Pair(){}
    	
    	public Pair(int first, int second){
    		this.first = first;
    		this.second = second;
    	}
    }
    ...
    }
    

    **步骤1:**适度缩放和裁剪图像。这是为了让程序对于文字区域有一个更好的预判,以减少不必要的运行消耗。

    private Mat processSrcImage(InputStream ins){
    	//将位图转换为Mat对象,ins为字节流对象,是主方法的输入参数。
    	Mat matSrc = bmp2Mat(ins, CvType.CV_8UC3);
    	//得到原始图像的宽度和高度
    	int srcWidth = matSrc.width();
    	int srcHeight = matSrc.height();
    	//默认缩放图像与原始图像一致
    	Mat matDest = matSrc;
    	//•设定缩放目标值为1200•
    	Size size = new Size();
    	if(Math.max(srcWidth, srcHeight) > 1200){
    		if(srcWidth > srcHeight){
    			size.width = 1200;
    			size.height = srcHeight * (1200 / srcWidth);
    		}else{
    			size.height = 1200;
    			size.width = srcWidth * (1200 / srcHeight);				
    		}
    		//缩放图像
    		matDest = new Mat((int)size.height, (int)size.width, matSrc.type());
    		Imgproc.resize(matSrc, matDest, size, 0, 0, Imgproc.INTER_AREA);
    	}
    	//•裁剪图像,设定文字的大致区域在图像底部1/3区域•
    	//尽量保证所要提取的字符串在这一区域拥有明显的识别特征
    	int cutHeight = (int)(size.width * 0.3333);
    	Mat matCut = matDest.submat(new Rect(0, size.height - cutHeight, size.width, cutHeight));
    	
    	//•设定字符单元高度上限为裁剪高度的1/4,宽度上限为原始宽度的1/3•
    	//字符在横向排列上可能间距很小,定位出来的字符单元可能包含多个文字
    	maxh = (int)(cutHeight * 0.25);
    	maxw = (int)(oriWidth * 0.3333);
    	//scanRect为Class的成员变量,区域越界校验时使用。
    	scanRect = new MrzOcrRect(0, 0, oriWidth, cutHeight);
    	...
    }
    //将原始图像数据流转换为Mat结构(Mat是opencv库中主要的图像数据结构,也可称为矩阵)。
    private static Mat bmp2Mat(InputStream ins, int type) throws MrzOcrException{
    	Mat matSrc = null;
    	BufferedImage imgbuf = null;
    	try {
    		imgbuf = ImageIO.read(ins);
    		byte[] data = ((DataBufferByte)imgbuf.getRaster().getDataBuffer()).getData();
    		matSrc = new Mat(imgbuf.getHeight(), imgbuf.getWidth(), type);
    		matSrc.put(0, 0, data);
    	} catch (IOException e) {
    		e.printStackTrace();
    	}
    	return matSrc;
    }
    

    **步骤2:**图像灰度化和滤波处理。一般情况下,我们得到的是一个彩色的点阵图像,图像中的每一个点通常由R、G、B三种颜色分量构成。但对于程序来说,这会成为其查找单个物体边界时沉重的程序消耗,图像灰度化处理就是为了让图像有更为简洁的数据结构。灰度图像的像素只要一个灰度值来表示,既不妨碍识别物体边缘,也可大大优化搜索效率。由于图像在拍摄过程中会产生大量随机分布的噪点(就如同声音在传播过程中产生的噪音,噪点则是违背了图像自然原貌,呈现异常颜色的像素点),进行滤波是为了像素之间的颜色过渡更为柔和,以最大程度的消除噪点对于查找物体边界的影响。

    public synchronized String identify(InputStream ins){
    	...
    	matCut = processSrcImage(ins);
    	//灰度处理
    	matGray = new Mat();
    	Imgproc.cvtColor(matCut, matGray, Imgproc.COLOR_BGR2GRAY);
    	//滤波处理
    	matFilter = new Mat();
    	Imgproc.blur(matGray, matFilter, new Size(3, 3));
    	//边缘查找与定位
    	List<MrzOcrMerger> mrzRegions = position(matFilter);
    	...
    }
    

    **步骤3:**边缘查找。这里将使用经典的Canny算法。在此之前,必须确定阈值的上限与下限,如果一个像素的梯度大于上限,则被认为是边缘像素,如果低于下限则被抛弃,如果介于两者之间,只有当其与高于上限阈值的像素连接时才会被接受。由于每张图像的前景与背景的颜色分界线(阈值)是不同的,那么还必须在外围设定逐次递增的阈值搜索区间,以便在多次的边缘查找结果中寻找到最佳结果。

    //返回的是一个字符兼并对象集合。
    private List<MrzOcrMerger> position(Mat matGray){
    	...
    	List<MrzOcrMerger> result = null;
    	Mat matCanny = new Mat();
    	//•设定搜索阈值的初始下限值为30,搜索次数为7次,递增值为10,阈值上限系数为3•
    	int t = 30;
    	for(int i=0; i<7; i++, t+=10){
    		double t1 = t;
    		double t2 = t1 * 3;
    		Imgproc.Canny(matGray, matCanny, t1, t2);
    		//根据边缘检测结果获取像素连通区域并进行字符单元兼并计算,后续会详细介绍getValidAreas方法。
    		List<MrzOcrMerger> areas = getValidAreas(matCanny);
    		matCanny.release();
    		if(areas == null || areas.size() != ls){
    			continue;
    		}else{
    			//装入第一个字符定位结果
    			if(result == null){
    				result = new ArrayList<MrzOcrMerger>();
    				for(int n=0; n<ls; n++){
    					result.add(areas.get(n));
    				}
    			}else{
    				//设定识别字符行数为2
    				for(int j=0; j<2; j++){
    					//获取当前搜索结果中字符串兼并区域的宽度
    					int w1 = areas.get(j).rctotal.getWidth();
    					//获取前次搜索结果中字符串兼并区域的宽度
    					int w2 = result.get(j).rctotal.getWidth();
    					/*随着阈值区间的向上位移,图像物体边缘细节将愈来愈多被忽略,
    					可被查找到的字符区域个数将呈下降趋势。也就是说违背这个规律将被视为异常情况而抛弃*/
    					if(w1 <= w2){
    						int l1 = areas.get(j).areas.size();
    						int l2 = result.get(j).areas.size();
    						//•每行字符数设定为44•
    						//如果当前集合内字符区域个数更接近44,将覆盖前一次搜索结果。
    						if(l1 <= 44 && l1 > l2){
    							result.set(j, areas.get(j));
    						}
    					}
    				}
    			}
    		}
    	}
    	return result;
    }
    

    **步骤4:**计算像素连通区域。值得注意的是,边缘查找后得到的图像已被二值化(图像中只存在黑色与白色两种颜色,也可称之为阈化图像),这是像素连通区域计算的前提。

    //递归调用的优化方案就是使用栈结构来进行迭代搜索
    private List<MrzOcrMerger> getValidAreas(Mat matBin){
    	//源图像的颜色深度必须为8位
    	if(matBin.empty() || matBin.type() != CvType.CV_8UC1){
    		return null;
    	}
    	
    	int rows = matBin.rows();
    	int cols = matBin.cols();
    	
    	List<MrzOcrRect> tempList = new ArrayList<MrzOcrRect>();
    	byte[] data = new byte[rows * cols];
    	matBin.get(0, 0, data);
    	//•设定中心点与紧邻点的水平与纵向搜索范围大小同时为2(实际上是个5*5矩阵)•
    	//由于噪点的存在,在计算连通区域时,边缘中出现一个像素大小的中断时应该同样视为一个有效的连通。
    	int dx = 2, dy = 2;
    	for(int i=dy; i<rows-dy; i++){
    		for(int j=dx; j<cols-dx; j++){
    			//将二维坐标转换为线性数组索引
    			int index = i * cols + j;
    			//判断是否是有效中心点(排除黑色像素点)
    			if(data[index] != 0){
    				//将首次获取到的中心点压入栈中
    				Stack<Pair> pixs = new Stack<Pair>();
    				pixs.push(new Pair(i, j));
    				//防止重复计算
    				data[index] = 0;
    				//初始化连通区域
    				MrzOcrRect rect = new MrzOcrRect(j, i, j, i);
    				while(!pixs.empty()){
    					//从栈中弹出中心点
    					Pair pix = pixs.pop();
    					int y = pix.first;
    					int x = pix.second;
    					//合并中心点与前次搜索区域构成的连通区域
    					if(x < rect.left) rect.left = x;
    					if(x > rect.right) rect.right = x;
    					if(y < rect.top) rect.top = y;
    					if(y > rect.bottom) rect.bottom = y;
    					//获取像素相邻区域。
    					MrzOcrPoint ptc = new MrzOcrPoint(x, y);
    					MrzOcrRect resRect = getNeighbor(ptc);
    					//对相邻区域进行连通判定,并压入栈内,以便迭代搜索
    					for(int n=resRect.left; n<resRect.right; n++){
    						for(int m=resRect.top; m<resRect.bottom; m++){
    							index = m * cols + n;
    							if(data[index] != 0){
    								pixs.push(new Pair(m, n));
    								data[index] = 0;
    							}
    						}
    					}
    				}
    				//以插入排序方式将搜索到的连通区域插入临时集合
    				sortInsert(tempList, rect);
    			}
    		}
    	}
    	//横向兼并连通区域(在下一章节会详细说明)
    	return searchAreas(tempList);
    }
    //根据中心点计算像素相邻区域
    private MrzOcrRect getNeighbor(MrzOcrPoint ptc){
    	MrzOcrRect result = new MrzOcrRect();
    	
    	//•设定像素邻域横向与纵向搜索范围为2个单位•
    	int dx = 2, dy = 2;
    	result.left = ptc.x - dx;
    	result.top = ptc.y - dy;
    	//考虑到运算速度,频繁调用的算式中应避免使用乘除法运算符。
    	result.right = result.left + dx + dx + 1;
    	result.bottom = result.top + dy + dy + 1;
    	//区域越界修正
    	if(result.left < 0) result.left = 0;
    	if(result.top < 0) result.top = 0;
    	if(result.right > scanRect.right) result.right = scanRect.right;
    	if(result.bottom > scanRect.bottom) result.bottom = scanRect.bottom;
    	
    	return result;
    }
    //以插入排序的方式收集像素连通区域
    private void sortInsert(List<MrzOcrRect> list, MrzOcrRect rect){
    	int h = rect.getHeight();
    	int w = rect.getWidth();
    	//•设定最小高度为10,最小宽度为3(例如“1”的宽度)•
    	//过滤掉不符合上述规则的矩形
    	if(h < 10 || h > maxh || w < 3 || w > maxw){
    		return;
    	}
    	//以x坐标值进行升序排序是为了自左向右依次兼并,确保字符的正确排列。
    	for(int i=0; i<list.size(); i++){
    		if(rect.left < list.get(i).left){
    			//该集合内区间[i,n]将向后位移。
    			list.add(i, rect);
    			return;
    		}
    	}
    	list.add(rect);
    }
    
    ----------
    
    //自定义的矩形对象
    public class MrzOcrRect {
    
    	int left, top, right, bottom;
    
    	public MrzOcrRect(){
    	}
    	
    	public MrzOcrRect(int left, int top, int right, int bottom){
    		this.left = left;
    		this.top = top;
    		this.right = right;
    		this.bottom = bottom;
    	}
    	
    	public void setRect(int left, int top, int right, int bottom){
    		this.left = left;
    		this.top = top;
    		this.right = right;
    		this.bottom = bottom;		
    	}
    	
    	public void setRect(final MrzOcrRect rect){
    		left = rect.left;
    		top = rect.top;
    		right = rect.right;
    		bottom = rect.bottom;
    	}
    	//在对输入的矩形进行边缘修正后(正数为收缩,负数为扩展),计算两个矩形是否存在交集
    	//rh为横向修正量,rv为纵向修正量
    	public boolean isIntersect(final MrzOcrRect rect, int rh, int rv){
    		if(left > rect.right - rh) return false;
    		if(right < rect.left + rh) return false;
    		if(top > rect.bottom - rv) return false;
    		if(bottom < rect.top + rv) return false;
    		return true;
    	}
    	//合并矩形
    	public void union(final MrzOcrRect rect){
    		if(left > rect.left) left = rect.left;
    		if(top > rect.top) top = rect.top;				
    		if(right < rect.right) right = rect.right;
    		if(bottom < rect.bottom) bottom = rect.bottom;			
    	}
    	//重载的区域交集运算方法1
    	public boolean isIntersect(final MrzOcrRect rect, int rh){
    		return isIntersect(rect, rh, 0);
    	}
    	//重载的区域交集运算方法1
    	public boolean isIntersect(final MrzOcrRect rect){
    		//•设定横向修正值为3是为了忽略边缘重叠的情况•
    		return isIntersect(rect, 3, 0);
    	}
    	//克隆矩形对象
    	public MrzOcrRect clone(){
    		return new MrzOcrRect(left, top, right, bottom);
    	}
    	
    	public int getHeight(){
    		return bottom - top;
    	}
    	
    	public int getWidth(){
    		return right - left;
    	}
    	//获取横向中心点
    	public double getHCenter(){
    		return (left + right) * 0.5;
    	}
    	//获取纵向中心点
    	public double getVCenter(){
    		return (top + bottom) * 0.5;
    	}
    }
    
    ----------
    
    //自定义坐标点对象
    public class MrzOcrPoint {
    	
    	int x, y;
    	
    	public MrzOcrPoint(int x, int y){
    		this.x = x;
    		this.y = y;
    	}
    	
    	public MrzOcrPoint(){
    		this(0, 0);
    	}
    }
    

    **步骤5:**兼并字符单元。根据字符串具有趋于横向等距排列,字符大小大致相等的特性,对散布的字符单元进行横向兼并运算,在构建应用所需要识别的字符串的同时排除掉其他不符合排列规则的像素连通区域。需要注意的是,由于拍摄角度和场景的不同,图像往往会出现倾斜和平面偏转的情况(文字出现了变形或字符行基线倾斜),那么在设定兼并规则时条件要相对宽松,而且要兼顾整体与局部的对比分析。

    //横向兼并字符单元
    private List<MrzOcrMerger> searchAreas(List<MrzOcrRect> srcList){
    	List<MrzOcrMerger> transList = new ArrayList<MrzOcrMerger>();
    	//第一轮兼并,判断待兼并区域与兼并连接点是否符合一定的兼并条件。
    	int i = 0;
    	LINE1:
    	for(; i<srcList.size(); i++){
    		MrzOcrRect rect = srcList.get(i);
    		for(int j=0; j<transList.size(); j++){
    			//获取兼并对象内字符单元集合的尾部元素
    			//在这里将兼并对象内字符单元集合中的最后一个元素称为兼并连接点
    			MrzOcrRect lastRect = transList.get(j).getLastArea();
    			//获取当前兼并对象内字符单元集合的平均宽度
    			double avgw = transList.get(j).getAvgWidth();
    			//兼并连接点与待兼并区域水平中心线不得出现一个字符高度的偏离(假定这些已定位区域为一个字符)
    			//这里使用连接点与待兼并区域进行比较是考虑到图像出现倾斜与平面偏转的情况。
    			//因为这两个区域在对比上波幅会相对趋缓,也符合逐次递增或递减的特性。
    			if(Math.abs(rect.getVCenter() - lastRect.getVCenter()) 
    				< Math.min(rect.getHeight(), lastRect.getHeight())
    				//•设定字符间隔距离系数为1.5•
    				//字符间隔不得超过平均字符宽度的1.5倍
    				&& rect.left - lastRect.right <= (avgw + rect.getWidth()) * 0.5 * 1.5
    				//•设定字符高度差异系数为0.5•
    				//字符高度不得出现0.5倍高度差距
    				&& Math.abs((double)rect.getHeight() / lastRect.getHeight() - 1) < 0.5
    				//不得出现区域相交(允许边缘重叠3个像素)
    				&& !rect.isIntersect(lastRect)){
    				//将符合兼并规则的区域插入到兼并对象中
    				transList.get(j).add(rect);
    				//在执行兼并操作后跳转到外层循环,以避免重复兼并
    				//注意下标i不是在循环内部定义的
    				continue LINE1;
    			}
    		}
    		//当兼并对象集合为空时或没有可加入的兼并对象时直接插入当前区域。
    		//可视为一个新的兼并起点
    		MrzOcrMerger merger = new MrzOcrMerger();
    		merger.add(rect);
    		transList.add(merger);
    	}
    	//对第一次兼并后的兼并结果总区域做图像比例的比较,
    	//以使其更符合应用需求中识别区域所呈现的图像比例特征
    	List<MrzOcrMerger> filterList = new ArrayList<MrzOcrMerger>();
    	i = 0;
    	LINE2:
    	for(; i<transList.size(); i++){
    		//获取兼并对象内字符单元集合的宽高比例(一行字符串)
    		double ration = transList.get(i).getRatio();
    		int length = transList.get(i).areas.size();
    		//•设定每行文字区域的宽度和高度之比为24:1,字符个数为44•
    		//如果假定单个字符区域为一个正方形,且字符之间存在间距,那么理想的结果应该大于44:1,
    		//但如果假定图像倾斜夹角存在正负15度的浮动,那么44:1的比例就不合适了
    		//图像倾斜的后果就是每行文字总区域的高度大大超过预期
    		//判定总区域比例大于24:1
    		//兼并对象内字符单元个数不得大于44
    		if(ration > 24 && length <= 44){
    			//对符合规则的兼并对象按字符个数降序排序
    			for(int j=0; j<filterList.size(); j++){
    				if(length > filterList.get(j).areas.size()){
    					filterList.add(j, transList.get(i));
    					continue LINE2;
    				}
    			}
    			filterList.add(transList.get(i));
    		}
    	}
    	
    	//•设定文字行数为2•
    	//由于设定识别对象为两行长度相等的字符,所以有必要比较一下两行文字宽度差异是否在预定的范围内
    	if(filterList.size() >= 2){
    		double[] params = new double[2];
    		for(int k=0; k<2; k++){
    			params[k] = filterList.get(k).rctotal.getWidth();
    		}
    		//•设定多行区域最大宽度与最小宽度差异不得大于100•
    		if(max(params) - min(params) <= 100){
    			List<MrzOcrMerger> subList = filterList.subList(0, 2);
    			List<MrzOcrMerger> resList = new ArrayList<MrzOcrMerger>();
    			//对兼并对象集合内元素按y坐标升序排序,以确定文字行的次序
    			i = 0;
    			LINE3:
    			for(; i<subList.size(); i++){
    				for(int j=0; j<resList.size(); j++){
    					if(subList.get(i).rctotal.top < resList.get(j).rctotal.top){
    						resList.add(j, subList.get(i));
    						continue LINE3;
    					}
    				}
    				resList.add(subList.get(i));
    			}
    			return resList;
    		}
    	}
    	
    	return null;
    }
    
    ----------
    
    //字符单元兼并对象
    public class MrzOcrMerger {
    	//兼并的字符单元集合
    	List<MrzOcrRect> areas;
    	
    	MrzOcrRect rctotal;
    	
    	private int htotal, wtotal;
    	
    	public MrzOcrMerger(){
    		areas = new ArrayList<MrzOcrRect>();
    		rctotal = new MrzOcrRect(); 
    	}
    	//获取平均高度
    	public double getAvgHeight(){
    		return (double)htotal / areas.size();
    	}
    	//获取平均宽度
    	public double getAvgWidth(){
    		return (double)wtotal / areas.size();
    	}
    	//获取兼并总区域宽度与高度的比例值
    	public double getRatio(){
    		return (double)rctotal.getWidth() / getAvgHeight();
    	}
    	//获取字符兼并内字符单元集合尾部元素,也可认为是获取可兼并连接点
    	public MrzOcrRect getLastArea(){
    		if(areas.size() > 0)
    			return areas.get(areas.size() - 1);
    		return null;
    	}
    	//兼并字符单元
    	public void add(MrzOcrRect rect){
    		areas.add(rect);
    		htotal += rect.getHeight();
    		wtotal += rect.getWidth();
    		//在兼并时计算总区域大小
    		if(areas.size() == 1){
    			rctotal.setRect(rect);
    		}else{
    			rctotal.union(rect);
    		}	
    	}
    }
    

    步骤6: 根据兼并结果内字符单元集合逐个截取文字图像。这些文字图像在提交到Tesseract模块进行识别之前,还需要进行一些处理。首先使用大津算法对文字图像进行阈化,然后对阈化结果进行画布拉伸(将文字图像置于一个更大的白色画布中,这样可以使Tesseract得到更为丰富的字符边缘信息),之后进行图像形态处理(腐蚀与膨胀),以得到三种形态的文字图像(正常、纤细、加粗),而Tesseract模块在识别图像文字的同时还会返回一个识别评分,将三种形态图像经过三次识别后再根据识别评分择优选取,可以很大程度上提升识别率。由于这一节与算法主题已经偏离,不再作代码展示。

    public synchronized String identify(InputStream ins){
    	...
    	List<MrzOcrMerger> mrzRegions = position(matFilter);
    	for(MrzOcrMerger merger : mrzRegions){
    		for(MrzOcrRect rect : merger.areas){
    			//使用灰度图像作为截取源图像
    			Mat matSub = matGray.submat(new Rect(rect.left, rect.top, 
    				rect.getWidth(), rect.getHeight()));
    			...
    		}
    	}
    	...
    }
    

    这是Android平台下的识别效果图,使用java与c++混编模式编写。
    在这里插入图片描述

    **延伸:**文字定位算法得到的字符单元区域集合对于整幅图像的倾斜校正也是有帮助的。通过线性回归方程式,能计算出由一行文字中每个字符区域的中心点所构成的拟合线,那么这条拟合线的斜率就是图像倾斜校正的参考值。
    由于本人水平有限,可能存在诸多谬误,还望多多指正

    展开全文
  • Android文字基线(Baseline)算法

    千次阅读 2017-11-17 10:47:24
    引言Baseline是文字绘制时所参照的基准线,只有先确定了Baseline的位置,我们才能准确的将文字绘制在我们想要的位置上。Baseline的概念在我们使用TextView等系统控件直接设置文字内容时是用不到的,但是如果我们想要...

    引言

    Baseline是文字绘制时所参照的基准线,只有先确定了Baseline的位置,我们才能准确的将文字绘制在我们想要的位置上。Baseline的概念在我们使用TextView等系统控件直接设置文字内容时是用不到的,但是如果我们想要在Canvas画布上面绘制文字时,Baseline的概念就必不可少了。
    我们先了解一下Android中Canvas画布绘制文字的方法,如下图:
    Android绘制文字方法参数.png

    参数示意:

    • text,文字内容
    • x,文字从画布上开始绘制的x坐标(Canvas是一个原点在左上角的平面坐标系)
    • y,Baseline所在的y坐标,不少人已开始以为y是绘制文字区域的底部坐标,其实是不正确的,这是两个概念
    • paint,画笔,设置的文字的大小颜色等属性
      了解了文字绘制的方法,我们现在就了解一下这个参数y(Baseline)的计算方法。

    Baseline的概念

    我们先看一行文字各区域的分布示意图
    文字区域示意图.png
    从上图来看,Baseline不难理解,它就是E和h的下边界线。我们还可以得出一个结论,文字的高度=Descent+Ascent
    然而,上面这个公式并不完全准确,我们再看一个图:
    文字区域简化计算图.png
    我们看到,如果文字的上方有一些特殊的符号,比如上图中的~或者是我们汉语拼音中的声调时,文字区域又会多出一部分Leading
    因此,完整的公式应该是文字的高度=Descent+Ascent+Leading
    那么,为什么第一幅图中没有说明Leading的存在呢,原因是我们通常在绘制一行英文或者中文时,Leading的高度为0。我们看一个证据图,下图是在绘制英文文字时调试取得的数据。
    文字各区域数值关系图.png
    其中leading=0,所以我们在文字绘制时不需要考虑Leading,图中的数值都是距离Baseline的距离,在Baseline上方为负值,下方为正值。

    Baseline位置(y轴坐标)的计算

    为了方便我们对计算过程进行理解,我画了一幅帮助图,如下:

    文字基线计算图.png

    假设我们是在画布Canvas的顶部绘制一行文字,规定一行文字的高度是y,文字区域的高度是Height(TOP和BOTTOM之间,TOP到0和BOTTOM到y的距离相等,这样文字才看起来是居中)。因此,0到y和TOP到BOTTOM的中线是重合的,y轴坐标都是y/2。
    我们要绘制一行文字时,设计必然会告诉我们0到y的距离,所以中线的位置也是固定的y/2,那么我们设置了Paint的文字大小后,Ascent和Descent又能直接得到,就可以算出中线到基线的距离,公式如下:
    基线到中线的距离=(Descent+Ascent)/2-Descent
    注意,实际获取到的Ascent是负数。公式推导过程如下:
    中线到BOTTOM的距离是(Descent+Ascent)/2,这个距离又等于Descent+中线到基线的距离,即(Descent+Ascent)/2=基线到中线的距离+Descent
    有了基线到中线的距离,我们只要知道任何一行文字中线的位置,就可以马上得到基线的位置,从而得到Canvas的drawText方法中参数y的值。

    Android获取中线到基线距离的代码,Paint需要设置文字大小textsize。

        /**
         * 计算绘制文字时的基线到中轴线的距离
         * 
         * @param p
         * @param centerY
         * @return 基线和centerY的距离
         */
        public static float getBaseline(Paint p) {
            FontMetrics fontMetrics = p.getFontMetrics();
            return (fontMetrics.descent - fontMetrics.ascent) / 2 -fontMetrics.descent;
        }
    展开全文
  • 详解一个完整的系统工程,通过Python+OpenCV识别图片中的表格并原样还原为Excel文件,涉及到Python矩阵运算、OpenCV各种处理、文字区域检测、文本OCR等丰富的内容,希望能帮助到入手Python做系统性任务的朋友们。

    Python Opencv 图片识别表格:线条信息计算(投影算法)

    通过OpenCV算法识别出图片中的框线后,面临几个问题:

    线条变换为绝对水平和垂直

    线条并非绝对的水平或垂直,需要变换为绝对的水平线或垂直线。简单的算法是可以选择线条的起点或终点作为坐标,但最好是可以选择线条的中心的坐标。这里设计了一种轴心投影法,通过线条在垂直轴上的投影得到中心点的坐标。以横线条为例,将线条的cv矩阵传入,计算在数据i轴上的投影,得到中心的i位置,即为绝对水平线的y轴坐标。这里一定要注意数据矩阵中的i、j分别对应着cv坐标轴的y和x。
    在这里插入图片描述
    通过投影算法,可以得到线条垂直坐标轴的轴心,同理,将线条在j轴上进行投影就可以得到线条的绝对水平或垂直的长度。

    仔细分析后我们会发现,在i轴上进行投影算法能得到绝对横线的宽度,在j轴上投影可以得到绝对横线的长度。那我们是否需要写一个i轴投影算法和一个j轴投影算法呢?其实投影原理都一样,只是投影的轴不同。如果将横线转置,向i轴投影,是不是就得到了绝对横线的长度呢?这样我们就可以复用同一套投影算法了。

    如果是竖线怎么办呢?竖线刚好相反,向j轴投影得到宽度,向i轴投影得到长度。如何复用同一套线段信息计算代码呢?没错,将竖线转置后就变成横线了,算法一致了!

    完成了以上思路的梳理,我们就可以写出对应的投影算法了。

    线条的断裂

    另一个问题是线条的断裂,由于干扰因素导致一根线条中间出现了断裂,为了应对这种问题,我们在投影计算时支持了break,即允许在投影截面上出现几个像素的断裂。

    在这里插入图片描述

    线条被分割为多条线段

    不是所有的表格框线都是一直连续的,看下面这个例子,红框中的表格框线被两个合并单元格分割成了三段。在线条信息计算时,需要精确的计算出每条线段起止坐标信息。为什么呢?这是因为稍后我们进行单元格重建算法的时候,需要知道表格中每一个横纵交叉点的完整情况,来决定每个单元格到底有几行几列。这个问题在表格结构分析算法会详细谈到的。
    在这里插入图片描述

    完整算法

    根据以上的分析,我们写出了下面完整的算法,实现了线条的投影计算、宽度计算、长度计算、线条中的线段信息的计算,支持了干扰造成的断裂等情况。

    '''
        计算线条的完整信息,包括线条的轴心位置、线条起止pos、内部线段的信息
        原理:通过坐标轴投影算法,获取垂直于该坐标轴的线段在该轴上的中心位置
        前提:输入的lines_matrix必须是i,j二维数组,且i为要投影的坐标轴(相当于cv图像的y轴,若要投影x轴请先转置后传入)
        输入:lines_matrix-线段的二值变反矩阵;max_break-最大支持线段中间出现的断裂(像素数量)
        输出:线段信息list
    
        line_info = {
            'axis': 0, # 线条轴心
            'wide': 0, # 线条粗细
            'len': 0,  # 线条总长度(线段长度之和)
            'segment': [], # 线条内部的线段信息 [[线段长度, 线段start, 线段end],...] 支持一根线条被分割为多条线段(中间跨域一个或多个合并单元格)
        }
    
        输入示例:lines_matrix
        i0-------------------------> j
        i1-------------------------> j
        i2-------------------------> j
        i3-------------------------> j
    '''
    def calc_line_info(lines_matrix, max_break, debug=False):
    
        # 计算i轴每个位置的投影是否有像素值
        project_i = [any(x) for x in lines_matrix] # 对每个i对应的list进行any操作,求出i轴上该位置是否出现像素点
    
        # 取出有像素值的i轴pos
        pos_i = [i for i,x in enumerate(project_i) if x==True]
    
        # 异常检测:若只检测到一条线或没有线 则返回空
        if len(pos_i)<=1:
            return []
    
        # 将连续的pos分组(支持连续pos出现10个像素的断裂)
        pos_group_i = [] 
        temp_group = [pos_i[0]] # 第一个pos默认满足要求,放入临时结果中
    
        # 可调参数 线段断层截面像素点
        for i in range(1,len(pos_i)): # 从第二个pos开始计算
            if pos_i[i]-pos_i[i-1]<=5: # 连续像素计为一组,支持截面断层
                temp_group.append(pos_i[i])
            else:
                pos_group_i.append(temp_group)  # 上一组结束,放入结果中
                temp_group = [pos_i[i]] # 新一组第一个pos默认满足要求,放入临时结果中
        
        # 最后一组pos放入结果中
        pos_group_i.append(temp_group)
    
        '''
            线条信息 数据结构
        '''
        line_info = {
            'axis': 0, # 线条轴心
            'wide': 0, # 线条粗细
            'len': 0,  # 线条总长度(线段长度之和)
            'segment': [], # 线条内部的线段信息 [[线段长度, 线段start, 线段end],...] 支持一根线条被分割为多条线段(中间跨域一个或多个合并单元格)
        }
    
        lines_info = []
        for (i, poses) in enumerate(pos_group_i):
            info = line_info.copy()
            info['axis'] = int(np.median(poses))
            info['wide'] = poses[-1]-poses[0]+1
    
            '''
                计算图像中线段的长度(即在j轴像素点的个数)
                注意有可能一条线段被分割成了多个分段(中间出现了合并单元格),因此该算法需要返回线段长度list[]
                同时要支持干扰造成的线段中间出现的断裂
            '''
    
            # 获取该条线所在的矩阵 并转置
            area = np.transpose( lines_matrix[poses[0]:poses[-1]+1] )
    
            # 取得每个投影点的像素情况
            mask = [str(any(x)+0) for x in area]  # any(x)+0可以将True False转为1 0
    
            # 将mask中连续的1分割出来,每段连续的1即为一条线段
            s = ''.join(mask).split('0')
            segs = [ [i,len(v)] for (i,v) in enumerate(s) if len(v)>0 ]
    
            # 调整每条线段在原始list中正确的pos(segs中的i只是s数组中的位置,并非mask中的位置)
            for i in range(1,len(segs)):
                # 每条线段的pos = segs中的i + 之前线段的总长度
                segs[i][0] = segs[i][0] + sum([x[1]-1 for (j,x) in enumerate(segs) if j<i])
    
            segments = [segs[0]] # 初始化为第一条线段
    
            # 若有多条线段,进行智能线段分析:因干扰造成的10像素内的断裂自动连在一起
            MAX_LEN_BREAK = max_break # 最大支持线段断裂长度
            if len(segs)>1:
                # 从第二条线段开始判断与上一条线段之间是断裂还是间隔
                for i in range(1,len(segs)):
                    delta = segs[i][0] - segs[i-1][0] - segs[i-1][1]
                    if delta<MAX_LEN_BREAK:
                        # 小于断裂长度,应连接线段
                        segments[-1] = [ segments[-1][0], segs[i][0]-segments[-1][0]+segs[i][1] ]
                    else:
                        # 为间隔,是不同的线段
                        segments.append(segs[i])
    
            # 线段数据重组为:[线段长度, 线段start, 线段end]
            segments = [ [x[1], x[0], x[0]+x[1]] for x in segments ]
    
            info['segment'] = segments
            info['len'] = sum([x[0] for x in segments])
    
            lines_info.append(info)
    
        if debug:
            print('\n\n----------lines info------------')
            x = [print(v) for v in lines_info]
    
        return lines_info
    

    那么calc_line_info传入的参数是什么呢?就是上一篇文章讲到的横线检测和竖线检测算法得到的横线和竖线的cv矩阵。

    展开全文
  • 文字检测算法——PSENet阅读笔记

    千次阅读 2020-05-05 14:43:38
    目前文字检测存在的挑战 大多数最先进的算法都需要精确的四边形bounding box来定位任意形状的文本,而不能检测curve文本,如Fig. 1(b) 对于两个比较接近的文本行可能会导致一个错误的检测,检测结果会覆盖两个...
  • A Single-Shot Arbitrarily-Shaped Text Detector based on,Context Attended Multi-Task Learning ...百度自研文字检测算法,实际上就是EAST算法的扩展,一阶段,输出为multitask,各个分支相互校正。
  • 收藏 | 一文突破人工智能AI十大算法

    千次阅读 2021-05-13 16:28:37
    点上方蓝字人工智能算法与Python大数据获取更多干货在右上方···设为星标★,第一时间获取资源仅做学术分享,如有侵权,联系删除转载于 :图灵人工智能通过本篇文章大家可以对人工智能A...
  • 极简算法

    千次阅读 2019-02-18 23:30:34
    内容简介 数学、逻辑学、计算机科学三大领域实属一家,彼此成就,彼此影响。从古希腊哲学到计算机,数字、计算、推理这些貌似简单的概念在三千年里融汇、碰撞。...本书描绘了一场人类探索数学、算法与逻辑...
  • 目录简介一、监督学习1、决策树(Decision Tree,DT)2、朴素贝叶斯分类器(Naive Bayesian Model,NBM)3、最小二乘法(Least squares)4、逻辑回归(Logistic Regression)5、支持向量机(SVM)6、K最近邻算法...
  • 算法之美

    千次阅读 2018-08-14 00:44:39
    在购物中心或者网上购物时,人们可以反复权衡再做出决定,但是将要入住旧金山的租客没有这个特权,他们必须迅速做出决定:要么舍弃其他所有可能的选择,就选定当前正在看的这套房子,要么掉头就走,再也不要回头。...
  • 接上文 计算机图形学 学习笔记(五):多边形裁剪,文字裁剪光栅图形学算法4.1 消隐算法简介和分类消隐当我们观察空间任何一个不透明的物体时,只能看到该物体朝向我们的那些表面,其余的表面由于被物体所遮挡我们看...
  • PCA算法原理及实现

    千次阅读 2020-08-07 22:05:25
    0.1), 其中地区这一项显得与众特别,毕竟其他的维度都是数值,就它是文字。我们把这样的维度称为类别,因为它是在有限的选项中选出来的(从世界上所有的地区中取一个),在计算机中表示这样的信息,我们可以有很多...
  • 双线性插值算法的详细总结

    万次阅读 多人点赞 2016-04-22 15:25:38
    里面用到了图像的单应性矩阵变换,在最后的图像重映射,由于目标图像的坐标是非整数的,所以需要用到插值的方法,用的就是双线性插值,下面的博文主要是查看了前辈的博客对双线性插值算法原理进行了一个总结,在这里...
  • 文字识别分为两个具体步骤:文字的检测和文字的识别,两者缺一不可,尤其是文字检测,是识别的前提条件,若文字都找不到,那何谈文字识别。今天我们首先来谈一下当今流行的文字检测技术有哪些。 文本检测不是一件...
  • 算法分析

    千次阅读 2020-07-29 22:19:53
    算法分析 基础知识 算法定义: 程序 = 算法 + 数据结构 算法是问题求解的有效策略.是解某一特定问题的一组有穷规则的集合。 算法特征 : 有限性、确定性、输入、输出、能行性 算法复杂性: 算法运行所需要的计算机...
  • 文本检测算法:CRAFT(CVPR2019)

    千次阅读 2020-10-13 20:03:56
    多种文本检测算法性能对比 (https://blog.csdn.net/qq_39707285/article/details/108754444) Character Region Awareness for Text Detection1. 关键点2. 算法2.1 网络结构2.2 训练2.2.1 生成GT 1. 关键点 先前的...
  • 浅析基于二维轮廓线重构表面算法

    千次阅读 2014-02-13 17:04:10
    轮廓线重构算法  由一组二维轮廓线重建出物体的三维表面是三维数据场可视化中的一种表面绘制方法。在医学图像可视化以及其他可视化领域中有着广泛的应用。三维表面重建实际上是对物体表面进行三角形划分,从轮廓...
  • 购物网站用算法来为你推荐商品,点评网站用算法来帮你选择餐馆,GPS 系统用算法来帮你选择好的路线,公司用算法来选择求职者……当机器最终学会如何学习时,将会发生什么? 不同于传统算法,现在悄然主导我们生活的...
  • 多种文本检测算法性能对比 (https://blog.csdn.net/qq_39707285/article/details/108754444) 论文题目:Character Region Awareness for Text Detection1. 关键点2. 算法2.1 综述2.2 检测阶段2.3 共享阶段 Character...
  • 文字识别(一)--传统方案综述

    万次阅读 多人点赞 2019-02-17 12:48:15
    文字识别是计算机视觉研究领域的分支之一,归属于模式识别和人工智能,是计算机科学的重要组成部分,本文将以上图为主要线索,简要阐述在文字识别领域中的各个组成部分(更侧重传统非深度学习端到端方案)。...
  • 图像学之底层算法基石其一

    千次阅读 2018-01-27 00:12:17
    直线段的扫描转换算法多边形的扫描转换与区域填充算法裁剪算法反走样算法消隐算法 三、直线具体算法 一、直线的扫描转换算法  在计算机光栅显示器屏幕上表示一条直线时,由于光栅显示屏是由像素点构成的,...
  • 最近邻插值 与 双线性插值算法 优化迭代 的 0.5 像素之差
  • 协同过滤推荐算法及应用

    万次阅读 多人点赞 2018-05-09 09:36:03
    1. CF协同过滤推荐算法原理1.1 概述什么是协同过滤 (Collaborative Filtering, 简称 CF)?首先想一个简单的问题,如果你现在想看个电影,但你不知道具体看哪部,你会怎么做?大部分的人会问问周围的朋友,看看最近有...
  • 算法是芯片、操作系统乃至整个信息系统的基础,我国是信息技术应用大国,算法的研究在我国却及其薄弱。时至今日,我国还在被国外技术封锁、核心技术卡脖子,而算法创新才能实现信息系统的创新,才能实现操作系统的...
  • 基本图形生成算法

    千次阅读 2015-12-20 21:09:50
    基本图形生成算法 DDA和Bresenham直线段生成算法; Bresenham圆弧生成算法
  • AI产品经理需要懂的算法和模型

    千次阅读 2018-11-28 16:57:05
    一个产品经理经常疑惑的概念:算法和模型的关系,产品经理懂得解决问题时将问题抽象为模型,对模型求解用算法,没有谁大谁小,算法和模型没有绝对的分界线。 这篇将主要从时下各种算法模型用于精准推荐都有其各自的...
  • PSO算法文献阅读笔记

    千次阅读 2020-10-08 14:20:10
    粒子群算法读书笔记精读 2020《电子信息学报》基于非线性降维的自然计算方法 孙小晴(2020-04-28) 1针对问题 高维大规模优化问题,陷入局部最优与收敛速度和时间复杂度的矛盾。 2创新点 非线性降维思想 - NDR 将...
  • 字符识别处理的信息可分为两大类:一类是文字信息,处理的主要是用各国家、各民族的文字(如:汉字,英文等)书写或印刷的文本信息,目前在印刷体和联机手写方面技术已趋向成熟,并推出了很多应用系统;另一类是...
  • 计算机图形学之光线跟踪算法的研究与实现2017年我的优秀毕业论文 版权所有使用者请联系我 刘创 QQ:903188593 2.2.2 Phong光照模型 事实上对于漫反射的物体表面,使用Lambert就足够,但是实际生活中并...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 17,040
精华内容 6,816
关键字:

文字中心线算法