溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

OpenCV基于傅里葉變換進(jìn)行文本的旋轉(zhuǎn)校正

發(fā)布時間:2020-04-02 09:51:58 來源:網(wǎng)絡(luò) 閱讀:15294 作者:BoyTNT 欄目:編程語言

本文描述一種利用OpenCV及傅里葉變換識別圖片中文本旋轉(zhuǎn)角度并自動校正的方法,由于對C#比較熟,因此本文將使用OpenCVSharp。 文章參考了http://johnhany.net/2013/11/dft-based-text-rotation-correction,對原作者表示感謝。我基于OpenCVSharp用C#進(jìn)行了重寫,希望能幫到同樣用OpenCVSharp的同學(xué)。


================= 正文開始 =================


手里有一張圖片如下,是經(jīng)過旋轉(zhuǎn)的,如何通過程序自動對它進(jìn)行旋轉(zhuǎn)校正? (旋轉(zhuǎn)校正是行分割、字符識別等后續(xù)工作的基礎(chǔ))

OpenCV基于傅里葉變換進(jìn)行文本的旋轉(zhuǎn)校正


傅里葉變換可以用于將圖像從時域轉(zhuǎn)換到頻域,對于分行的文本,其頻率譜上一定會有一定的特征,當(dāng)圖像旋轉(zhuǎn)時,其頻譜也會同步旋轉(zhuǎn),因此找出這個特征的傾角,就可以將圖像旋轉(zhuǎn)校正回去。


先來對原始圖像進(jìn)行一下傅里葉變換,需要這么幾步:


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

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


2、將圖像擴(kuò)展到合適的尺寸以方便快速變換

  OpenCV中的DFT對圖像尺寸有一定要求,需要用GetOptimalDFTSize方法來找到合適的大小,根據(jù)這個大小建立新的圖像,把原圖像拷貝過去,多出來的部分直接填充0。

int width = Cv.GetOptimalDFTSize(src.Width);
int height = Cv.GetOptimalDFTSize(src.Height);
var padded = new IplImage(width, height, BitDepth.U8, 1);//擴(kuò)展后的圖像,單通道
Cv.CopyMakeBorder(src, padded, new CvPoint(0, 0), BorderType.Constant, CvScalar.ScalarAll(0));


3、進(jìn)行DFT運(yùn)算

  DFT要分別計算實(shí)部和虛部,這里準(zhǔn)備2個單通道的圖像,實(shí)部從原圖像中拷貝數(shù)據(jù),虛部清零,然后把它們Merge為一個雙通道圖像再進(jìn)行DFT計算,完成后再Split開。

//實(shí)部、虛部(單通道)
var real = new IplImage(padded.Size, BitDepth.F32, 1);
var imaginary = new IplImage(padded.Size, BitDepth.F32, 1);
//合成(雙通道)
var fourier = new IplImage(padded.Size, BitDepth.F32, 2);

//圖像復(fù)制到實(shí)部,虛部清零
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、對數(shù)據(jù)進(jìn)行適當(dāng)調(diào)整

  上一步中得到的實(shí)部保留下來作為變換結(jié)果,并計算幅度:magnitude = sqrt(real^2 + imaginary^2)。

  考慮到幅度變化范圍很大,還要用log函數(shù)把數(shù)值范圍縮小。

  最后經(jīng)過歸一化,就會得到圖像的特征譜了。

//計算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(1), real);
Cv.Log(real, real);

//歸一化
Cv.Normalize(real, real, 0, 1, NormType.MinMax);


此時圖像是這樣的:

OpenCV基于傅里葉變換進(jìn)行文本的旋轉(zhuǎn)校正


5、移動中心

  DFT操作的結(jié)果低頻部分位于四角,高頻部分在中心,習(xí)慣上會把頻域原點(diǎn)調(diào)整到中心去,也就是把低頻部分移動到中心。

/// <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 / 2;
    int cx = col / 2;
    
    var q0 = p_w_picpath.Clone(new CvRect(0, 0, cx, cy));   //左上
    var q1 = p_w_picpath.Clone(new CvRect(0, cy, cx, cy));  //左下
    var q2 = p_w_picpath.Clone(new CvRect(cx, cy, cx, cy)); //右下
    var q3 = p_w_picpath.Clone(new CvRect(cx, 0, cx, cy));  //右上
    
    Cv.SetImageROI(p_w_picpath, new CvRect(0, 0, cx, cy));
    q2.Copy(p_w_picpath);
    Cv.ResetImageROI(p_w_picpath);
    
    Cv.SetImageROI(p_w_picpath, new CvRect(0, 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, 0, cx, cy));
    q1.Copy(p_w_picpath);
    Cv.ResetImageROI(p_w_picpath);
}

最終得到圖像如下:

OpenCV基于傅里葉變換進(jìn)行文本的旋轉(zhuǎn)校正


可以明顯的看到過中心有一條傾斜的直線,可以用霍夫變換把它檢測出來,然后計算角度。 需要以下幾步:


1、二值化

  把剛才得到的傅里葉譜放到0-255的范圍,然后進(jìn)行二值化,此處以150作為分界點(diǎn)。

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

 得到圖像如下:

OpenCV基于傅里葉變換進(jìn)行文本的旋轉(zhuǎn)校正


2、Houge直線檢測

  由于HoughLine2方法只接受8UC1格式的圖片,因此要先進(jìn)行轉(zhuǎn)換再調(diào)用HoughLine2方法,這里的threshold參數(shù)取的100,能夠檢測出3條直線來。

//構(gòu)造8UC1格式圖像
var gray = new IplImage(real.Size, BitDepth.U8, 1);
Cv.ConvertScale(real, gray);

//找直線
var storage = Cv.CreateMemStorage();
var lines = Cv.HoughLines2(gray, storage, HoughLinesMethod.Standard, 1, Cv.PI / 180, 100);


3、找到符合條件的那條斜線,獲取角度

float angel = 0f;
float piThresh = (float)Cv.PI / 90;
float pi2 = (float)Cv.PI / 2;
for (int i = 0; i < lines.Total; ++i)
{
    //極坐標(biāo)下的點(diǎn),X是極徑,Y是夾角,我們只關(guān)心夾角
    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、角度轉(zhuǎn)換

  由于DFT的特點(diǎn),只有輸入圖像是正方形時,檢測到的角度才是真正文本的旋轉(zhuǎn)角度,但原圖像明顯不是,因此還要根據(jù)長寬比進(jìn)行變換,最后得到的angelD就是真正的旋轉(zhuǎn)角度了。

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


5、旋轉(zhuǎn)校正

   這一步比較簡單了,構(gòu)建一個仿射變換矩陣,然后調(diào)用WarpAffine進(jìn)行變換,就得到校正后的圖像了。最后顯示到界面上。

var center = new CvPoint2D32f(src.Width / 2.0, src.Height / 2.0);//圖像中心
var rotMat = Cv.GetRotationMatrix2D(center, angelD, 1.0);//構(gòu)造仿射變換矩陣
var dst = new IplImage(src.Size, BitDepth.U8, 1);

//執(zhí)行變換,產(chǎn)生的空白部分用255填充,即純白
Cv.WarpAffine(src, dst, rotMat, Interpolation.Cubic | Interpolation.FillOutliers, CvScalar.ScalarAll(255));

//展示
using (var win = new CvWindow("Rotation"))
{
    win.Image = dst;
    Cv.WaitKey();
}


最終結(jié)果如下,效果還不錯:

OpenCV基于傅里葉變換進(jìn)行文本的旋轉(zhuǎn)校正


最后放完整代碼:

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);

            //轉(zhuǎn)換到合適的大小,以適應(yīng)快速變換
            int width = Cv.GetOptimalDFTSize(src.Width);
            int height = Cv.GetOptimalDFTSize(src.Height);
            var padded = new IplImage(width, height, BitDepth.U8, 1);
            Cv.CopyMakeBorder(src, padded, new CvPoint(0, 0), BorderType.Constant, CvScalar.ScalarAll(0));
            
            //實(shí)部、虛部(單通道)
            var real = new IplImage(padded.Size, BitDepth.F32, 1);
            var imaginary = new IplImage(padded.Size, BitDepth.F32, 1);
            //合并(雙通道)
            var fourier = new IplImage(padded.Size, BitDepth.F32, 2);
            
            //圖像復(fù)制到實(shí)部,虛部清零
            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(1), real);
            Cv.Log(real, real);
            
            //歸一化,落入0-255范圍
            Cv.Normalize(real, real, 0, 255, NormType.MinMax);
            
            //把低頻移動到中心
            ShiftDFT(real);
            
            //二值化,以150作為分界點(diǎn),經(jīng)驗(yàn)值,需要根據(jù)實(shí)際情況調(diào)整
            Cv.Threshold(real, real, 150, 255, ThresholdType.Binary);
            
            //由于HoughLines2方法只接受8UC1格式的圖片,因此進(jìn)行轉(zhuǎn)換
            var gray = new IplImage(real.Size, BitDepth.U8, 1);
            Cv.ConvertScale(real, gray);
            
            //找直線,threshold參數(shù)取100,經(jīng)驗(yàn)值,需要根據(jù)實(shí)際情況調(diào)整
            var storage = Cv.CreateMemStorage();
            var lines = Cv.HoughLines2(gray, storage, HoughLinesMethod.Standard, 1, Cv.PI / 180, 100);
            
            //找到符合條件的那條斜線
            float angel = 0f;
            float piThresh = (float)Cv.PI / 90;
            float pi2 = (float)Cv.PI / 2;
            for (int i = 0; i < lines.Total; ++i)
            {
                //極坐標(biāo)下的點(diǎn),X是極徑,Y是夾角,我們只關(guān)心夾角
                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);
            
            //轉(zhuǎn)換角度
            if (angel != pi2)
            {
                float angelT = (float)(src.Height * Math.Tan(angel) / src.Width);
                angel = (float)Math.Atan(angelT);
            }
            float angelD = angel * 180 / (float)Cv.PI;
            Console.WriteLine("angtlD = {0}", angelD);

            //旋轉(zhuǎn)
            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, 1);
            Cv.WarpAffine(src, dst, rotMat, Interpolation.Cubic | Interpolation.FillOutliers, CvScalar.ScalarAll(255));
            
            //顯示
            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 / 2;
            int cx = col / 2;
            
            var q0 = p_w_picpath.Clone(new CvRect(0, 0, cx, cy));//左上
            var q1 = p_w_picpath.Clone(new CvRect(0, cy, cx, cy));//左下
            var q2 = p_w_picpath.Clone(new CvRect(cx, cy, cx, cy));//右下
            var q3 = p_w_picpath.Clone(new CvRect(cx, 0, cx, cy));//右上
            
            Cv.SetImageROI(p_w_picpath, new CvRect(0, 0, cx, cy));
            q2.Copy(p_w_picpath);
            Cv.ResetImageROI(p_w_picpath);
            
            Cv.SetImageROI(p_w_picpath, new CvRect(0, 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, 0, cx, cy));
            q1.Copy(p_w_picpath);
            Cv.ResetImageROI(p_w_picpath);
        }
    }
}



最后吐槽一下51cto的編譯器,總是把代碼的換行和縮進(jìn)弄沒,還要手工再處理一遍,真是受夠了,難道是我打開的方式不對?


PS:最近增加了源碼,因?yàn)榧恿薿pencv的dll,比較大,下載鏈接

http://down.51cto.com/data/2329576



向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI