Быстрое размытие растрового изображения для Android SDK
В настоящее время в приложении для Android, которое я разрабатываю, я перебираю пиксели изображения, чтобы размыть его. Для изображения размером 640x480 это занимает около 30 секунд.
Просматривая приложения в Android Market, я наткнулся на одно, которое включает функцию размытия, и их размытие происходит очень быстро (примерно за 5 секунд), поэтому они, должно быть, используют другой метод размытия.
Кто-нибудь знает более быстрый способ, кроме перебора пикселей?
Переведено автоматически
Ответ 1
Для будущих пользователей Google, вот алгоритм, который я портировал с Quasimondo. Это что-то среднее между прямоугольным размытием и размытием по Гауссу, это очень красиво и к тому же довольно быстро.
Обновление для людей, сталкивающихся с проблемой ArrayIndexOutOfBoundsException : @anthonycr в комментариях предоставляет эту информацию :
Я обнаружил, что при замене Math.abs на StrictMath.abs или какой-либо другой реализации abs сбой не происходит.
/**
* Stack Blur v1.0 from
* http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html
* Java Author: Mario Klingemann <mario at quasimondo.com>
* http://incubator.quasimondo.com
*
* created Feburary 29, 2004
* Android port : Yahel Bouaziz <yahel at kayenko.com>
* http://www.kayenko.com
* ported april 5th, 2012
*
* This is a compromise between Gaussian Blur and Box blur
* It creates much better looking blurs than Box Blur, but is
* 7x faster than my Gaussian Blur implementation.
*
* I called it Stack Blur because this describes best how this
* filter works internally: it creates a kind of moving stack
* of colors whilst scanning through the image. Thereby it
* just has to add one new block of color to the right side
* of the stack and remove the leftmost color. The remaining
* colors on the topmost layer of the stack are either added on
* or reduced by one, depending on if they are on the right or
* on the left side of the stack.
*
* If you are using this algorithm in your code please add
* the following line:
* Stack Blur Algorithm by Mario Klingemann <mario@quasimondo.com>
*/
public Bitmap fastblur(Bitmap sentBitmap, float scale, int radius) {
int width = Math.round(sentBitmap.getWidth() * scale);
int height = Math.round(sentBitmap.getHeight() * scale);
sentBitmap = Bitmap.createScaledBitmap(sentBitmap, width, height, false);
Bitmap bitmap = sentBitmap.copy(sentBitmap.getConfig(), true);
if (radius < 1) {
return (null);
}
int w = bitmap.getWidth();
int h = bitmap.getHeight();
int[] pix = new int[w * h];
Log.e("pix", w + " " + h + " " + pix.length);
bitmap.getPixels(pix, 0, w, 0, 0, w, h);
int wm = w - 1;
int hm = h - 1;
int wh = w * h;
int div = radius + radius + 1;
int r[] = new int[wh];
int g[] = new int[wh];
int b[] = new int[wh];
int rsum, gsum, bsum, x, y, i, p, yp, yi, yw;
int vmin[] = new int[Math.max(w, h)];
int divsum = (div + 1) >> 1;
divsum *= divsum;
int dv[] = new int[256 * divsum];
for (i = 0; i < 256 * divsum; i++) {
dv[i] = (i / divsum);
}
yw = yi = 0;
int[][] stack = new int[div][3];
int stackpointer;
int stackstart;
int[] sir;
int rbs;
int r1 = radius + 1;
int routsum, goutsum, boutsum;
int rinsum, ginsum, binsum;
for (y = 0; y < h; y++) {
rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
for (i = -radius; i <= radius; i++) {
p = pix[yi + Math.min(wm, Math.max(i, 0))];
sir = stack[i + radius];
sir[0] = (p & 0xff0000) >> 16;
sir[1] = (p & 0x00ff00) >> 8;
sir[2] = (p & 0x0000ff);
rbs = r1 - Math.abs(i);
rsum += sir[0] * rbs;
gsum += sir[1] * rbs;
bsum += sir[2] * rbs;
if (i > 0) {
rinsum += sir[0];
ginsum += sir[1];
binsum += sir[2];
} else {
routsum += sir[0];
goutsum += sir[1];
boutsum += sir[2];
}
}
stackpointer = radius;
for (x = 0; x < w; x++) {
r[yi] = dv[rsum];
g[yi] = dv[gsum];
b[yi] = dv[bsum];
rsum -= routsum;
gsum -= goutsum;
bsum -= boutsum;
stackstart = stackpointer - radius + div;
sir = stack[stackstart % div];
routsum -= sir[0];
goutsum -= sir[1];
boutsum -= sir[2];
if (y == 0) {
vmin[x] = Math.min(x + radius + 1, wm);
}
p = pix[yw + vmin[x]];
sir[0] = (p & 0xff0000) >> 16;
sir[1] = (p & 0x00ff00) >> 8;
sir[2] = (p & 0x0000ff);
rinsum += sir[0];
ginsum += sir[1];
binsum += sir[2];
rsum += rinsum;
gsum += ginsum;
bsum += binsum;
stackpointer = (stackpointer + 1) % div;
sir = stack[(stackpointer) % div];
routsum += sir[0];
goutsum += sir[1];
boutsum += sir[2];
rinsum -= sir[0];
ginsum -= sir[1];
binsum -= sir[2];
yi++;
}
yw += w;
}
for (x = 0; x < w; x++) {
rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
yp = -radius * w;
for (i = -radius; i <= radius; i++) {
yi = Math.max(0, yp) + x;
sir = stack[i + radius];
sir[0] = r[yi];
sir[1] = g[yi];
sir[2] = b[yi];
rbs = r1 - Math.abs(i);
rsum += r[yi] * rbs;
gsum += g[yi] * rbs;
bsum += b[yi] * rbs;
if (i > 0) {
rinsum += sir[0];
ginsum += sir[1];
binsum += sir[2];
} else {
routsum += sir[0];
goutsum += sir[1];
boutsum += sir[2];
}
if (i < hm) {
yp += w;
}
}
yi = x;
stackpointer = radius;
for (y = 0; y < h; y++) {
// Preserve alpha channel: ( 0xff000000 & pix[yi] )
pix[yi] = ( 0xff000000 & pix[yi] ) | ( dv[rsum] << 16 ) | ( dv[gsum] << 8 ) | dv[bsum];
rsum -= routsum;
gsum -= goutsum;
bsum -= boutsum;
stackstart = stackpointer - radius + div;
sir = stack[stackstart % div];
routsum -= sir[0];
goutsum -= sir[1];
boutsum -= sir[2];
if (x == 0) {
vmin[y] = Math.min(y + r1, hm) * w;
}
p = x + vmin[y];
sir[0] = r[p];
sir[1] = g[p];
sir[2] = b[p];
rinsum += sir[0];
ginsum += sir[1];
binsum += sir[2];
rsum += rinsum;
gsum += ginsum;
bsum += binsum;
stackpointer = (stackpointer + 1) % div;
sir = stack[stackpointer];
routsum += sir[0];
goutsum += sir[1];
boutsum += sir[2];
rinsum -= sir[0];
ginsum -= sir[1];
binsum -= sir[2];
yi += w;
}
}
Log.e("pix", w + " " + h + " " + pix.length);
bitmap.setPixels(pix, 0, w, 0, 0, w, h);
return (bitmap);
}
Ответ 2
Руководство по размытию Android 2016
с приложением Showcase / Benchmark и исходным кодом на GitHub. Также ознакомьтесь с фреймворком Blur, над которым я сейчас работаю: Dali.
После долгих экспериментов я теперь могу смело дать вам несколько надежных рекомендаций, которые облегчат вашу жизнь в Android при использовании Android Framework.
Загрузите и используйте растровое изображение в уменьшенном масштабе (для очень размытых изображений)
Никогда не используйте полный размер растрового изображения. Чем больше изображение, тем больше должно быть размыто, а также тем выше должен быть радиус размытия, и обычно, чем больше радиус размытия, тем дольше работает алгоритм.
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 8;
Bitmap blurTemplate = BitmapFactory.decodeResource(getResources(), R.drawable.myImage, options);
Это загрузит растровое изображение с inSampleSize
8, то есть только 1/64 от исходного изображения. Протестируйте то, что inSampleSize
соответствует вашим потребностям, но оставьте 2 ^ n (2,4,8, ...), чтобы избежать ухудшения качества из-за масштабирования. Подробнее в Google doc
Еще одним огромным преимуществом является то, что загрузка растрового изображения будет действительно быстрой. В моем раннем тестировании размытия я выяснил, что дольше всего в процессе размытия загружается изображение. Итак, для загрузки изображения с разрешением 1920х1080 с диска моему Nexus 5 потребовалось 500 мс, в то время как размытие заняло всего 250 мс или около того.
Используйте Renderscript
Renderscript предоставляет ScriptIntrinsicBlur
фильтр размытия по Гауссу. Он обладает хорошим визуальным качеством и является самым быстрым, который вы реально можете получить на Android. Google утверждает, что он "обычно в 2-3 раза быстрее, чем многопоточная реализация на C, и часто в 10 с лишним раз быстрее, чем реализация на Java". Renderscript действительно сложный (использует самое быстрое устройство обработки (графический процессор, интернет-провайдер и т.д.) и т.д.), А также для него есть библиотека поддержки v8, делающая его совместимым до 2.2. Ну, по крайней мере, теоретически, благодаря моим собственным тестам и отчетам других разработчиков кажется, что использовать Renderscript вслепую невозможно, поскольку фрагментация оборудования / драйверов, похоже, вызывает проблемы с некоторыми устройствами, даже с более высоким уровнем SDK (например, у меня были проблемы с 4.1 Nexus S), поэтому будьте осторожны и тестируйте на многих устройствах. устройств. Вот простой пример, который поможет вам начать:
//define this only once if blurring multiple times
RenderScript rs = RenderScript.create(context);
(...)
//this will blur the bitmapOriginal with a radius of 8 and save it in bitmapOriginal
final Allocation input = Allocation.createFromBitmap(rs, bitmapOriginal); //use this constructor for best performance, because it uses USAGE_SHARED mode which reuses memory
final Allocation output = Allocation.createTyped(rs, input.getType());
final ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
script.setRadius(8f);
script.setInput(input);
script.forEach(output);
output.copyTo(bitmapOriginal);
При использовании поддержки Gradle версии 8, которая специально рекомендована Google "потому что они включают последние улучшения", вам нужно всего лишь добавить 2 строки в свой сценарий сборки и использовать android.support.v8.renderscript
с текущими инструментами сборки (обновленный синтаксис для плагина Android Gradle версии 14+)
android {
...
defaultConfig {
...
renderscriptTargetApi 19
renderscriptSupportModeEnabled true
}
}
Простой бенчмарк на Nexus 5 - сравнение RenderScript с различными другими реализациями java и Renderscript:
Среднее время выполнения для каждого размытия при разных размерах рисунка
Мегапиксели в секунду, которые могут быть размыты
Каждое значение равно среднему значению в 250 раундов. RS_GAUSS_FAST
является ScriptIntrinsicBlur
(и почти всегда самым быстрым), другие, которые начинаются с RS_
, в основном представляют собой сверточные реализации с простыми ядрами. Подробности алгоритмов можно найти здесь. Это не просто размытие, поскольку значительная часть - это сбор мусора, который измеряется. Это можно увидеть здесь (ScriptIntrinsicBlur
на изображении 100x100 с примерно 500 раундами)
Пики - это gc.
Вы можете проверить сами, приложение для бенчмарка есть в Playstore: BlurBenchmark
Растровое изображение используется повторно везде, где это возможно (если приоритет: производительность > объем памяти)
Если вам нужно несколько размытий для живого размытия или чего-то подобного, и ваша память позволяет это, не загружайте растровое изображение из drawables несколько раз, а сохраните его "кэшированным" в переменной-члене. В этом случае всегда старайтесь использовать одни и те же переменные, чтобы свести сбор мусора к минимуму.
Также обратите внимание на новую inBitmap
опцию при загрузке из файла или drawable, которая позволит повторно использовать память растровых изображений и сэкономить время на сборку мусора.
Для перехода от резкого к размытому
Простой и наивный метод заключается в том, чтобы просто использовать 2 ImageViews
, один из них размытый, а альфа-обесцвечивает их. Но если вы хотите более утонченный вид, который плавно переходит от резкого к размытому, тогда ознакомьтесь с постом Романа Нурика о том, как сделать это, как в его приложении Muzei.
В основном он объясняет, что предварительно размывает некоторые кадры с разной степенью размытия и использует их в качестве ключевых кадров в анимации, которая выглядит действительно плавной.
Ответ 3
Это снимок в темноте, но вы можете попробовать уменьшить изображение, а затем увеличить его снова. Это можно сделать с помощью Bitmap.createScaledBitmap(Bitmap src, int dstWidth, int dstHeight, boolean filter)
. Убедитесь в этом и установите параметру filter значение true. Он будет выполняться в машинном коде, поэтому может быть быстрее.
Ответ 4
РЕДАКТИРОВАТЬ (апрель 2014 г.): Похоже, что эта страница вопросов и ответов по-прежнему набирает много просмотров. Я знаю, что этот пост всегда получает положительные отзывы. Но если вы читаете это, вам нужно понимать, что опубликованные здесь ответы (как мои, так и принятый ответ) устарели. Если вы хотите реализовать эффективное размытие сегодня, вам следует использовать RenderScript вместо NDK или Java. RenderScript работает на Android 2.2+ (с использованием библиотеки поддержки Android), поэтому нет причин не использовать его.
Далее следует старый ответ, но будьте осторожны, поскольку он устарел.
Для гуглеров future2, вот алгоритм, который я портировал с порта Яхеля алгоритма Квазимондо, но с использованием NDK. Он, конечно, основан на ответе Яхеля. Но здесь используется собственный код на C, так что это быстрее. Намного быстрее. Примерно в 40 раз быстрее.
Я обнаружил, что все манипуляции с изображениями должны выполняться на Android с помощью NDK ... поначалу это несколько раздражает в реализации (прочитайте отличный учебник по использованию JNI и NDK здесь), но намного лучше и для многих вещей работает почти в реальном времени.
Для справки, используя Java-функцию Yahel, потребовалось 10 секунд, чтобы размыть мое изображение размером 480x532 пикселей с радиусом размытия 10. Но при использовании родной версии C это заняло 250 мс. И я почти уверен, что его все еще можно дополнительно оптимизировать... Я только что сделал глупое преобразование кода java, вероятно, есть какие-то манипуляции, которые можно сократить, не хотел тратить слишком много времени на рефакторинг всего этого.
#include <jni.h>
#include <string.h>
#include <math.h>
#include <stdio.h>
#include <android/log.h>
#include <android/bitmap.h>
#define LOG_TAG "libbitmaputils"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
typedef struct {
uint8_t red;
uint8_t green;
uint8_t blue;
uint8_t alpha;
} rgba;
JNIEXPORT void JNICALL Java_com_insert_your_package_ClassName_functionToBlur(JNIEnv* env, jobject obj, jobject bitmapIn, jobject bitmapOut, jint radius) {
LOGI("Blurring bitmap...");
// Properties
AndroidBitmapInfo infoIn;
void* pixelsIn;
AndroidBitmapInfo infoOut;
void* pixelsOut;
int ret;
// Get image info
if ((ret = AndroidBitmap_getInfo(env, bitmapIn, &infoIn)) < 0 || (ret = AndroidBitmap_getInfo(env, bitmapOut, &infoOut)) < 0) {
LOGE("AndroidBitmap_getInfo() failed ! error=%d", ret);
return;
}
// Check image
if (infoIn.format != ANDROID_BITMAP_FORMAT_RGBA_8888 || infoOut.format != ANDROID_BITMAP_FORMAT_RGBA_8888) {
LOGE("Bitmap format is not RGBA_8888!");
LOGE("==> %d %d", infoIn.format, infoOut.format);
return;
}
// Lock all images
if ((ret = AndroidBitmap_lockPixels(env, bitmapIn, &pixelsIn)) < 0 || (ret = AndroidBitmap_lockPixels(env, bitmapOut, &pixelsOut)) < 0) {
LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
}
int h = infoIn.height;
int w = infoIn.width;
LOGI("Image size is: %i %i", w, h);
rgba* input = (rgba*) pixelsIn;
rgba* output = (rgba*) pixelsOut;
int wm = w - 1;
int hm = h - 1;
int wh = w * h;
int whMax = max(w, h);
int div = radius + radius + 1;
int r[wh];
int g[wh];
int b[wh];
int rsum, gsum, bsum, x, y, i, yp, yi, yw;
rgba p;
int vmin[whMax];
int divsum = (div + 1) >> 1;
divsum *= divsum;
int dv[256 * divsum];
for (i = 0; i < 256 * divsum; i++) {
dv[i] = (i / divsum);
}
yw = yi = 0;
int stack[div][3];
int stackpointer;
int stackstart;
int rbs;
int ir;
int ip;
int r1 = radius + 1;
int routsum, goutsum, boutsum;
int rinsum, ginsum, binsum;
for (y = 0; y < h; y++) {
rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
for (i = -radius; i <= radius; i++) {
p = input[yi + min(wm, max(i, 0))];
ir = i + radius; // same as sir
stack[ir][0] = p.red;
stack[ir][1] = p.green;
stack[ir][2] = p.blue;
rbs = r1 - abs(i);
rsum += stack[ir][0] * rbs;
gsum += stack[ir][1] * rbs;
bsum += stack[ir][2] * rbs;
if (i > 0) {
rinsum += stack[ir][0];
ginsum += stack[ir][1];
binsum += stack[ir][2];
} else {
routsum += stack[ir][0];
goutsum += stack[ir][1];
boutsum += stack[ir][2];
}
}
stackpointer = radius;
for (x = 0; x < w; x++) {
r[yi] = dv[rsum];
g[yi] = dv[gsum];
b[yi] = dv[bsum];
rsum -= routsum;
gsum -= goutsum;
bsum -= boutsum;
stackstart = stackpointer - radius + div;
ir = stackstart % div; // same as sir
routsum -= stack[ir][0];
goutsum -= stack[ir][1];
boutsum -= stack[ir][2];
if (y == 0) {
vmin[x] = min(x + radius + 1, wm);
}
p = input[yw + vmin[x]];
stack[ir][0] = p.red;
stack[ir][1] = p.green;
stack[ir][2] = p.blue;
rinsum += stack[ir][0];
ginsum += stack[ir][1];
binsum += stack[ir][2];
rsum += rinsum;
gsum += ginsum;
bsum += binsum;
stackpointer = (stackpointer + 1) % div;
ir = (stackpointer) % div; // same as sir
routsum += stack[ir][0];
goutsum += stack[ir][1];
boutsum += stack[ir][2];
rinsum -= stack[ir][0];
ginsum -= stack[ir][1];
binsum -= stack[ir][2];
yi++;
}
yw += w;
}
for (x = 0; x < w; x++) {
rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
yp = -radius * w;
for (i = -radius; i <= radius; i++) {
yi = max(0, yp) + x;
ir = i + radius; // same as sir
stack[ir][0] = r[yi];
stack[ir][1] = g[yi];
stack[ir][2] = b[yi];
rbs = r1 - abs(i);
rsum += r[yi] * rbs;
gsum += g[yi] * rbs;
bsum += b[yi] * rbs;
if (i > 0) {
rinsum += stack[ir][0];
ginsum += stack[ir][1];
binsum += stack[ir][2];
} else {
routsum += stack[ir][0];
goutsum += stack[ir][1];
boutsum += stack[ir][2];
}
if (i < hm) {
yp += w;
}
}
yi = x;
stackpointer = radius;
for (y = 0; y < h; y++) {
output[yi].red = dv[rsum];
output[yi].green = dv[gsum];
output[yi].blue = dv[bsum];
rsum -= routsum;
gsum -= goutsum;
bsum -= boutsum;
stackstart = stackpointer - radius + div;
ir = stackstart % div; // same as sir
routsum -= stack[ir][0];
goutsum -= stack[ir][1];
boutsum -= stack[ir][2];
if (x == 0) vmin[y] = min(y + r1, hm) * w;
ip = x + vmin[y];
stack[ir][0] = r[ip];
stack[ir][1] = g[ip];
stack[ir][2] = b[ip];
rinsum += stack[ir][0];
ginsum += stack[ir][1];
binsum += stack[ir][2];
rsum += rinsum;
gsum += ginsum;
bsum += binsum;
stackpointer = (stackpointer + 1) % div;
ir = stackpointer; // same as sir
routsum += stack[ir][0];
goutsum += stack[ir][1];
boutsum += stack[ir][2];
rinsum -= stack[ir][0];
ginsum -= stack[ir][1];
binsum -= stack[ir][2];
yi += w;
}
}
// Unlocks everything
AndroidBitmap_unlockPixels(env, bitmapIn);
AndroidBitmap_unlockPixels(env, bitmapOut);
LOGI ("Bitmap blurred.");
}
int min(int a, int b) {
return a > b ? b : a;
}
int max(int a, int b) {
return a > b ? a : b;
}
Затем используйте это следующим образом (учитывая класс с именем com.insert.your.package.className и встроенную функцию с именем functionToBlur, как указано в приведенном выше коде):
// Create a copy
Bitmap bitmapOut = bitmapIn.copy(Bitmap.Config.ARGB_8888, true);
// Blur the copy
functionToBlur(bitmapIn, bitmapOut, __radius);
Ожидается растровое изображение RGB_8888!
Чтобы использовать растровое изображение RGB_565, либо создайте преобразованную копию перед передачей параметра (фу), либо измените функцию на использование нового rgb565
типа вместо rgba
:
typedef struct {
uint16_t byte0;
} rgb565;
Проблема в том, что если вы сделаете это, вы больше не сможете прочитать .red
, .green
и .blue
пиксель, вам нужно правильно прочитать байт, да. Когда мне это было нужно раньше, я сделал это:
r = (pixels[x].byte0 & 0xF800) >> 8;
g = (pixels[x].byte0 & 0x07E0) >> 3;
b = (pixels[x].byte0 & 0x001F) << 3;
Но, вероятно, есть какой-то менее тупой способ сделать это. Боюсь, я не очень хорошо разбираюсь в C-кодировании низкого уровня.