本文描述一种利用OpenCV及傅里叶变换识别图片中文本旋转角度并自动校正的方法,由于对C#比较熟,因此本文将使用OpenCVSharp。 文章参考了http://johnhany.net/2013/11/dft-based-text-rotation-correction,对原作者表示感谢。我基于OpenCVSharp用C#进行了重写,希望能帮到同样用OpenCVSharp的同学。

================= 正文开始 =================

手里有一张图片如下,是经过旋转的,如何通过程序自动对它进行旋转校正? (旋转校正是行分割、字符识别等后续工作的基础)

傅里叶变换可以用于将图像从时域转换到频域,对于分行的文本,其频率谱上一定会有一定的特征,当图像旋转时,其频谱也会同步旋转,因此找出这个特征的倾角,就可以将图像旋转校正回去。

先来对原始图像进行一下傅里叶变换,需要这么几步:

1、以灰度方式读入原文件

string filename = "source.jpg";
var src = IplImage.FromFile(filename, LoadMode.GrayScale);

2、将图像扩展到合适的尺寸以方便快速变换

OpenCV中的DFT对图像尺寸有一定要求,需要用GetOptimalDFTSize方法来找到合适的大小,根据这个大小建立新的图像,把原图像拷贝过去,多出来的部分直接填充0。

int width = Cv.GetOptimalDFTSize(src.Width);
int height = Cv.GetOptimalDFTSize(src.Height);
var padded = new IplImage(width, height, BitDepth.U8, );//扩展后的图像,单通道
Cv.CopyMakeBorder(src, padded, new CvPoint(, ), BorderType.Constant, CvScalar.ScalarAll());

3、进行DFT运算

DFT要分别计算实部和虚部,这里准备2个单通道的图像,实部从原图像中拷贝数据,虚部清零,然后把它们Merge为一个双通道图像再进行DFT计算,完成后再Split开。

//实部、虚部(单通道)
var real = new IplImage(padded.Size, BitDepth.F32, );
var imaginary = new IplImage(padded.Size, BitDepth.F32, );
//合成(双通道)
var fourier = new IplImage(padded.Size, BitDepth.F32, ); //图像复制到实部,虚部清零
Cv.ConvertScale(padded, real);
Cv.Zero(imaginary); //合并、变换、再分解
Cv.Merge(real, imaginary, null, null, fourier);
Cv.DFT(fourier, fourier, DFTFlag.Forward);
Cv.Split(fourier, real, imaginary, null, null);

4、对数据进行适当调整

上一步中得到的实部保留下来作为变换结果,并计算幅度:magnitude = sqrt(real^2 + imaginary^2)。

考虑到幅度变化范围很大,还要用log函数把数值范围缩小。

最后经过归一化,就会得到图像的特征谱了。

//计算sqrt(re^2+im^2),再存回re
Cv.Pow(real, real, 2.0);
Cv.Pow(imaginary, imaginary, 2.0);
Cv.Add(real, imaginary, real);
Cv.Pow(real, real, 0.5); //计算log(1+re),存回re
Cv.AddS(real, CvScalar.ScalarAll(), real);
Cv.Log(real, real); //归一化
Cv.Normalize(real, real, , , NormType.MinMax);

此时图像是这样的:

5、移动中心

DFT操作的结果低频部分位于四角,高频部分在中心,习惯上会把频域原点调整到中心去,也就是把低频部分移动到中心。

/// <summary>
/// 将低频部分移动到图像中心
/// </summary>
/// <param name="p_w_picpath"></param>
/// <remarks>
/// 0 | 3 2 | 1
/// ------- ===> -------
/// 1 | 2 3 | 0
/// </remarks>
private static void ShiftDFT(IplImage p_w_picpath)
{
int row = p_w_picpath.Height;
int col = p_w_picpath.Width;
int cy = row / ;
int cx = col / ; var q0 = p_w_picpath.Clone(new CvRect(, , cx, cy)); //左上
var q1 = p_w_picpath.Clone(new CvRect(, cy, cx, cy)); //左下
var q2 = p_w_picpath.Clone(new CvRect(cx, cy, cx, cy)); //右下
var q3 = p_w_picpath.Clone(new CvRect(cx, , cx, cy)); //右上 Cv.SetImageROI(p_w_picpath, new CvRect(, , cx, cy));
q2.Copy(p_w_picpath);
Cv.ResetImageROI(p_w_picpath); Cv.SetImageROI(p_w_picpath, new CvRect(, cy, cx, cy));
q3.Copy(p_w_picpath);
Cv.ResetImageROI(p_w_picpath); Cv.SetImageROI(p_w_picpath, new CvRect(cx, cy, cx, cy));
q0.Copy(p_w_picpath);
Cv.ResetImageROI(p_w_picpath); Cv.SetImageROI(p_w_picpath, new CvRect(cx, , cx, cy));
q1.Copy(p_w_picpath);
Cv.ResetImageROI(p_w_picpath);
}

最终得到图像如下:

可以明显的看到过中心有一条倾斜的直线,可以用霍夫变换把它检测出来,然后计算角度。 需要以下几步:

1、二值化

把刚才得到的傅里叶谱放到0-255的范围,然后进行二值化,此处以150作为分界点。

Cv.Normalize(real, real, , , NormType.MinMax);
Cv.Threshold(real, real, , , ThresholdType.Binary);

得到图像如下:

2、Houge直线检测

由于HoughLine2方法只接受8UC1格式的图片,因此要先进行转换再调用HoughLine2方法,这里的threshold参数取的90,能够检测出3条直线来。

//构造8UC1格式图像
var gray = new IplImage(real.Size, BitDepth.U8, );
Cv.ConvertScale(real, gray); //找直线
var storage = Cv.CreateMemStorage();
var lines = Cv.HoughLines2(gray, storage, HoughLinesMethod.Standard, , Cv.PI / , 9);

3、找到符合条件的那条斜线,获取角度

float angel = 0f;
float piThresh = (float)Cv.PI / ;
float pi2 = (float)Cv.PI / ;
for (int i = ; i < lines.Total; ++i)
{
//极坐标下的点,X是极径,Y是夹角,我们只关心夹角
var p = lines.GetSeqElem<CvPoint2D32f>(i);
float theta = p.Value.Y;
if (Math.Abs(theta) >= piThresh && Math.Abs(theta - pi2) >= piThresh)
{
angel = theta;
break;
}
}
angel = angel < pi2 ? angel : (angel - (float)Cv.PI);

4、角度转换

由于DFT的特点,只有输入图像是正方形时,检测到的角度才是真正文本的旋转角度,但原图像明显不是,因此还要根据长宽比进行变换,最后得到的angelD就是真正的旋转角度了。

if (angel != pi2)
{
float angelT = (float)(src.Height * Math.Tan(angel) / src.Width);
angel = (float)Math.Atan(angelT);
}
float angelD = angel * / (float)Cv.PI;

5、旋转校正

这一步比较简单了,构建一个仿射变换矩阵,然后调用WarpAffine进行变换,就得到校正后的图像了。最后显示到界面上。

var center = new CvPoint2D32f(src.Width / 2.0, src.Height / 2.0);//图像中心
var rotMat = Cv.GetRotationMatrix2D(center, angelD, 1.0);//构造仿射变换矩阵
var dst = new IplImage(src.Size, BitDepth.U8, ); //执行变换,产生的空白部分用255填充,即纯白
Cv.WarpAffine(src, dst, rotMat, Interpolation.Cubic | Interpolation.FillOutliers, CvScalar.ScalarAll()); //展示
using (var win = new CvWindow("Rotation"))
{
win.Image = dst;
Cv.WaitKey();
}

最终结果如下,效果还不错:

最后放完整代码:

using System;
using System.Collections.Generic;
using System.IO;
using System.Text; using OpenCvSharp;
using OpenCvSharp.Extensions;
using OpenCvSharp.Utilities; namespace OpenCvTest
{
class Program
{
static void Main(string[] args)
{
//以灰度方式读入原文件
string filename = "source.jpg";
var src = IplImage.FromFile(filename, LoadMode.GrayScale); //转换到合适的大小,以适应快速变换
int width = Cv.GetOptimalDFTSize(src.Width);
int height = Cv.GetOptimalDFTSize(src.Height);
var padded = new IplImage(width, height, BitDepth.U8, );
Cv.CopyMakeBorder(src, padded, new CvPoint(, ), BorderType.Constant, CvScalar.ScalarAll()); //实部、虚部(单通道)
var real = new IplImage(padded.Size, BitDepth.F32, );
var imaginary = new IplImage(padded.Size, BitDepth.F32, );
//合并(双通道)
var fourier = new IplImage(padded.Size, BitDepth.F32, ); //图像复制到实部,虚部清零
Cv.ConvertScale(padded, real);
Cv.Zero(imaginary); //合并、变换、再分解
Cv.Merge(real, imaginary, null, null, fourier);
Cv.DFT(fourier, fourier, DFTFlag.Forward);
Cv.Split(fourier, real, imaginary, null, null); //计算sqrt(re^2+im^2),再存回re
Cv.Pow(real, real, 2.0);
Cv.Pow(imaginary, imaginary, 2.0);
Cv.Add(real, imaginary, real);
Cv.Pow(real, real, 0.5); //计算log(1+re),存回re
Cv.AddS(real, CvScalar.ScalarAll(), real);
Cv.Log(real, real); //归一化,落入0-255范围
Cv.Normalize(real, real, , , NormType.MinMax); //把低频移动到中心
ShiftDFT(real); //二值化,以150作为分界点,经验值,需要根据实际情况调整
Cv.Threshold(real, real, , , ThresholdType.Binary); //由于HoughLines2方法只接受8UC1格式的图片,因此进行转换
var gray = new IplImage(real.Size, BitDepth.U8, );
Cv.ConvertScale(real, gray); //找直线,threshold参数取90,经验值,需要根据实际情况调整
var storage = Cv.CreateMemStorage();
var lines = Cv.HoughLines2(gray, storage, HoughLinesMethod.Standard, , Cv.PI / , 9); //找到符合条件的那条斜线
float angel = 0f;
float piThresh = (float)Cv.PI / ;
float pi2 = (float)Cv.PI / ;
for (int i = ; i < lines.Total; ++i)
{
//极坐标下的点,X是极径,Y是夹角,我们只关心夹角
var p = lines.GetSeqElem<CvPoint2D32f>(i);
float theta = p.Value.Y; if (Math.Abs(theta) >= piThresh && Math.Abs(theta - pi2) >= piThresh)
{
angel = theta;
break;
}
}
angel = angel < pi2 ? angel : (angel - (float)Cv.PI);
Cv.ReleaseMemStorage(storage); //转换角度
if (angel != pi2)
{
float angelT = (float)(src.Height * Math.Tan(angel) / src.Width);
angel = (float)Math.Atan(angelT);
}
float angelD = angel * / (float)Cv.PI;
Console.WriteLine("angtlD = {0}", angelD); //旋转
var center = new CvPoint2D32f(src.Width / 2.0, src.Height / 2.0);
var rotMat = Cv.GetRotationMatrix2D(center, angelD, 1.0);
var dst = new IplImage(src.Size, BitDepth.U8, );
Cv.WarpAffine(src, dst, rotMat, Interpolation.Cubic | Interpolation.FillOutliers, CvScalar.ScalarAll()); //显示
using (var window = new CvWindow("Image"))
{
window.Image = src;
using (var win2 = new CvWindow("Dest"))
{
win2.Image = dst;
Cv.WaitKey();
}
}
} /// <summary>
/// 将低频部分移动到图像中心
/// </summary>
/// <param name="p_w_picpath"></param>
/// <remarks>
/// 0 | 3 2 | 1
/// ------- ===> -------
/// 1 | 2 3 | 0
/// </remarks>
private static void ShiftDFT(IplImage p_w_picpath)
{
int row = p_w_picpath.Height;
int col = p_w_picpath.Width;
int cy = row / ;
int cx = col / ; var q0 = p_w_picpath.Clone(new CvRect(, , cx, cy));//左上
var q1 = p_w_picpath.Clone(new CvRect(, cy, cx, cy));//左下
var q2 = p_w_picpath.Clone(new CvRect(cx, cy, cx, cy));//右下
var q3 = p_w_picpath.Clone(new CvRect(cx, , cx, cy));//右上 Cv.SetImageROI(p_w_picpath, new CvRect(, , cx, cy));
q2.Copy(p_w_picpath);
Cv.ResetImageROI(p_w_picpath); Cv.SetImageROI(p_w_picpath, new CvRect(, cy, cx, cy));
q3.Copy(p_w_picpath);
Cv.ResetImageROI(p_w_picpath); Cv.SetImageROI(p_w_picpath, new CvRect(cx, cy, cx, cy));
q0.Copy(p_w_picpath);
Cv.ResetImageROI(p_w_picpath); Cv.SetImageROI(p_w_picpath, new CvRect(cx, , cx, cy));
q1.Copy(p_w_picpath);
Cv.ResetImageROI(p_w_picpath);
}
}
}

OpenCV.Net基于傅里叶变换进行文本的旋转校正的更多相关文章

  1. OpenCV基于傅里叶变换进行文本的旋转校正

    傅里叶变换可以用于将图像从时域转换到频域,对于分行的文本,其频率谱上一定会有一定的特征,当图像旋转时,其频谱也会同步旋转,因此找出这个特征的倾角,就可以将图像旋转校正回去. 先来对原始图像进行一下傅里 ...

  2. OpenCV实现基于傅里叶变换的旋转文本校正

    代码 先给出代码,再详细解释一下过程: #include <opencv2/core/core.hpp> #include <opencv2/imgproc/imgproc.hpp& ...

  3. 基于css3的3D立方体旋转特效

    今天给大家分享一款基于css3的3D立方体旋转特效.这款特效适用浏览器:360.FireFox.Chrome.Safari.Opera.傲游.搜狗.世界之窗. 不支持IE8及以下浏览器.效果图如下 : ...

  4. (原)使用opencv的warpAffine函数对图像进行旋转

    转载请注明出处: http://www.cnblogs.com/darkknightzh/p/5070576.html 参考网址: http://stackoverflow.com/questions ...

  5. tensorflow实现基于LSTM的文本分类方法

    tensorflow实现基于LSTM的文本分类方法 作者:u010223750 引言 学习一段时间的tensor flow之后,想找个项目试试手,然后想起了之前在看Theano教程中的一个文本分类的实 ...

  6. 一文详解如何用 TensorFlow 实现基于 LSTM 的文本分类(附源码)

    雷锋网按:本文作者陆池,原文载于作者个人博客,雷锋网已获授权. 引言 学习一段时间的tensor flow之后,想找个项目试试手,然后想起了之前在看Theano教程中的一个文本分类的实例,这个星期就用 ...

  7. 基于animation.css实现动画旋转特效

    分享一款基于animation.css实现动画旋转特效.这是一款基于CSS3实现的酷炫的动画旋转特效代码.效果图如下: 在线预览   源码下载 实现的代码. html代码: <div class ...

  8. 基于 Spark 的文本情感分析

    转载自:https://www.ibm.com/developerworks/cn/cognitive/library/cc-1606-spark-seniment-analysis/index.ht ...

  9. (4.2)基于LingPipe的文本基本极性分析【demo】

    酒店评论情感分析系统(四)—— 基于LingPipe的文本基本极性分析[demo] (Positive (favorable) vs. Negative (unfavorable)) 这篇文章为Lin ...

随机推荐

  1. Define the Data Model and Set the Initial Data 定义数据模型并设置初始数据

    This topic describes how to define the business model and the business logic for WinForms and ASP.NE ...

  2. Dynamics CRM 2015/2016新特性之三十三:有了ExecuteTransactionRequest,再也不用担心部分成功部分失败了

    关注本人微信和易信公众号: 微软动态CRM专家罗勇 ,回复216或者20160329可方便获取本文,同时可以在第一间得到我发布的最新的博文信息,follow me!我的网站是 www.luoyong. ...

  3. linux用户管理章节笔记

    1 更改有效用户组 :newgrp zeng 把当前用户的有效用户组更改为zeng.事后可以使用groups命令查看. 2 在使用useradd命令增加用户时,在/etc/passwd的值一般会参考 ...

  4. 记录C#-WPF布局面板

    StackPanel:适合水平或者垂直方向的布局 DockPanel:区域布局 WrapPanel:自动换行的StackPanel布局 Grid:网格布局

  5. [MySQL] mysql地理位置服务geometry字段类型

    这个字段类型是mysql5.7新增的功能,主要就是解决坐标存储和距离计算的常见问题 创建表:CREATE TABLE `service` ( `id` bigint(20) NOT NULL AUTO ...

  6. Centos系统配置bond0

    版权声明:本文为博主原创文章,支持原创,转载请附上原文出处链接和本声明. 本文链接地址:https://www.cnblogs.com/wannengachao/p/11942254.html 1.查 ...

  7. PAT 1145 1078| hashing哈希表 平方探测法

    pat 1145: 参考链接 Quadratic probing (with positive increments only) is used to solve the collisions.:平方 ...

  8. mybatis中<include>标签的作用

    MyBatis中sql标签定义SQL片段,include标签引用,可以复用SQL片段 sql标签中id属性对应include标签中的refid属性.通过include标签将sql片段和原sql片段进行 ...

  9. C getchar()

    C getchar() #include <stdio.h> int main() { ; char str[size]; ; char ch; printf("Enter wh ...

  10. JWT签名与验签

    签名Token生产 using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; usi ...