Эффективно подсчитывайте количество прозрачных пикселей в UIImage/CIImage с помощью Metal.

Каков самый быстрый способ подсчитать, сколько прозрачных пикселей существует в CIImage/UIImage?

Например:

введите здесь описание изображения

Моя первая мысль, если мы говорим об эффективности, это использовать Metal Kernel с помощью CIColorKernel или около того, но я не могу понять, как использовать его для вывода количества.

Также другие идеи, которые я имел в виду:

  1. Использовать какой-то средний цвет для его расчета, чем краснее, тем больше заполнено пикселями? Может быть, какой-то линейный расчет зависит от размера изображения (используя CIAreaAverage CIFilter?
  2. Посчитать пиксели один за другим и проверить значения RGB?
  3. Использование параллельных возможностей Metal, аналогично этому сообщению: Подсчет цветных пикселей на графическом процессоре — теория ?
  4. Уменьшите изображение, а затем подсчитайте? Или все другие процессы, предложенные выше, выполняются только с масштабированной версией, а многократное ее обратно зависит от пропорций масштабирования после расчета?

Как быстрее всего получить этот показатель?


person Roi Mulia    schedule 20.06.2021    source источник


Ответы (2)


То, что вы хотите выполнить, — это операция сокращения, которая не обязательно хорошо подходит для графического процессора из-за его массивно-параллельной природы. Я бы рекомендовал не писать операцию сокращения для графического процессора самостоятельно, а использовать некоторые высокооптимизированные встроенные API, предоставляемые Apple (например, CIAreaAverage или соответствующие шейдеры производительности Metal).

Наиболее эффективный способ немного зависит от вашего варианта использования, в частности, откуда берется изображение (загружается через UIImage/CGImage или результат конвейера Core Image?) и где вам понадобится результирующий счетчик (на стороне ЦП/Swift). или как вход для другого фильтра Core Image?).
Это также зависит от того, могут ли пиксели быть полупрозрачными (альфа не 0.0 или 1.0).

Если изображение находится на графическом процессоре и/или счетчик должен использоваться на графическом процессоре, я бы рекомендовал использовать CIAreaAverage. Альфа-значение результата должно отражать процент прозрачных пикселей. Обратите внимание, что это работает только в том случае, если теперь есть полупрозрачные пиксели.

Следующим лучшим решением, вероятно, является просто повторение данных пикселей на ЦП. Это может быть несколько миллионов пикселей, но сама операция выполняется очень быстро, так что это почти не займет времени. Вы даже можете использовать многопоточность, разделив изображение на куски и используя concurrentPerform(...) из DispatchQueue.

Последним, но, вероятно, излишним решением будет использование Accelerate (это сделает @FlexMonkey счастливым): загрузите пиксельные данные изображения в буфер vDSP и используйте методы sum или average для вычисления процента с использованием векторных единиц ЦП.

Пояснение

Когда я говорил, что операция редукции не обязательно хорошо подходит для GPU, я имел в виду, что ее довольно сложно реализовать эффективным способом и далеко не так просто, как последовательный алгоритм.

Конечно, проверка прозрачности пикселя может выполняться параллельно, но результаты должны быть собраны в одно значение, что требует чтения нескольких ядер графического процессора и запись значений в ту же память. Обычно это требует некоторой синхронизации (и тем самым препятствует параллельному выполнению) и влечет за собой затраты на задержку из-за доступа к общей или глобальной памяти. Вот почему эффективные алгоритмы сбора данных для графического процессора обычно следуют многоэтапному древовидному подходу. Я настоятельно рекомендую прочитать публикации NVIDIA по этой теме (например, здесь и здесь). Именно поэтому я рекомендовал использовать встроенные API, когда это возможно, поскольку команда Apple Metal знает, как лучше всего оптимизировать эти алгоритмы для своего оборудования.

Также есть пример реализации сокращения в Спецификации языка затенения металлов Apple (стр. 158), который использует встроенные функции simd_shuffle для эффективной передачи промежуточных значений вниз по дереву. Однако общий принцип такой же, как описано в публикациях NVIDIA, ссылки на которые приведены выше.

person Frank Schlegel    schedule 20.06.2021
comment
…также у Apple есть хорошая статья, в которой обсуждается интеграция Accelerate в рабочий процесс Core Image: разработчик. apple.com/documentation/accelerate/ - person Simon Gladman; 21.06.2021
comment
Спасибо, Саймон! Добавьте к этому: если вам действительно нужна только двоичная маска, Roi, вы можете рассмотреть возможность использования для нее одноканальной цели рисования (например, используя kCGImageAlphaOnly в качестве информации о растровом изображении). Тогда вам не нужен переход от чередующегося к плоскому шагу, упомянутому Саймоном выше. - person Frank Schlegel; 21.06.2021
comment
Подсчет пикселей действительно является массивно-параллельной операцией, поэтому я не понимаю, почему вы заявляете, что она не подходит для графического процессора. Фактически, для графического процессора подходит практически любая операция, основанная на пикселях, поскольку ее можно разбить на отдельные пиксели или ядра. - person Jeshua Lacock; 22.06.2021
comment
Вы правы, я немного неточно выразился. Я добавил пояснение к своему ответу. - person Frank Schlegel; 24.06.2021
comment
Кроме того, вы изменили свой ответ, но он начинается с заведомо ложной информации. - person Jeshua Lacock; 24.06.2021
comment
Я рад, что ты нашел быстрое решение, Иешуа. Но я по-прежнему не думаю, что мое утверждение о том, что операция редукции по своей сути не подходит для SIMD-устройства, такого как GPU, ложно. Определенно есть способы реализовать это эффективным образом (см. пример Apple, который я добавил к своему ответу), но это не так просто. Вот почему я рекомендовал использовать для этого встроенные высокоуровневые API, когда это возможно. - person Frank Schlegel; 25.06.2021
comment
Если он работает достаточно хорошо для приложений реального времени, он хорошо подходит для графического процессора, ИМХО. Если бы у вас был исходный код, я мог бы сравнить производительность, иначе это все просто теория. - person Jeshua Lacock; 25.06.2021

Чтобы ответить на ваш вопрос, как сделать металл, вы бы использовали device atomic_int.

По сути, вы создаете Int MTLBuffer и передаете его своему ядру и увеличиваете его на atomic_fetch_add_explicit.

Создать буфер один раз:

var bristleCounter = 0
counterBuffer = device.makeBuffer(bytes: &bristleCounter, length: MemoryLayout<Int>.size, options: [.storageModeShared])

Сбросить счетчик на 0 и привязать буфер счетчика:

var z = 0
counterBuffer.contents().copyMemory(from: &z, byteCount: MemoryLayout<Int>.size)
kernelEncoder.setBuffer(counterBuffer, offset: 0, index: 0)

Ядро:

kernel void myKernel (device atomic_int *counter [[buffer(0)]]) {}

Увеличьте счетчик в ядре (и получите значение):

int newCounterValue = atomic_fetch_add_explicit(counter, 1, memory_order_relaxed);

Получить счетчик на стороне процессора:

kernelEncoder.endEncoding()
kernelBuffer.commit()
kernelBuffer.waitUntilCompleted()
    
//Counter from kernel now in counterBuffer
let bufPointer = counterBuffer.contents().load(as: Int.self)
print("Counter: \(bufPointer)")
person Jeshua Lacock    schedule 22.06.2021
comment
Проблема в том, что каждому из сотен ядер графического процессора необходимо читать и записывать одно и то же значение из глобального адресного пространства. Даже при использовании встроенных функций atomic вы по-прежнему (а) блокируете любое параллельное выполнение, поскольку только одно ядро ​​​​может получить доступ к значению за раз, и (б) вызываете большую задержку при доступе к глобальной памяти. - person Frank Schlegel; 24.06.2021
comment
Вы хотите участвовать в гонках? На любом современном чипе достаточно быстро даже с большими изображениями. Первоначально вопрос касается того, как это можно реализовать в Metal, поэтому он уместен, даже если вы думаете, что ваш подход быстрее (и я предполагаю, что это только предположение). - person Jeshua Lacock; 25.06.2021
comment
Металл также является наиболее гибким подходом. Дополнительные функции могут не понадобиться в данный момент, но при таком подходе было бы довольно просто реализовать дополнительные возможности по мере необходимости. Это бесконечно настраиваемый. - person Jeshua Lacock; 25.06.2021
comment
Я не хотел вас лично обидеть, извините! Вопрос касался наиболее эффективного решения, поэтому я счел правильным потратить 20 минут на то, чтобы перечислить альтернативы и обсудить их плюсы и минусы. При желании я, вероятно, могу потратить больше времени на написание примера кода, но, как я сказал в своем ответе, в идеале это будет зависеть от окружающего варианта использования (откуда поступают данные и где используется результат). - person Frank Schlegel; 25.06.2021
comment
И вы правы, решение, которое вы предоставили, определенно работает, и компилятор и планировщик могут помочь заставить его работать достаточно быстро. Тем не менее, я по-прежнему не думаю, что это хорошее решение, поскольку оно нарушает несколько лучших практик программирования на GPU. - person Frank Schlegel; 25.06.2021
comment
Ну, без сравнения производительности или исходного кода, это просто теория. На практике, по моему опыту, использование металла для выполнения таких задач достаточно быстро для приложений реального времени. Дело не в том, что я принял ваш отрицательный голос лично, а в том, что этот вопрос спрашивает, как это можно сделать в металле и имеет тег металла, и я предоставил полный и полностью рабочий код, который не заслуживает отрицательного голоса в духе SO. - person Jeshua Lacock; 25.06.2021