【C#】用 EmguCV 改寫 imadjust 函數

  • 756
  • 0
  • 2018-01-15

EmguCV 似乎沒有提供現成的 imadjust 函數,使用者得自行設計。
但 Google 搜尋得到的版本駁雜,效能差異也很大,無奈打消複製貼上的想法。
這裡就稍微整理一下,然後寫一個適合自己的函數吧。

一開始先來設計個簡單的畫面吧。

主視窗的程式碼大概長這樣。
在這邊影像載入格式是用 Image<Gray, byte>,而不是另外一種常見的 Mat 格式。
因為 Image 內部的方法比較多,可以省去許多麻煩事。
另外使用灰階而不是全彩格式,是因為目前夏恩都在做灰階影像處理,若有全彩影像處理的需要就自己改吧。

using System;
using System.Linq;
using System.Drawing;
using System.Diagnostics;
using System.Windows.Forms;
using System.Drawing.Imaging;
using Emgu.CV;
using Emgu.CV.Structure;

namespace TestWindowsForms
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {          
        }

        private void button1_Click(object sender, EventArgs e)
        {
            Image<Gray, byte> ImageBefore 
               = new Image<Gray, byte>(@"D:\Test\1.jpg");

            Image<Gray, byte> ImageAfter 
               = new Image<Gray, byte>(ImageBefore.Width, ImageBefore.Height);

            Stopwatch sw = new Stopwatch();
            sw.Reset();

            //計時開始
            sw = Stopwatch.StartNew();
            //使用 Stretchlim 自動尋找影像的上下界
            double[] LH = MyCV.Stretchlim(ImageBefore);  
            //調整對比至 [0 , 1] 、參數 gamma = 1
            bool status 
              = MyCV.Imadjust(ImageBefore, ImageAfter, LH[0], LH[1], 0, 1, 1);
            
            //計時結束
            sw.Stop();
            
            if (status)
            {
                pictureBox1.Image = ImageBefore.Bitmap;
                pictureBox2.Image = ImageAfter.Bitmap;
            }
                
            TimeSpan el = sw.Elapsed;
            label1.Text = el.TotalSeconds.ToString();
        }
    }
}

再來是自行設計的類別。
這個類別又分為三個函數:Imhist,找影像灰階值分布;Stretchlim,找影像上下界;Imadjust,對比調整。
有很多人討論過各種影像處理的技巧,這邊夏恩直接挑速度最快的來用。

public class MyCV
{
    public static int[] Imhist(Image<Gray, byte> src)
    {
        int[] Hist = new int[256];
        int Width = src.Width;
        int Height = src.Height;

        Rectangle rect = new Rectangle(0, 0, Width, Height);
        Bitmap srcBmp = src.Bitmap;
        BitmapData BmpdataIn = srcBmp.LockBits(rect, 
                                               ImageLockMode.ReadWrite, 
                                               PixelFormat.Format8bppIndexed);
        IntPtr ScanIn = BmpdataIn.Scan0;
        unsafe
        {
            byte* PtrIn = (byte*)(void*)ScanIn;
            int OffsetIn = BmpdataIn.Stride - Width;
            for (int row = 0; row < Height; row++)
            {
                for (int col = 0; col < Width; col++, PtrIn++)
                    ++Hist[*PtrIn];

                PtrIn += OffsetIn;
            }
        }
        srcBmp.UnlockBits(BmpdataIn);
        return Hist;
    }

    public static double[] Stretchlim(Image<Gray, byte> src)
    {
        int Width = src.Width;
        int Height = src.Height;

        double[] LowHigh = { 0, 0 };

        int[] Hist = Imhist(src);
        int Total = Hist.Sum();

        for (int i = 0; i < 256; i++)
        {
            double Sum = 0;
            for (int j = 0; j < i; j++)
                Sum += Hist[j];

            if ((Sum / Total) > 0.01)
            {
                LowHigh[0] = (double)i / 256;
                break;
            }
        }

        for (int i = 0; i < 256; i++)
        {
            double Sum = 0;
            for (int j = 0; j < i; j++)
                Sum += Hist[j];

            if ((Sum / Total) >= 0.99)
            {
                LowHigh[1] = (double)i / 256;
                break;
            }
        }

        if (LowHigh[0] == LowHigh[1])
        {
            LowHigh[0] = 0;
            LowHigh[1] = 1;
        }
        return LowHigh;
    }

    public static bool Imadjust(Image<Gray, byte> src, 
                                Image<Gray, byte> dst,
                                double Lowin, double Highin,
                                double Lowout, double Highout, double gamma)
    {
        if (Lowin < 0 || Lowin > 1 || Highin < 0 || Highout > 1 || 
            Lowout < 0 || Lowout > 1 || Highout < 0 || Highout > 1 || 
            Highin < Lowin || Highout < Lowout)
            return false;

        double LI = Lowin * 255;
        double HI = Highin * 255;
        double LO = Lowout * 255;
        double HO = Highout * 255;
        double dIn = HI - LI;
        double dOut = HO - LO;

        int Width = src.Width;
        int Height = src.Height;
        Rectangle rect = new Rectangle(0, 0, Width, Height);

        Bitmap srcBmp = src.Bitmap;
        Bitmap dstBmp = dst.Bitmap;

        BitmapData BmpdataIn = srcBmp.LockBits(rect, 
                                               ImageLockMode.ReadWrite, 
                                               PixelFormat.Format8bppIndexed);

        BitmapData BmpdataOut = dstBmp.LockBits(rect, 
                                               ImageLockMode.ReadWrite, 
                                               PixelFormat.Format8bppIndexed);

        IntPtr ScanIn = BmpdataIn.Scan0;
        IntPtr ScanOut = BmpdataOut.Scan0;

        unsafe
        {
            byte* PtrIn = (byte*)(void*)ScanIn;
            byte* PtrOut = (byte*)(void*)ScanOut;

            int OffsetIn = BmpdataIn.Stride - Width;
            int OffsetOut = BmpdataOut.Stride - Width;

            double value = 0;
            for (int row = 0; row < Height; row++)
            {
                for (int col = 0; col < Width; col++, PtrIn++, PtrOut++)
                {
                    val = Math.Pow((*PtrIn - LI) / dIn, gamma) * dOut + LO;
                    if (val > 255)
                        val = 255;
                    else if (val < 0)
                        val = 0;

                    *PtrOut = (byte)val;
                }
                PtrIn += OffsetIn;
                PtrOut += OffsetOut;
            }
        }

        srcBmp.UnlockBits(BmpdataIn);
        dstBmp.UnlockBits(BmpdataOut);
        dst.Bitmap = dstBmp;
        return true;
    }
}

程式寫好之後來實測一下,執行時間的單位是秒。
這張測試照片是從維基百科抓回來的,大小是 450*300,附上連結


以上的 Imadjust 是傳入 Image 影像格式後,拿出內含的 Bitmap 來做影像處理。
有另外一種直接使用 Image 的寫法夏恩也有試過,請看以下。
為了方便稱呼,下面這個稱為方法2,一開始寫的那個稱為方法1。

public static bool Imadjust(Image<Gray, byte> src, 
                            Image<Gray, byte> dst,
                            double Lowin, double Highin,
                            double Lowout, double Highout, double gamma)
{
    if (Lowin < 0 || Lowin > 1 || Highin < 0 || Highout > 1 || Highin < Lowin ||
        Lowout < 0 || Lowout > 1 || Highout < 0 || Highout > 1 || Highout < Lowout)
        return false;
            
    double LI = Lowin * 255;
    double HI = Highin * 255;
    double LO = Lowout * 255;
    double HO = Highout * 255;
    double dIn = HI - LI;
    double dOut = HO - LO;

    MIplImage srcMIplImage 
      = (MIplImage)System.Runtime.InteropServices.Marshal.PtrToStructure(src.Ptr, typeof(MIplImage));
    MIplImage dstMIplImage 
      = (MIplImage)System.Runtime.InteropServices.Marshal.PtrToStructure(dst.Ptr, typeof(MIplImage));

    unsafe
    {
        byte* srcpixel = (byte*)srcMIplImage.ImageData;
        byte* dstpixel = (byte*)dstMIplImage.ImageData;

        for (int row = 0; row < src.Height; row++)
        {
            double value = 0;
            for (int col = 0; col < src.Width; col++)
            {
                value = Math.Pow((*srcpixel - LI) / dIn, gamma) * dOut + LO;

                if (value > 255)
                    value = 255;
                else if (value < 0)
                    value = 0;

                *dstpixel = (byte)value;

                srcpixel++; dstpixel++;
            }
        }
    }
    return true;
}

兩種寫法哪一個比較好,看執行時間就知道。
這次夏恩挑一張比較大的影像來測試,影像大小是10929*5553,這裡附上載點

方法1:

方法2:

方法1和方法2的效率差了4倍多。
那只好選用方法1啦!

後記:
為了實現 imadjust 這個函數,找資料又測程式,真的很花時間!
在 MATLAB 裡面可是一句話的事情而已,反觀自己做的函數...
大概跟 MATLAB 只有87%像而已,還在再努力阿...

參考資料:
1. [C#]用指標存取影像的方法
2. C#影像處理入門(-bitmap類和圖像像素值獲取方法)​
3. C#影像處理的極限​
4. matlab imadjust 用 opencv改写
5. 图像处理学习笔记之MATLAB中imhist、imadjust、stretchlim函数实现