Last active
July 6, 2024 16:36
-
-
Save ZacSweers/aac5f2e684e125e64ae6b1ad0a2540aa to your computer and use it in GitHub Desktop.
Demo implementation of client-side image blurriness detection on Android using renderscript.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright (c) 2018. Uber Technologies | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
package com.uber.blurdetectiondemo | |
import android.content.Context | |
import android.graphics.Bitmap | |
import android.graphics.Color | |
import android.renderscript.Allocation | |
import android.renderscript.Element | |
import android.renderscript.RenderScript | |
import android.renderscript.ScriptIntrinsicBlur | |
import android.renderscript.ScriptIntrinsicColorMatrix | |
import android.renderscript.ScriptIntrinsicConvolve3x3 | |
import android.widget.Toast | |
import androidx.annotation.ColorInt | |
object BlurrinessDetection { | |
private val CLASSIC_MATRIX = floatArrayOf( | |
-1.0f, -1.0f, -1.0f, | |
-1.0f, 8.0f, -1.0f, | |
-1.0f, -1.0f, -1.0f | |
) | |
/** | |
* This attempts to determine if a given [sourceBitmap] is blurry using a combination of | |
* renderscript utilities. | |
* | |
* Operations go like this: | |
* - Soft blur | |
* - Greyscale | |
* - 3x3 convolution | |
* | |
* @return if the source bitmap is "blurry" | |
*/ | |
fun runDetection(context: Context, sourceBitmap: Bitmap): Boolean { | |
val rs = RenderScript.create(context) | |
// First apply a soft blur to smoothen out visual artifacts | |
val smootherBitmap = Bitmap.createBitmap(sourceBitmap.width, | |
sourceBitmap.height, | |
sourceBitmap.config | |
) | |
val blurIntrinsic = ScriptIntrinsicBlur.create(rs, Element.RGBA_8888(rs)) | |
val source = Allocation.createFromBitmap(rs, | |
sourceBitmap, | |
Allocation.MipmapControl.MIPMAP_NONE, | |
Allocation.USAGE_SHARED | |
) | |
val blurTargetAllocation = Allocation.createFromBitmap(rs, | |
smootherBitmap, | |
Allocation.MipmapControl.MIPMAP_NONE, | |
Allocation.USAGE_SHARED | |
) | |
blurIntrinsic.apply { | |
setRadius(1f) | |
setInput(source) | |
forEach(blurTargetAllocation) | |
} | |
blurTargetAllocation.copyTo(smootherBitmap) | |
// Greyscale so we're only dealing with white <--> black pixels, this is so we only need to | |
// detect pixel | |
// luminosity | |
val greyscaleBitmap = Bitmap.createBitmap(sourceBitmap.width, | |
sourceBitmap.height, | |
sourceBitmap.config | |
) | |
val smootherInput = Allocation.createFromBitmap(rs, | |
smootherBitmap, | |
Allocation.MipmapControl.MIPMAP_NONE, | |
Allocation.USAGE_SHARED | |
) | |
val greyscaleTargetAllocation = Allocation.createFromBitmap(rs, | |
greyscaleBitmap, | |
Allocation.MipmapControl.MIPMAP_NONE, | |
Allocation.USAGE_SHARED | |
) | |
// Inverts and greyscales the image | |
val colorIntrinsic = ScriptIntrinsicColorMatrix.create(rs) | |
colorIntrinsic.setGreyscale() | |
colorIntrinsic.forEach(smootherInput, greyscaleTargetAllocation) | |
greyscaleTargetAllocation.copyTo(greyscaleBitmap) | |
// Run edge detection algorithm using a laplacian matrix convolution | |
// Apply 3x3 convolution to detect edges | |
val edgesBitmap = Bitmap.createBitmap(sourceBitmap.width, | |
sourceBitmap.height, | |
sourceBitmap.config | |
) | |
val greyscaleInput = Allocation.createFromBitmap(rs, | |
greyscaleBitmap, | |
Allocation.MipmapControl.MIPMAP_NONE, | |
Allocation.USAGE_SHARED | |
) | |
val edgesTargetAllocation = Allocation.createFromBitmap(rs, | |
edgesBitmap, | |
Allocation.MipmapControl.MIPMAP_NONE, | |
Allocation.USAGE_SHARED | |
) | |
val convolve = ScriptIntrinsicConvolve3x3.create(rs, Element.U8_4(rs)) | |
convolve.setInput(greyscaleInput) | |
convolve.setCoefficients(CLASSIC_MATRIX) // Or use others | |
convolve.forEach(edgesTargetAllocation) | |
edgesTargetAllocation.copyTo(edgesBitmap) | |
@ColorInt val mostLuminousColor = mostLuminousColorFromBitmap(edgesBitmap) | |
val colorHex = "#" + Integer.toHexString(mostLuminousColor) | |
val isBlurry = mostLuminousColor > Color.parseColor("#CECECE") // Demo threshold | |
// Note - in Android, Color.BLACK is -16777216 and Color.WHITE is -1, so range is somewhere in between. Higher is more luminous | |
Toast.makeText(context, | |
"Most luminous color is $colorHex. Blurry = $isBlurry", | |
Toast.LENGTH_SHORT | |
).show() | |
return isBlurry | |
} | |
/** | |
* Resolves the most luminous color pixel in a given bitmap. | |
* | |
* @param bitmap Source bitmap. | |
* @return The most luminous color pixel in the `bitmap` | |
*/ | |
@ColorInt | |
fun mostLuminousColorFromBitmap(bitmap: Bitmap): Int { | |
bitmap.setHasAlpha(false) | |
val pixels = IntArray(bitmap.height * bitmap.width) | |
bitmap.getPixels(pixels, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height) | |
@ColorInt var mostLuminousColor = Color.BLACK | |
for (pixel in pixels) { | |
if (pixel > mostLuminousColor) { | |
mostLuminousColor = pixel | |
} | |
} | |
return mostLuminousColor | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
mostLuminousColor returns positive values , but we are expecting only - values?
val isBlurry = mostLuminousColor > Color.parseColor("#CECECE") // Demo threshold
shouldnt this be isNotBlurry? since the value is bigger, means more luminous, and therefore not blurry?
@softlion any recommendation for renderscript replacement?