Add enhancement: portrait blur with deeplab

This commit is contained in:
Ming Ming 2022-05-12 16:33:18 +08:00
parent 0c400786e0
commit 762fe521b2
12 changed files with 274 additions and 12 deletions

View file

@ -8,3 +8,5 @@ const homeFolderNotFoundUrl =
"https://gitlab.com/nkming2/nc-photos/-/wikis/help/home-folder-not-found"; "https://gitlab.com/nkming2/nc-photos/-/wikis/help/home-folder-not-found";
const enhanceZeroDceUrl = const enhanceZeroDceUrl =
"https://gitlab.com/nkming2/nc-photos/-/wikis/help/enhance/zero-dce"; "https://gitlab.com/nkming2/nc-photos/-/wikis/help/enhance/zero-dce";
const enhanceDeepLabPortraitBlurUrl =
"https://gitlab.com/nkming2/nc-photos/-/wikis/help/enhance/portrait-blur-(deeplab)";

View file

@ -1187,6 +1187,10 @@
"@deletePermanentlyLocalConfirmationDialogContent": { "@deletePermanentlyLocalConfirmationDialogContent": {
"description": "Make sure the user wants to delete the items from the current device" "description": "Make sure the user wants to delete the items from the current device"
}, },
"enhancePortraitBlurTitle": "Portrait blur",
"@enhancePortraitBlurTitle": {
"description": "Blur the background of a photo"
},
"errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues", "errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues",
"@errorUnauthenticated": { "@errorUnauthenticated": {

View file

@ -88,6 +88,7 @@
"enhanceLowLightTitle", "enhanceLowLightTitle",
"collectionEnhancedPhotosLabel", "collectionEnhancedPhotosLabel",
"deletePermanentlyLocalConfirmationDialogContent", "deletePermanentlyLocalConfirmationDialogContent",
"enhancePortraitBlurTitle",
"errorAlbumDowngrade" "errorAlbumDowngrade"
], ],
@ -194,6 +195,7 @@
"enhanceLowLightTitle", "enhanceLowLightTitle",
"collectionEnhancedPhotosLabel", "collectionEnhancedPhotosLabel",
"deletePermanentlyLocalConfirmationDialogContent", "deletePermanentlyLocalConfirmationDialogContent",
"enhancePortraitBlurTitle",
"errorAlbumDowngrade" "errorAlbumDowngrade"
], ],
@ -355,6 +357,7 @@
"enhanceLowLightTitle", "enhanceLowLightTitle",
"collectionEnhancedPhotosLabel", "collectionEnhancedPhotosLabel",
"deletePermanentlyLocalConfirmationDialogContent", "deletePermanentlyLocalConfirmationDialogContent",
"enhancePortraitBlurTitle",
"errorAlbumDowngrade" "errorAlbumDowngrade"
], ],
@ -366,14 +369,16 @@
"enhanceTooltip", "enhanceTooltip",
"enhanceLowLightTitle", "enhanceLowLightTitle",
"collectionEnhancedPhotosLabel", "collectionEnhancedPhotosLabel",
"deletePermanentlyLocalConfirmationDialogContent" "deletePermanentlyLocalConfirmationDialogContent",
"enhancePortraitBlurTitle"
], ],
"fi": [ "fi": [
"enhanceTooltip", "enhanceTooltip",
"enhanceLowLightTitle", "enhanceLowLightTitle",
"collectionEnhancedPhotosLabel", "collectionEnhancedPhotosLabel",
"deletePermanentlyLocalConfirmationDialogContent" "deletePermanentlyLocalConfirmationDialogContent",
"enhancePortraitBlurTitle"
], ],
"fr": [ "fr": [
@ -384,7 +389,8 @@
"enhanceTooltip", "enhanceTooltip",
"enhanceLowLightTitle", "enhanceLowLightTitle",
"collectionEnhancedPhotosLabel", "collectionEnhancedPhotosLabel",
"deletePermanentlyLocalConfirmationDialogContent" "deletePermanentlyLocalConfirmationDialogContent",
"enhancePortraitBlurTitle"
], ],
"pl": [ "pl": [
@ -412,20 +418,23 @@
"enhanceTooltip", "enhanceTooltip",
"enhanceLowLightTitle", "enhanceLowLightTitle",
"collectionEnhancedPhotosLabel", "collectionEnhancedPhotosLabel",
"deletePermanentlyLocalConfirmationDialogContent" "deletePermanentlyLocalConfirmationDialogContent",
"enhancePortraitBlurTitle"
], ],
"pt": [ "pt": [
"enhanceTooltip", "enhanceTooltip",
"enhanceLowLightTitle", "enhanceLowLightTitle",
"collectionEnhancedPhotosLabel", "collectionEnhancedPhotosLabel",
"deletePermanentlyLocalConfirmationDialogContent" "deletePermanentlyLocalConfirmationDialogContent",
"enhancePortraitBlurTitle"
], ],
"ru": [ "ru": [
"enhanceTooltip", "enhanceTooltip",
"enhanceLowLightTitle", "enhanceLowLightTitle",
"collectionEnhancedPhotosLabel", "collectionEnhancedPhotosLabel",
"deletePermanentlyLocalConfirmationDialogContent" "deletePermanentlyLocalConfirmationDialogContent",
"enhancePortraitBlurTitle"
] ]
} }

View file

@ -72,6 +72,16 @@ class EnhanceHandler {
}, },
); );
break; break;
case _Algorithm.deepLab3Portrait:
await ImageProcessor.deepLab3Portrait(
"${account.url}/${file.path}",
file.filename,
headers: {
"Authorization": Api.getAuthorizationHeaderValue(account),
},
);
break;
} }
} }
@ -105,6 +115,13 @@ class EnhanceHandler {
link: enhanceZeroDceUrl, link: enhanceZeroDceUrl,
algorithm: _Algorithm.zeroDce, algorithm: _Algorithm.zeroDce,
), ),
if (platform_k.isAndroid)
_Option(
title: L10n.global().enhancePortraitBlurTitle,
subtitle: "DeepLap v3",
link: enhanceDeepLabPortraitBlurUrl,
algorithm: _Algorithm.deepLab3Portrait,
),
]; ];
final Account account; final Account account;
@ -115,6 +132,7 @@ class EnhanceHandler {
enum _Algorithm { enum _Algorithm {
zeroDce, zeroDce,
deepLab3Portrait,
} }
class _Option { class _Option {

View file

@ -18,6 +18,7 @@ rootProject.allprojects {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
maven { url 'https://jitpack.io' }
} }
} }
@ -55,6 +56,7 @@ dependencies {
implementation "androidx.annotation:annotation:1.3.0" implementation "androidx.annotation:annotation:1.3.0"
implementation "androidx.core:core-ktx:1.7.0" implementation "androidx.core:core-ktx:1.7.0"
implementation "androidx.exifinterface:exifinterface:1.3.3" implementation "androidx.exifinterface:exifinterface:1.3.3"
implementation 'com.github.android:renderscript-intrinsics-replacement-toolkit:b6363490c3'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'org.tensorflow:tensorflow-lite:2.8.0' implementation 'org.tensorflow:tensorflow-lite:2.8.0'
} }

View file

@ -30,6 +30,19 @@ class ImageProcessorChannelHandler(context: Context) :
} }
} }
"deepLab3Portrait" -> {
try {
deepLab3Portrait(
call.argument("fileUrl")!!,
call.argument("headers"),
call.argument("filename")!!,
result
)
} catch (e: Throwable) {
result.error("systemException", e.toString(), null)
}
}
else -> result.notImplemented() else -> result.notImplemented()
} }
} }
@ -45,12 +58,25 @@ class ImageProcessorChannelHandler(context: Context) :
private fun zeroDce( private fun zeroDce(
fileUrl: String, headers: Map<String, String>?, filename: String, fileUrl: String, headers: Map<String, String>?, filename: String,
result: MethodChannel.Result result: MethodChannel.Result
) = method(
fileUrl, headers, filename, ImageProcessorService.METHOD_ZERO_DCE,
result
)
private fun deepLab3Portrait(
fileUrl: String, headers: Map<String, String>?, filename: String,
result: MethodChannel.Result
) = method(
fileUrl, headers, filename,
ImageProcessorService.METHOD_DEEL_LAP_PORTRAIT, result
)
private fun method(
fileUrl: String, headers: Map<String, String>?, filename: String,
method: String, result: MethodChannel.Result
) { ) {
val intent = Intent(context, ImageProcessorService::class.java).apply { val intent = Intent(context, ImageProcessorService::class.java).apply {
putExtra( putExtra(ImageProcessorService.EXTRA_METHOD, method)
ImageProcessorService.EXTRA_METHOD,
ImageProcessorService.METHOD_ZERO_DCE
)
putExtra(ImageProcessorService.EXTRA_FILE_URL, fileUrl) putExtra(ImageProcessorService.EXTRA_FILE_URL, fileUrl)
putExtra( putExtra(
ImageProcessorService.EXTRA_HEADERS, ImageProcessorService.EXTRA_HEADERS,

View file

@ -18,6 +18,7 @@ import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
import com.nkming.nc_photos.plugin.image_processor.DeepLab3Portrait
import com.nkming.nc_photos.plugin.image_processor.ZeroDce import com.nkming.nc_photos.plugin.image_processor.ZeroDce
import java.io.File import java.io.File
import java.net.HttpURLConnection import java.net.HttpURLConnection
@ -27,6 +28,7 @@ class ImageProcessorService : Service() {
companion object { companion object {
const val EXTRA_METHOD = "method" const val EXTRA_METHOD = "method"
const val METHOD_ZERO_DCE = "zero-dce" const val METHOD_ZERO_DCE = "zero-dce"
const val METHOD_DEEL_LAP_PORTRAIT = "DeepLab3Portrait"
const val EXTRA_FILE_URL = "fileUrl" const val EXTRA_FILE_URL = "fileUrl"
const val EXTRA_HEADERS = "headers" const val EXTRA_HEADERS = "headers"
const val EXTRA_FILENAME = "filename" const val EXTRA_FILENAME = "filename"
@ -91,6 +93,9 @@ class ImageProcessorService : Service() {
val method = intent.getStringExtra(EXTRA_METHOD) val method = intent.getStringExtra(EXTRA_METHOD)
when (method) { when (method) {
METHOD_ZERO_DCE -> onZeroDce(startId, intent.extras!!) METHOD_ZERO_DCE -> onZeroDce(startId, intent.extras!!)
METHOD_DEEL_LAP_PORTRAIT -> onDeepLapPortrait(
startId, intent.extras!!
)
else -> { else -> {
Log.e(TAG, "Unknown method: $method") Log.e(TAG, "Unknown method: $method")
// we can't call stopSelf here as it'll stop the service even if // we can't call stopSelf here as it'll stop the service even if
@ -100,7 +105,20 @@ class ImageProcessorService : Service() {
} }
} }
private fun onZeroDce(startId: Int, extras: Bundle) { private fun onZeroDce(startId: Int, extras: Bundle) =
onMethod(startId, extras, METHOD_ZERO_DCE)
private fun onDeepLapPortrait(startId: Int, extras: Bundle) =
onMethod(startId, extras, METHOD_DEEL_LAP_PORTRAIT)
/**
* Handle methods without arguments
*
* @param startId
* @param extras
* @param method
*/
private fun onMethod(startId: Int, extras: Bundle, method: String) {
val fileUrl = extras.getString(EXTRA_FILE_URL)!! val fileUrl = extras.getString(EXTRA_FILE_URL)!!
@Suppress("Unchecked_cast") @Suppress("Unchecked_cast")
@ -109,7 +127,7 @@ class ImageProcessorService : Service() {
val filename = extras.getString(EXTRA_FILENAME)!! val filename = extras.getString(EXTRA_FILENAME)!!
addCommand( addCommand(
ImageProcessorCommand( ImageProcessorCommand(
startId, METHOD_ZERO_DCE, fileUrl, headers, filename startId, method, fileUrl, headers, filename
) )
) )
} }
@ -396,6 +414,9 @@ private open class ImageProcessorCommandTask(context: Context) :
ImageProcessorService.METHOD_ZERO_DCE -> ZeroDce(context).infer( ImageProcessorService.METHOD_ZERO_DCE -> ZeroDce(context).infer(
fileUri fileUri
) )
ImageProcessorService.METHOD_DEEL_LAP_PORTRAIT -> DeepLab3Portrait(
context
).infer(fileUri)
else -> throw IllegalArgumentException( else -> throw IllegalArgumentException(
"Unknown method: ${cmd.method}" "Unknown method: ${cmd.method}"
) )

View file

@ -22,3 +22,7 @@ inline fun <T> HttpURLConnection.use(block: (HttpURLConnection) -> T): T {
disconnect() disconnect()
} }
} }
inline fun ByteArray.transform(transform: (Byte) -> Byte) {
forEachIndexed{ i, v -> this[i] = transform(v) }
}

View file

@ -0,0 +1,150 @@
package com.nkming.nc_photos.plugin.image_processor
import android.content.Context
import android.graphics.*
import android.net.Uri
import android.util.Log
import com.google.android.renderscript.Toolkit
import com.nkming.nc_photos.plugin.BitmapResizeMethod
import com.nkming.nc_photos.plugin.BitmapUtil
import com.nkming.nc_photos.plugin.transform
import org.tensorflow.lite.Interpreter
import java.io.File
import java.nio.ByteBuffer
import java.nio.FloatBuffer
/**
* DeepLab is a state-of-art deep learning model for semantic image
* segmentation, where the goal is to assign semantic labels (e.g., person, dog,
* cat and so on) to every pixel in the input image
*
* See: https://github.com/tensorflow/models/tree/master/research/deeplab
*/
private class DeepLab3(context: Context) {
companion object {
private const val MODEL = "lite-model_mobilenetv2-dm05-coco_dr_1.tflite"
const val WIDTH = 513
const val HEIGHT = 513
private const val TAG = "DeepLab3"
}
enum class Label(val value: Int) {
BACKGROUND(0),
AEROPLANE(1),
BICYCLE(2),
BIRD(3),
BOAT(4),
BOTTLE(5),
BUS(6),
CAR(7),
CAT(8),
CHAIR(9),
COW(10),
DINING_TABLE(11),
DOG(12),
HORSE(13),
MOTORBIKE(14),
PERSON(15),
POTTED_PLANT(16),
SHEEP(17),
SOFA(18),
TRAIN(19),
TV(20),
}
fun infer(imageUri: Uri): ByteBuffer {
val interpreter =
Interpreter(TfLiteHelper.loadModelFromAsset(context, MODEL))
interpreter.allocateTensors()
Log.i(TAG, "Converting bitmap to input")
val inputBitmap =
BitmapUtil.loadImageFixed(context, imageUri, WIDTH, HEIGHT)
val input = TfLiteHelper.bitmapToRgbFloatArray(inputBitmap)
val output = FloatBuffer.allocate(WIDTH * HEIGHT * Label.values().size)
Log.i(TAG, "Inferring")
interpreter.run(input, output)
return TfLiteHelper.argmax(output, WIDTH, HEIGHT, Label.values().size)
}
private val context = context
}
class DeepLab3Portrait(context: Context) {
companion object {
private const val RADIUS = 16
private const val MAX_WIDTH = 2048
private const val MAX_HEIGHT = 1536
private const val TAG = "DeepLab3Portrait"
}
fun infer(imageUri: Uri): Bitmap {
val segmentMap = deepLab.infer(imageUri).also {
postProcessSegmentMap(it)
}
return enhance(imageUri, segmentMap, RADIUS)
}
/**
* Post-process the segment map.
*
* The resulting segment map will:
* 1. Contain only the most significant label (the one with the most pixel)
* 2. The label value set to 255
* 3. The background set to 0
*
* @param segmentMap
*/
private fun postProcessSegmentMap(segmentMap: ByteBuffer) {
// keep only the largest segment
val count = mutableMapOf<Byte, Int>()
segmentMap.array().forEach {
if (it != DeepLab3.Label.BACKGROUND.value.toByte()) {
count[it] = (count[it] ?: 0) + 1
}
}
val keep = count.maxByOrNull { it.value }?.key
segmentMap.array().transform { if (it == keep) 0xFF.toByte() else 0 }
}
private fun enhance(
imageUri: Uri, segmentMap: ByteBuffer, radius: Int
): Bitmap {
Log.i(TAG, "[enhance] Enhancing image")
// downscale original to prevent OOM
val orig = BitmapUtil.loadImage(
context, imageUri, MAX_WIDTH, MAX_HEIGHT, BitmapResizeMethod.FIT,
isAllowSwapSide = true, shouldUpscale = false
)
val bg = Toolkit.blur(orig, radius)
var alpha = Bitmap.createBitmap(
DeepLab3.WIDTH, DeepLab3.HEIGHT, Bitmap.Config.ALPHA_8
)
alpha.copyPixelsFromBuffer(segmentMap)
alpha = Bitmap.createScaledBitmap(alpha, orig.width, orig.height, true)
// blur the mask to smoothen the edge
alpha = Toolkit.blur(alpha, 16)
File(context.filesDir, "alpha.png").outputStream().use {
alpha.compress(Bitmap.CompressFormat.PNG, 50, it)
}
val shader = ComposeShader(
BitmapShader(orig, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP),
BitmapShader(alpha, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP),
PorterDuff.Mode.DST_ATOP
)
val paint = Paint().apply {
setShader(shader)
}
Canvas(bg).apply {
drawRect(0f, 0f, orig.width.toFloat(), orig.height.toFloat(), paint)
}
return bg
}
private val context = context
private val deepLab = DeepLab3(context)
}

View file

@ -88,5 +88,20 @@ interface TfLiteHelper {
outputBitmap.copyPixelsFromBuffer(buffer) outputBitmap.copyPixelsFromBuffer(buffer)
return outputBitmap return outputBitmap
} }
fun argmax(
output: FloatBuffer, width: Int, height: Int, channel: Int
): ByteBuffer {
val product = ByteBuffer.allocate(width * height)
val array = output.array()
var j = 0
for (i in 0 until width * height) {
val pixel = array.slice(j until j + channel)
val max = pixel.indices.maxByOrNull { pixel[it] }!!
product.put(i, max.toByte())
j += channel
}
return product
}
} }
} }

View file

@ -15,6 +15,17 @@ class ImageProcessor {
"filename": filename, "filename": filename,
}); });
static Future<void> deepLab3Portrait(
String fileUrl,
String filename, {
Map<String, String>? headers,
}) =>
_methodChannel.invokeMethod("deepLab3Portrait", <String, dynamic>{
"fileUrl": fileUrl,
"headers": headers,
"filename": filename,
});
static const _methodChannel = static const _methodChannel =
MethodChannel("${k.libId}/image_processor_method"); MethodChannel("${k.libId}/image_processor_method");
} }