iOS加速框架vImage - 提升性能?

6

我一直在使用OpenCV和苹果的Accelerate框架,发现Accelerate的性能较慢,而且苹果的文档有限。以以下内容为例:

void equalizeHistogram(const cv::Mat &planar8Image, cv::Mat &equalizedImage)
{
    cv::Size size = planar8Image.size();
    vImage_Buffer planarImageBuffer = {
        .width = static_cast<vImagePixelCount>(size.width),
        .height = static_cast<vImagePixelCount>(size.height),
        .rowBytes = planar8Image.step,
        .data = planar8Image.data
    };

    vImage_Buffer equalizedImageBuffer = {
        .width = static_cast<vImagePixelCount>(size.width),
        .height = static_cast<vImagePixelCount>(size.height),
        .rowBytes = equalizedImage.step,
        .data = equalizedImage.data
    };

    TIME_START(VIMAGE_EQUALIZE_HISTOGRAM);
    vImage_Error error = vImageEqualization_Planar8(&planarImageBuffer, &equalizedImageBuffer, kvImageNoFlags);
    TIME_END(VIMAGE_EQUALIZE_HISTOGRAM);
    if (error != kvImageNoError) {
        NSLog(@"%s, vImage error %zd", __PRETTY_FUNCTION__, error);
    }
}

这个调用大约需要20毫秒。这在我的应用中是无法使用的,也许直方图均衡化本质上就很慢,但我还测试了BGRA->灰度和发现OpenCV可以在约5毫秒内完成,而vImage需要约20毫秒。
在测试其他功能时,我发现有一个制作了一个简单滑块应用程序的项目,其中包含一个模糊函数(gist),我进行了清理以进行测试,大约也需要20毫秒。
有什么技巧可以使这些功能更快吗?

1
虽然有些人不喜欢询问针对性能的框架的问题,但我认为这个问题具有很大的价值。苹果公司宣传Accelerate是一种轻松获得高性能代码的方法,但文档在使用Accelerate方面非常简单,因此SO可以通过获取与此主题相关的一些代码示例来改进这一点。 - Cameron Lowell Palmer
3个回答

7
要使用equalizeHistogram函数获得每秒30帧,必须将图像去交错(从ARGBxxxx转换为PlanarX),并仅使R(红)G(绿)B(蓝)均衡化;如果您均衡化A(透明度),则帧率将降至至少24。
以下是完全按照您要求的代码,速度快,效果佳:
- (CVPixelBufferRef)copyRenderedPixelBuffer:(CVPixelBufferRef)pixelBuffer {

CVPixelBufferLockBaseAddress( pixelBuffer, 0 );

unsigned char *base = (unsigned char *)CVPixelBufferGetBaseAddress( pixelBuffer );
size_t width = CVPixelBufferGetWidth( pixelBuffer );
size_t height = CVPixelBufferGetHeight( pixelBuffer );
size_t stride = CVPixelBufferGetBytesPerRow( pixelBuffer );

vImage_Buffer _img = {
    .data = base,
    .height = height,
    .width = width,
    .rowBytes = stride
};

vImage_Error err;
vImage_Buffer _dstA, _dstR, _dstG, _dstB;

err = vImageBuffer_Init( &_dstA, height, width, 8 * sizeof( uint8_t ), kvImageNoFlags);
if (err != kvImageNoError)
    NSLog(@"vImageBuffer_Init (alpha) error: %ld", err);

err = vImageBuffer_Init( &_dstR, height, width, 8 * sizeof( uint8_t ), kvImageNoFlags);
if (err != kvImageNoError)
    NSLog(@"vImageBuffer_Init (red) error: %ld", err);

err = vImageBuffer_Init( &_dstG, height, width, 8 * sizeof( uint8_t ), kvImageNoFlags);
if (err != kvImageNoError)
    NSLog(@"vImageBuffer_Init (green) error: %ld", err);

err = vImageBuffer_Init( &_dstB, height, width, 8 * sizeof( uint8_t ), kvImageNoFlags);
if (err != kvImageNoError)
    NSLog(@"vImageBuffer_Init (blue) error: %ld", err);

err = vImageConvert_ARGB8888toPlanar8(&_img, &_dstA, &_dstR, &_dstG, &_dstB, kvImageNoFlags);
if (err != kvImageNoError)
    NSLog(@"vImageConvert_ARGB8888toPlanar8 error: %ld", err);

err = vImageEqualization_Planar8(&_dstR, &_dstR, kvImageNoFlags);
if (err != kvImageNoError)
    NSLog(@"vImageEqualization_Planar8 (red) error: %ld", err);

err = vImageEqualization_Planar8(&_dstG, &_dstG, kvImageNoFlags);
if (err != kvImageNoError)
    NSLog(@"vImageEqualization_Planar8 (green) error: %ld", err);

err = vImageEqualization_Planar8(&_dstB, &_dstB, kvImageNoFlags);
if (err != kvImageNoError)
    NSLog(@"vImageEqualization_Planar8 (blue) error: %ld", err);

err = vImageConvert_Planar8toARGB8888(&_dstA, &_dstR, &_dstG, &_dstB, &_img, kvImageNoFlags);
if (err != kvImageNoError)
    NSLog(@"vImageConvert_Planar8toARGB8888 error: %ld", err);

err = vImageContrastStretch_ARGB8888( &_img, &_img, kvImageNoError );
if (err != kvImageNoError)
    NSLog(@"vImageContrastStretch_ARGB8888 error: %ld", err);

free(_dstA.data);
free(_dstR.data);
free(_dstG.data);
free(_dstB.data);

CVPixelBufferUnlockBaseAddress( pixelBuffer, 0 );

return (CVPixelBufferRef)CFRetain( pixelBuffer );

请注意,即使我对 alpha 通道进行了操作,我仍然分配了它。这是因为在 ARGB8888 和 Planar8 之间来回转换需要分配和引用 alpha 通道缓冲区。无论如何,性能和质量都得到了提升。

另外,请注意,在将 Planar8 缓冲区转换为单个 ARGB8888 缓冲区后,我执行了对比度拉伸;这是因为与直方图均衡化函数逐个通道应用相比,它更快,并且获得了相同的结果(对比度拉伸函数不会像直方图均衡化一样导致 alpha 通道失真)。


哦,还有一件事:如果你这样做(也就是说,在均衡和对比度拉伸中省略了 alpha 通道),图像会好看一百倍。由于某种原因,将这些“增强”应用于 alpha 通道会严重扭曲 ARGB 组合。 - James Bush
这是非常有趣的信息。我甚至没有考虑过这个。你是通过实验发现的吗? - Cameron Lowell Palmer
1
实验是我的强项;在将产品交到别人手中之前,我总是探索每一个可能性。正如你刚才所说,结果确实令人着迷。 - James Bush
我喜欢这个答案。 :) - Cameron Lowell Palmer
所以你从大约20毫秒(50帧)到大约33毫秒(30帧),或者我理解错了吗?此外,我很惊讶转换为平面图像然后再转回来比直接在ARGB图像上处理更快,但苹果似乎也暗示这样做更快。是因为你只需要处理3个通道,这些通道已经准备好进行SIMD处理吗?对我来说,这似乎涉及很多复制,但不知何故它仍然更快... - SO_fix_the_vote_sorting_bug

6

尽量避免重复分配vImage_Buffer。

vImage加速性能的关键之一是重复使用vImage_Buffer。苹果有限的文档中多次提到这一点,但我却没有注意到。

在上述模糊代码示例中,我重新设计了测试应用程序,将vImage_Buffer输入和输出缓冲区设置为每个图像只设置一次,而不是每次调用boxBlur都设置一次。每次调用可以减少不到10毫秒,这在响应时间上产生了明显的差异。

这表明,在看到性能改进之前,Accelerate需要一些时间来启动。第一次调用此方法花费了34毫秒。

- (UIImage *)boxBlurWithSize:(int)boxSize
{
    vImage_Error error;
    error = vImageBoxConvolve_ARGB8888(&_inputImageBuffer,
                                       &_outputImageBuffer,
                                       NULL,
                                       0,
                                       0,
                                       boxSize,
                                       boxSize,
                                       NULL,
                                       kvImageEdgeExtend);
    if (error) {
        NSLog(@"vImage error %zd", error);
    }

    CGImageRef modifiedImageRef = vImageCreateCGImageFromBuffer(&_outputImageBuffer,
                                                                &_inputImageFormat,
                                                                NULL,
                                                                NULL,
                                                                kvImageNoFlags,
                                                                &error);

    UIImage *returnImage = [UIImage imageWithCGImage:modifiedImageRef];
    CGImageRelease(modifiedImageRef);

    return returnImage;
}

加速器以其所能达到的任何速度运行。问题在于,超过一定大小的新内存仅在虚拟上分配,然后稍后才映射。每次触摸新页面时,操作系统内核都会出现故障,将整个页面清零,然后交换回来。这就是减慢加速器速度的原因。预先分配和重复使用内存使向量代码能够不间断地运行,这意味着它可以全速运行。这不仅是加速器的问题,而是所有事情的问题。但是,当您推动光速时,像这样的宇宙尘埃就成为了一个问题。 - Ian Ollmann
1
@IanOllmann 绝对没错。我记录这些内容的目标是为了搞清楚这些关键概念。一些主题在现有的少量文档中只是顺带提到,但我在网上看到了很多糟糕的例子,它们假设使用 Accelerate 是快速的。由于 Accelerate 的机制是隐藏的,因此在实验时,您可能会计时调用的两侧并忽略 malloc/free 时间,但是我们已经确定 malloc 和 free 不是性能问题。 - Cameron Lowell Palmer
@CameronLowellPalmer 为了明确vImage缓冲区的“重新分配”和“重用”,我有如下解释:1)我认为这个例子展示了良好的vImage缓冲区“重用” - https://github.com/Itseez/opencv_for_ios_book_samples/blob/0b38fb11b63b2c96723906309c644447ba4fa8cc/CvEffects/CvEffects/Processing_Accelerate.cpp#L18 - 2)而这个例子则是“重新分配”vImage缓冲区时不正确或低效的情况 - https://github.com/Duffycola/opencv-ios-demos/blob/41d575284675553b5671abfc0facd1778e319b5e/shared/CvFilterController.mm#L180 - 我的理解是否正确? - kiranpradeep
1
@Kiran 一般来说,这些是好的做和不做的例子。OpenCV会尽可能地重用内存,因此您要依赖于OpenCV的行为,而代码块中的malloc绝对是一个不好的迹象。 - Cameron Lowell Palmer
1
在使用OpenCV时,不需要分配vImage_Buffer;没有alloc、init、malloc或任何类似的函数。你只需将OpenCV矩阵作为引用传递到方法中(即,在其前面加上一个&符号),并创建一个缓冲区指针,就像我在本帖底部提供的示例一样。 - James Bush

5
要在OpenCV中使用vImage,请将对您的OpenCV矩阵的引用传递给类似于此方法的方法:
long contrastStretch_Accelerate(const Mat& src, Mat& dst) {
    vImagePixelCount rows = static_cast<vImagePixelCount>(src.rows);
    vImagePixelCount cols = static_cast<vImagePixelCount>(src.cols);

    vImage_Buffer _src = { src.data, rows, cols, src.step };
    vImage_Buffer _dst = { dst.data, rows, cols, dst.step };

    vImage_Error err;

    err = vImageContrastStretch_ARGB8888( &_src, &_dst, 0 );
    return err;
}

调用此方法的方式,从您的OpenCV代码块中看起来像这样:

- (void)processImage:(Mat&)image;
{
    contrastStretch_Accelerate(image, image);
}

这很简单,由于这些都是指针引用,因此没有任何一种“深度复制”。它尽可能快速和高效,所有与上下文相关的性能考虑问题暂时不提(我也可以帮您处理这些问题)。

顺带一提:你知道吗,在将OpenCV与vImage混合时,需要更改通道排列吗?如果不知道,在调用任何OpenCV矩阵上的vImage函数之前,请先调用:

const uint8_t map[4] = { 3, 2, 1, 0 };
err = vImagePermuteChannels_ARGB8888(&_img, &_img, map, kvImageNoFlags);
if (err != kvImageNoError)
    NSLog(@"vImagePermuteChannels_ARGB8888 error: %ld", err);

执行相同的调用,映射和所有操作,以便将图像返回到适合OpenCV矩阵的通道顺序。


是的,我熟悉使用OpenCV作为图像的后端,如果您在项目中使用OpenCV,这将非常有用。+1 - Cameron Lowell Palmer

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