CI卷积链导致整数溢出

CIConvolution chain leads to integer overflow

我一直在使用 Core Image 的卷积过滤器进行一些工作,我注意到足够长的卷积链会导致意外输出,我怀疑这是基础整数、浮点数或半数上的数值溢出的结果float 类型用于保存像素数据。这是特别出乎意料的,因为文档说每个卷积的输出值都是 "clamped to the range between 0.0 and 1.0",所以更大的值不应该在过滤器的连续传递中累积,但这正是正在发生的事情。

我这里有一些示例代码可以演示这种令人惊讶的行为。您应该能够将它按原样粘贴到几乎任何 Xcode 项目中,在它的末尾设置一个断点,运行 它在适当的平台上(我使用的是 iPhone Xs,不是模拟器),然后当中断发生时使用 Quick Looks 检查过滤器链。

import CoreImage
import CoreImage.CIFilterBuiltins


// --------------------
// CREATE A WHITE IMAGE
// --------------------

// the desired size of the image
let size = CGSize(width: 300, height: 300)

// create a pixel buffer to use as input; every pixel is bgra(0,0,0,0) by default
var pixelBufferOut: CVPixelBuffer?
CVPixelBufferCreate(kCFAllocatorDefault, Int(size.width), Int(size.height), kCVPixelFormatType_32BGRA, nil, &pixelBufferOut)
let input = pixelBufferOut!

// create an image from the input
let image = CIImage(cvImageBuffer: input)

// create a color matrix filter that will turn every pixel white
// bgra(0,0,0,0) becomes bgra(1,1,1,1)
let matrixFilter = CIFilter.colorMatrix()
matrixFilter.biasVector = CIVector(string: "1 1 1 1")

// turn the image white
matrixFilter.inputImage = image
let whiteImage = matrixFilter.outputImage!

// the matrix filter sets the image's extent to infinity
// crop it back to original size so Quick Looks can display the image
let cropped = whiteImage.cropped(to: CGRect(origin: .zero, size: size))


// ------------------------------
// CONVOLVE THE IMAGE SEVEN TIMES
// ------------------------------

// create a 3x3 convolution filter with every weight set to 1
let convolutionFilter = CIFilter.convolution3X3()
convolutionFilter.weights = CIVector(string: "1 1 1 1 1 1 1 1 1")

// 1
convolutionFilter.inputImage = cropped
let convolved = convolutionFilter.outputImage!

// 2
convolutionFilter.inputImage = convolved
let convolvedx2 = convolutionFilter.outputImage!

// 3
convolutionFilter.inputImage = convolvedx2
let convolvedx3 = convolutionFilter.outputImage!

// 4
convolutionFilter.inputImage = convolvedx3
let convolvedx4 = convolutionFilter.outputImage!

// 5
convolutionFilter.inputImage = convolvedx4
let convolvedx5 = convolutionFilter.outputImage!

// 6
convolutionFilter.inputImage = convolvedx5
let convolvedx6 = convolutionFilter.outputImage!

// 7
convolutionFilter.inputImage = convolvedx6
let convolvedx7 = convolutionFilter.outputImage!

// <-- put a breakpoint here
// when you run the code you can hover over the variables
// to see what the image looks like at various stages through
// the filter chain; you will find that the image is still white
// up until the seventh convolution, at which point it turns black

进一步证明这是一个溢出问题的证据是,如果我使用 CIContext 将图像渲染到输出像素缓冲区,我有机会通过CIContextOption.workingFormat 选项。在我的平台上,默认值为 CIFormat.RGBAh,这意味着每个颜色通道都使用 16 位浮点数。相反,如果我使用 CIFormat.RGBAf,它使用完整的 32 位浮点数,这个问题就会消失,因为溢出 32 位比溢出 16 位需要更多的时间。

我对这里发生的事情的洞察是正确的还是我完全不对?关于钳位的文档是错误的还是过滤器的错误?

文档似乎已经过时了。也许它来自 Core Image 在 iOS 上默认使用 8 位无符号字节纹理格式的时间,因为它们被限制在 0.01.0 之间。

对于浮点型格式,值不再被限制,而是按照内核返回的方式存储。由于您从白色 (1.0) 开始并应用了 7 个具有非标准化权重的连续卷积(1 而不是 1/9),您最终得到每个通道的值 9^7 = 4,782,969,这超出了 16 位浮点数范围。

为了避免类似的情况,您应该规范化您的卷积权重,以便它们总和为 1.0

顺便说一句:要创建一定尺寸的白色图像,只需这样做:

let image = CIImage(color: .white).cropped(to: CGSize(width: 300, height: 300))