Java 實現高斯模糊和影象的空間卷積

Tony沈哲發表於2017-04-04

#高斯模糊

高斯模糊(英語:Gaussian Blur),也叫高斯平滑,是在Adobe Photoshop、GIMP以及Paint.NET等影象處理軟體中廣泛使用的處理效果,通常用它來減少影象雜訊以及降低細節層次。這種模糊技術生成的影象,其視覺效果就像是經過一個半透明螢幕在觀察影象,這與鏡頭焦外成像效果散景以及普通照明陰影中的效果都明顯不同。高斯平滑也用於計算機視覺演算法中的預先處理階段,以增強影象在不同比例大小下的影象效果。 從數學的角度來看,影象的高斯模糊過程就是影象與正態分佈做卷積。由於正態分佈又叫作高斯分佈,所以這項技術就叫作高斯模糊。影象與圓形方框模糊做卷積將會生成更加精確的焦外成像效果。由於高斯函式的傅立葉變換是另外一個高斯函式,所以高斯模糊對於影象來說就是一個低通濾波器。

高斯模糊運用了高斯的正態分佈的密度函式,計算影象中每個畫素的變換。

Java 實現高斯模糊和影象的空間卷積
gaussian-function.png

根據一維高斯函式,可以推導得到二維高斯函式:

Java 實現高斯模糊和影象的空間卷積
二維高斯函式.png

Java 實現高斯模糊和影象的空間卷積
二維的正太分佈.png

其中r是模糊半徑,r^2 = x^2 + y^2,σ是正態分佈的標準偏差。在二維空間中,這個公式生成的曲面的等高線是從中心開始呈正態分佈的同心圓。分佈不為零的畫素組成的卷積矩陣與原始影象做變換。每個畫素的值都是周圍相鄰畫素值的加權平均。原始畫素的值有最大的高斯分佈值,所以有最大的權重,相鄰畫素隨著距離原始畫素越來越遠,其權重也越來越小。這樣進行模糊處理比其它的均衡模糊濾波器更高地保留了邊緣效果。

其實,在iOS上實現高斯模糊是件很容易的事兒。早在iOS 5.0就有了Core Image的API,而且在CoreImage.framework庫中,提供了大量的濾鏡實現。

+(UIImage *)coreBlurImage:(UIImage *)image withBlurNumber:(CGFloat)blur 
{ 
    CIContext *context = [CIContext contextWithOptions:nil]; 
    CIImage *inputImage= [CIImage imageWithCGImage:image.CGImage]; 
    //設定filter
    CIFilter *filter = [CIFilter filterWithName:@"CIGaussianBlur"]; 
    [filter setValue:inputImage forKey:kCIInputImageKey]; [filter setValue:@(blur) forKey: @"inputRadius"]; 
    //模糊圖片
    CIImage *result=[filter valueForKey:kCIOutputImageKey]; 
    CGImageRef outImage=[context createCGImage:result fromRect:[result extent]];       
    UIImage *blurImage=[UIImage imageWithCGImage:outImage];           
    CGImageRelease(outImage); 
    return blurImage;
}複製程式碼

在Android上實現高斯模糊也可以使用原生的API-----RenderScript,不過需要Android的API是17以上,也就是Android 4.2版本。

    /**
     * 使用RenderScript實現高斯模糊的演算法
     * @param bitmap
     * @return
     */
    public Bitmap blur(Bitmap bitmap){
        //Let's create an empty bitmap with the same size of the bitmap we want to blur
        Bitmap outBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
        //Instantiate a new Renderscript
        RenderScript rs = RenderScript.create(getApplicationContext());
        //Create an Intrinsic Blur Script using the Renderscript
        ScriptIntrinsicBlur blurScript = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
        //Create the Allocations (in/out) with the Renderscript and the in/out bitmaps
        Allocation allIn = Allocation.createFromBitmap(rs, bitmap);
        Allocation allOut = Allocation.createFromBitmap(rs, outBitmap);
        //Set the radius of the blur: 0 < radius <= 25
        blurScript.setRadius(20.0f);
        //Perform the Renderscript
        blurScript.setInput(allIn);
        blurScript.forEach(allOut);
        //Copy the final bitmap created by the out Allocation to the outBitmap
        allOut.copyTo(outBitmap);
        //recycle the original bitmap
        bitmap.recycle();
        //After finishing everything, we destroy the Renderscript.
        rs.destroy();

        return outBitmap;

    }複製程式碼

我們開發的影象框架cv4j也提供了一個濾鏡來實現高斯模糊。

GaussianBlurFilter filter = new GaussianBlurFilter();
filter.setSigma(10);

RxImageData.bitmap(bitmap).addFilter(filter).into(image2);複製程式碼

Java 實現高斯模糊和影象的空間卷積
使用RenderScript實現高斯模糊.png

Java 實現高斯模糊和影象的空間卷積
使用cv4j實現高斯模糊.png

可以看出,cv4j實現的高斯模糊跟RenderScript實現的效果一致。

其中,GaussianBlurFilter的程式碼如下:

public class GaussianBlurFilter implements CommonFilter {
    private float[] kernel;
    private double sigma = 2;
    ExecutorService mExecutor;
    CompletionService<Void> service;

    public GaussianBlurFilter() {
        kernel = new float[0];
    }

    public void setSigma(double a) {
        this.sigma = a;
    }

    @Override
    public ImageProcessor filter(final ImageProcessor src){
        final int width = src.getWidth();
        final int height = src.getHeight();
        final int size = width*height;
        int dims = src.getChannels();
        makeGaussianKernel(sigma, 0.002, (int)Math.min(width, height));

        mExecutor = TaskUtils.newFixedThreadPool("cv4j",dims);
        service = new ExecutorCompletionService<>(mExecutor);

        // save result
        for(int i=0; i<dims; i++) {

            final int temp = i;
            service.submit(new Callable<Void>() {
                public Void call() throws Exception {
                    byte[] inPixels = src.toByte(temp);
                    byte[] temp = new byte[size];
                    blur(inPixels, temp, width, height); // H Gaussian
                    blur(temp, inPixels, height, width); // V Gaussain
                    return null;
                }
            });
        }

        for (int i = 0; i < dims; i++) {
            try {
                service.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        mExecutor.shutdown();
        return src;
    }

    /**
     * <p> here is 1D Gaussian        , </p>
     *
     * @param inPixels
     * @param outPixels
     * @param width
     * @param height
     */
    private void blur(byte[] inPixels, byte[] outPixels, int width, int height)
    {
        int subCol = 0;
        int index = 0, index2 = 0;
        float sum = 0;
        int k = kernel.length-1;
        for(int row=0; row<height; row++) {
            int c = 0;
            index = row;
            for(int col=0; col<width; col++) {
                sum = 0;
                for(int m = -k; m< kernel.length; m++) {
                    subCol = col + m;
                    if(subCol < 0 || subCol >= width) {
                        subCol = 0;
                    }
                    index2 = row * width + subCol;
                    c = inPixels[index2] & 0xff;
                    sum += c * kernel[Math.abs(m)];
                }
                outPixels[index] = (byte)Tools.clamp(sum);
                index += height;
            }
        }
    }

    public void makeGaussianKernel(final double sigma, final double accuracy, int maxRadius) {
        int kRadius = (int)Math.ceil(sigma*Math.sqrt(-2*Math.log(accuracy)))+1;
        if (maxRadius < 50) maxRadius = 50;         // too small maxRadius would result in inaccurate sum.
        if (kRadius > maxRadius) kRadius = maxRadius;
        kernel = new float[kRadius];
        for (int i=0; i<kRadius; i++)               // Gaussian function
            kernel[i] = (float)(Math.exp(-0.5*i*i/sigma/sigma));
        double sum;                                 // sum over all kernel elements for normalization
        if (kRadius < maxRadius) {
            sum = kernel[0];
            for (int i=1; i<kRadius; i++)
                sum += 2*kernel[i];
        } else
            sum = sigma * Math.sqrt(2*Math.PI);

        for (int i=0; i<kRadius; i++) {
            double v = (kernel[i]/sum);
            kernel[i] = (float)v;
        }
        return;
    }
}複製程式碼

#空間卷積
二維卷積在影象處理中會經常遇到,影象處理中用到的大多是二維卷積的離散形式。

Java 實現高斯模糊和影象的空間卷積
二維卷積的離散形式.png

以下是cv4j實現的各種卷積效果。

Java 實現高斯模糊和影象的空間卷積
各種卷積效果1.png

Java 實現高斯模糊和影象的空間卷積
各種卷積效果2.png

cv4j 目前支援如下的空間卷積濾鏡

filter 名稱 作用
ConvolutionHVFilter 卷積 模糊或者降噪
MinMaxFilter 最大最小值濾波 去噪聲
SAPNoiseFilter 椒鹽噪聲 增加噪聲
SharpFilter 銳化 增強
MedimaFilter 中值濾波 去噪聲
LaplasFilter 拉普拉斯 提取邊緣
FindEdgeFilter 尋找邊緣 梯度提取
SobelFilter 梯度 獲取x、y方向的梯度提取
VarianceFilter 方差濾波 高通濾波
MaerOperatorFilter 馬爾操作 高通濾波
USMFilter USM 增強

#總結
cv4jgloomyfish和我一起開發的影象處理庫,目前還處於早期的版本。

目前已經實現的功能:

Java 實現高斯模糊和影象的空間卷積
cv4j.png

這周,我們對 cv4j 做了較大的調整,對整體架構進行了優化。還加上了空間卷積功能(圖片增強、銳化、模糊等等)。接下來,我們會做二值影象的分析(腐蝕、膨脹、開閉操作、輪廓提取等等)

相關文章