本文描述一种利用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. Shell(三):echo、printf、test命令

    一.echo 1.显示普通字符串: echo "today is a wonderful day" 这里的双引号可以省略. 2.显示转义字符: echo "\" ...

  2. 软件设计之基于Java的连连看小游戏(二)——游戏基础界面的制作及事件的添加

    上次完成到游戏首页的制作,今天完成了游戏基础界面的制作以及事件的简单添加.由于功能尚未完全实现,因此游戏界面的菜单列表只是简单地添加了一下,其余菜单列表以及倒计时等在后续的制作中逐一完善. 1.首先在 ...

  3. Flask(Jinja2) 服务端模板注入漏洞(SSTI)

    flask Flask 是一个 web 框架.也就是说 Flask 为你提供工具,库和技术来允许你构建一个 web 应用程序.这个 wdb 应用程序可以使一些 web 页面.博客.wiki.基于 we ...

  4. golang的析构函数

    runtime.SetFinalizer 使用这个函数可以给一个对象设置一个析构函数,如果这个对象没有引用了,那么就会调用这个析构函数,然后会把这个对象给释放掉

  5. Windows Redis 开机启动后台运行

    1. 从 Redis 的安装目录进入 cmd 2. 在 cmd 中输入, 将Redis绑定为 Windows 服务, 并设置为后台启动: redis-server --service-install ...

  6. Git入门基础教程和SourceTree应用

    目录 一.Git的安装 1.1 图形化界面 1.2 命令行界面 二.本地仓库的创建与提交 2.1 图形化界面 2.1.1 首先在电脑上有一个空白目录 2.1.2 打开SourceTree 2.1.3 ...

  7. cidr地址段

    CIDR采用各种长度的"网络前缀"来代替分类地址中的网络号和子网号,其格式为:IP地址 = {<网络前缀>,<主机号>}.为了区分网络前缀,通常采用&quo ...

  8. Spring Boot 自定义Filter

    一.Filter功能 它使用户可以改变一个 request和修改一个response. Filter 不是一个servlet,它不能产生一个response,它能够在一个request到达servle ...

  9. ubuntu18.40 rtx2080ti安装显卡驱动/cuda/cudnn/tensorflow-gpu

    电脑环境 ubuntu 18.40 gpu  rtx2080ti 一.安装显卡驱动 刚开始尝试用手动安装方式安装驱动 下载了驱动程序但是因为没有gcc所以放弃这种方法 后尝试最简单的方式 在 菜单-- ...

  10. Alpha冲刺(1/4)

    队名:天码行空 组长博客连接 作业博客连接 团队燃尽图(共享): GitHub当日代码/文档签入记录展示(共享): 组员情况: 组员1:卢欢(组长) 过去两天完成了哪些任务:SVN管理工具搭建 展示G ...