在Swift中进行高斯图像金字塔的下采样和上采样

11

介绍

我对编写一个函数感兴趣,该函数可以输出高斯金字塔的下一级(最终我想要创建拉普拉斯金字塔),用于图像处理。 (参考链接:https://en.wikipedia.org/wiki/Pyramid_(image_processing)#Gaussian_pyramid

降采样问题

现在,简化的部分是,当您缩小/放大图片时,在调整大小之前会将5点滤波器与图像进行卷积。

然而,制作图像金字塔的有趣部分在于,您必须按照0.5或2的因子将图像进行降采样和上采样,具体取决于您所采取的方向。 Swift有一些方法可以做到这一点,例如使用CIAffineTransform和CILanczosTransform,但我想知道是否有更加朴素的方法来做到这一点,因为我不关心调整大小后的图像质量。 在本帖中,我将使用Lenna(512x512)作为示例,如下所示:

著名的Lenna

如果我们想将图像按2的因子缩小,我们将采用所有奇数像素数据来形成一个新图像。 在MATLAB中,这是在高斯模糊之后执行的,如下所示:

如果I是您的输入图像,大小为NxM,并且存储了P的3个颜色映射(512x512x3矩阵),则通过0.5比例缩小的降采样图像为

R = I(1:2:end,1:2:end,:)

新图像就是前一个图像的奇数列和行。 此操作将生成256x256照片,即高斯金字塔的第一级。

下采样的Lenna

在 Swift 中是否有这样的方法?可以使用 Core Image 还是 OpenGL 自定义滤镜来实现?

上采样问题:

上采样通常仅用于创建拉普拉斯金字塔。然而,朴素的想法是执行以下操作:

初始化一个空图像上下文 R,其大小与要上采样到的大小相同。在本例中,我们将对上面显示的下采样 Lenna 照片进行上采样,因此 R 必须是一个 512x512 的空白图像。

接下来,将下采样图像 I 的像素值乘以 4。可以通过将图像与 3x3 矩阵 [0,0,0;0,4,0;0,0,0] 卷积来完成。然后可以将图像的像素均匀地分布到较大的空白图像 R 中。这看起来像这样:

输入图像描述

最后,可以对此图像采用相同的 5 点高斯模糊来恢复上采样图像:

输入图像描述

我想知道在 Swift 中是否可以使用类似的上采样方法。

另一件我不确定的事情是,调整图像大小以进行高斯/拉普拉斯滤波技术是否真的很重要。如果不是,则肯定可以使用内置的最快方法而不是尝试自己制作。


你看过这个吗:https://developer.apple.com/reference/metalperformanceshaders/mpsimagegaussianpyramid - Flex Monkey
我已经尝试过,但是在使用自定义过滤器获取所需内容之前,我真的希望能找到一种替代方法。在图像处理中,这些问题并不罕见,因此我认为可以通过苹果的内置方式来解决。 - DaveNine
我不确定它是否具备你所需的所有功能,但你可以尝试使用加速框架(Accelerate Framework) https://developer.apple.com/videos/play/wwdc2013/713/ - juanjo
2个回答

2

GPUImage处理库可以提供一些上采样功能,并可能导致您的拉普拉斯金字塔

pod 'GPUImage'

锐化上采样:

UIImage *inputImage = [UIImage imageNamed:@"cutelady"];
GPUImagePicture *stillImageSource = [[GPUImagePicture alloc]initWithImage:inputImage];
GPUImageSharpenFilter *stillImageFilter = [[GPUImageSharpenFilter alloc] init];
[stillImageSource addTarget:stillImageFilter];
[stillImageFilter useNextFrameForImageCapture];
[stillImageSource processImage];
UIImage *currentFilteredVideoFrame = [stillImageFilter imageFromCurrentFramebuffer];

LANCZOS上采样:

UIImage *inputImage = [UIImage imageNamed:@"cutelady"];
GPUImagePicture *stillImageSource = [[GPUImagePicture alloc] initWithImage:inputImage];
GPUImageLanczosResamplingFilter *stillImageFilter = [[GPUImageLanczosResamplingFilter alloc] init];
[stillImageSource addTarget:stillImageFilter];
[stillImageFilter useNextFrameForImageCapture];
[stillImageSource processImage];
[stillImageSource forceProcessingAtSizeRespectingAspectRatio:CGSizeMake(200, 200)];
UIImage *currentFilteredVideoFrame = [stillImageFilter imageFromCurrentFramebuffer];
cell.imageView.image = currentFilteredVideoFrame;

需要注意的是,您正在使用Lanczos变换——一种特定的重采样方法——来重新调整图像的大小。如果您想尽可能保留更多细节,这种变换是理想的选择,但这并不是我要问的问题——事实上,为了创建图像金字塔,您几乎不需要关心保留细节。 - DaveNine

1
我已经取得了一些进展,可以基本上将其视为对我的问题的回答,虽然有些地方有点不同,而且我认为这种方法并不是很快。我希望能听到任何人的意见,看看如何使这段代码更快。在下面的代码中,似乎调整图像大小占用了大部分时间,我得到了大量的 ovveride outputImage 部分的调用,但我不知道为什么会这样。不幸的是,当我运行下面的拉普拉斯金字塔函数时,它需要大约5秒钟才能完成一个275x300的照片。这太糟糕了,我有点不知道如何加速。我怀疑重采样滤波器是罪魁祸首。然而,我不熟练,不知道如何使它更快。
首先,是自定义过滤器:
第一个通过简单的重新缩放来调整图像大小。在这种情况下,我认为这是最好的调整大小技术,因为在调整大小时只复制像素。例如,如果我们有以下像素块并执行2.0缩放,则映射如下所示:
public class ResampleFilter: CIFilter
{
    var inputImage : CIImage?
    var inputScaleX: CGFloat = 1
    var inputScaleY: CGFloat = 1
    let warpKernel = CIWarpKernel(string:
        "kernel vec2 resample(float inputScaleX, float inputScaleY)" +
            "   {                                                      " +
            "       float y = (destCoord().y / inputScaleY);           " +
            "       float x = (destCoord().x / inputScaleX);           " +
            "       return vec2(x,y);                                  " +
            "   }                                                      "
    )

    override public var outputImage: CIImage!
    {
        if let inputImage = inputImage,
            kernel = warpKernel
        {
            let arguments = [inputScaleX, inputScaleY]

            let extent = CGRect(origin: inputImage.extent.origin,
                                size: CGSize(width: inputImage.extent.width*inputScaleX,
                                    height: inputImage.extent.height*inputScaleY))

            return kernel.applyWithExtent(extent,
                                          roiCallback:
                {
                    (index,rect) in
                    let sampleX = rect.origin.x/self.inputScaleX
                    let sampleY = rect.origin.y/self.inputScaleY
                    let sampleWidth = rect.width/self.inputScaleX
                    let sampleHeight = rect.height/self.inputScaleY

                    let sampleRect = CGRect(x: sampleX, y: sampleY, width: sampleWidth, height: sampleHeight)

                    return sampleRect
                },
                                          inputImage : inputImage,
                                          arguments : arguments)

        }
        return nil
    }
}

这是一个简单的差异混合。
public class DifferenceOfImages: CIFilter
{
    var inputImage1 : CIImage?  //Initializes input
    var inputImage2 : CIImage?
    var kernel = CIKernel(string:  //The actual custom kernel code
        "kernel vec4 Difference(__sample image1,__sample image2)" +
            "       {                                               " +
            "           float colorR = image1.r - image2.r;         " +
            "           float colorG = image1.g - image2.g;         " +
            "           float colorB = image1.b - image2.b;         " +
            "           return vec4(colorR,colorG,colorB,1);        " +
        "       }                                               "
    )
    var extentFunction: (CGRect, CGRect) -> CGRect =
        { (a: CGRect, b: CGRect) in return CGRectZero }


    override public var outputImage: CIImage!
    {
        guard let inputImage1 = inputImage1,
            inputImage2 = inputImage2,
            kernel = kernel
            else
        {
            return nil
        }

        //apply to whole image
        let extent = extentFunction(inputImage1.extent,inputImage2.extent)
        //arguments of the kernel
        let arguments = [inputImage1,inputImage2]
        //return the rectangle that defines the part of the image that CI needs to render rect in the output
        return kernel.applyWithExtent(extent,
                                      roiCallback:
            { (index, rect) in
                return rect

            },
                                      arguments: arguments)

    }

}

现在是一些函数定义:
这个函数只是按照Burt和Adelson论文中描述的相同的5点滤波器对图像进行高斯模糊。不确定如何消除似乎多余的尴尬边框像素。
public func GaussianFilter(ciImage: CIImage) -> CIImage
{

    //5x5 convolution to image
    let kernelValues: [CGFloat] = [
        0.0025, 0.0125, 0.0200, 0.0125, 0.0025,
        0.0125, 0.0625, 0.1000, 0.0625, 0.0125,
        0.0200, 0.1000, 0.1600, 0.1000, 0.0200,
        0.0125, 0.0625, 0.1000, 0.0625, 0.0125,
        0.0025, 0.0125, 0.0200, 0.0125, 0.0025 ]

    let weightMatrix = CIVector(values: kernelValues,
                                count: kernelValues.count)

    let filter = CIFilter(name: "CIConvolution5X5",
                          withInputParameters: [
                            kCIInputImageKey: ciImage,
                            kCIInputWeightsKey: weightMatrix])!

    let final = filter.outputImage!

    let rect = CGRect(x: 0, y: 0, width: ciImage.extent.size.width, height: ciImage.extent.size.height)

    return final.imageByCroppingToRect(rect)

}

这个函数只是简化了resample的使用。您可以指定新图像的目标大小。在我看来,这比设置比例参数更容易处理。

public func resampleImage(inputImage: CIImage, sizeX: CGFloat, sizeY: CGFloat) -> CIImage
{
    let inputWidth : CGFloat = inputImage.extent.size.width
    let inputHeight : CGFloat = inputImage.extent.size.height

    let scaleX = sizeX/inputWidth
    let scaleY = sizeY/inputHeight

    let resamplefilter = ResampleFilter()
    resamplefilter.inputImage = inputImage
    resamplefilter.inputScaleX = scaleX
    resamplefilter.inputScaleY = scaleY
    return resamplefilter.outputImage
}

这个函数只是简化了差分滤波器的使用。请注意它是imageOne - ImageTwo
public func Difference(imageOne:CIImage,imageTwo:CIImage) -> CIImage
{
    let generalFilter = DifferenceOfImages()

    generalFilter.inputImage1 = imageOne
    generalFilter.inputImage2 = imageTwo

    generalFilter.extentFunction = { (fore, back) in return back.union(fore)}
    return generalFilter.outputImage

}

这个函数计算每个金字塔的级别尺寸,并将它们存储在一个数组中。以后会有用。

public func LevelDimensions(image: CIImage,levels:Int) -> [[CGFloat]]
{
    let inputWidth : CGFloat = image.extent.width
    let inputHeight : CGFloat = image.extent.height

    var levelSizes : [[CGFloat]] = [[inputWidth,inputHeight]]
    for j in 1...(levels-1)
    {
        let temp = [floor(inputWidth/pow(2.0,CGFloat(j))),floor(inputHeight/pow(2,CGFloat(j)))]
        levelSizes.append(temp)
    }
    return levelSizes
}

现在进入正题:这个函数将创建一个给定层数的高斯金字塔。
public func GaussianPyramid(image: CIImage,levels:Int) -> [CIImage]
{
    let PyrLevel = LevelDimensions(image, levels: levels)

    var GauPyr : [CIImage] = [image]
    var I : CIImage
    var J : CIImage

    for j in 1 ... levels-1
    {
        J = GaussianFilter(GauPyr[j-1])
        I = resampleImage(J, sizeX: PyrLevel[j][0], sizeY: PyrLevel[j][1])
        GauPyr.append(I)

    }
    return GauPyr
}

最后,此函数使用给定的级别数创建拉普拉斯金字塔。请注意,在两个金字塔函数中,每个级别都存储在数组中。

public func LaplacianPyramid(image:CIImage,levels:Int) -> [CIImage]
{
    let PyrLevel = LevelDimensions(image, levels:levels)

    var LapPyr : [CIImage] = []
    var I : CIImage
    var J : CIImage

    J = image
    for j in 0 ... levels-2
    {
        let blur = GaussianFilter(J)
        I = resampleImage(blur, sizeX: PyrLevel[j+1][0], sizeY: PyrLevel[j+1][1])
        let diff = Difference(J,imageTwo: resampleImage(I, sizeX: PyrLevel[j][0], sizeY: PyrLevel[j][1]))
        LapPyr.append(diff)
        J = I

    }
    LapPyr.append(J)
    return LapPyr
}

我还没有做的事情就是编写一个函数,从图像金字塔中获取新图像。稍后会完成并进行编辑。 - DaveNine
只是想提一下,人们需要一种保存浮点数中的负号的方法,因为你对它们进行的任何计算都必须保留。这尤其适用于 Laplacian Level。 - DaveNine

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接