From e343e5974168b09c691ea142ef17230af2bb98ac Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Wed, 4 May 2022 01:19:47 +0800 Subject: [PATCH] Add image enhancement algorithm: ZeroDCE --- plugin/android/build.gradle | 1 + plugin/android/src/main/AndroidManifest.xml | 12 +- .../zero_dce_lite_200x300_iter8_60.tflite | Bin 0 -> 53992 bytes .../com/nkming/nc_photos/plugin/BitmapUtil.kt | 162 ++++++++++++ .../com/nkming/nc_photos/plugin/Event.kt | 15 ++ .../plugin/ImageProcessorChannelHandler.kt | 61 +++++ .../nc_photos/plugin/ImageProcessorService.kt | 250 ++++++++++++++++++ .../kotlin/com/nkming/nc_photos/plugin/K.kt | 6 + .../nkming/nc_photos/plugin/NcPhotosPlugin.kt | 18 ++ .../com/nkming/nc_photos/plugin/Util.kt | 14 + .../plugin/image_processor/TfLiteHelper.kt | 92 +++++++ .../plugin/image_processor/ZeroDce.kt | 77 ++++++ .../outline_auto_fix_high_white_24.png | Bin 0 -> 400 bytes .../drawable-hdpi/outline_image_white_24.png | Bin 0 -> 253 bytes .../outline_auto_fix_high_white_24.png | Bin 0 -> 254 bytes .../drawable-mdpi/outline_image_white_24.png | Bin 0 -> 177 bytes .../outline_auto_fix_high_white_24.png | Bin 0 -> 410 bytes .../drawable-xhdpi/outline_image_white_24.png | Bin 0 -> 295 bytes .../outline_auto_fix_high_white_24.png | Bin 0 -> 654 bytes .../outline_image_white_24.png | Bin 0 -> 407 bytes .../outline_auto_fix_high_white_24.png | Bin 0 -> 749 bytes .../outline_image_white_24.png | Bin 0 -> 532 bytes plugin/lib/nc_photos_plugin.dart | 1 + plugin/lib/src/image_processor.dart | 15 ++ 24 files changed, 722 insertions(+), 2 deletions(-) create mode 100644 plugin/android/src/main/assets/zero_dce_lite_200x300_iter8_60.tflite create mode 100644 plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/BitmapUtil.kt create mode 100644 plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/Event.kt create mode 100644 plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/ImageProcessorChannelHandler.kt create mode 100644 plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/ImageProcessorService.kt create mode 100644 plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/Util.kt create mode 100644 plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/image_processor/TfLiteHelper.kt create mode 100644 plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/image_processor/ZeroDce.kt create mode 100644 plugin/android/src/main/res/drawable-hdpi/outline_auto_fix_high_white_24.png create mode 100644 plugin/android/src/main/res/drawable-hdpi/outline_image_white_24.png create mode 100644 plugin/android/src/main/res/drawable-mdpi/outline_auto_fix_high_white_24.png create mode 100644 plugin/android/src/main/res/drawable-mdpi/outline_image_white_24.png create mode 100644 plugin/android/src/main/res/drawable-xhdpi/outline_auto_fix_high_white_24.png create mode 100644 plugin/android/src/main/res/drawable-xhdpi/outline_image_white_24.png create mode 100644 plugin/android/src/main/res/drawable-xxhdpi/outline_auto_fix_high_white_24.png create mode 100644 plugin/android/src/main/res/drawable-xxhdpi/outline_image_white_24.png create mode 100644 plugin/android/src/main/res/drawable-xxxhdpi/outline_auto_fix_high_white_24.png create mode 100644 plugin/android/src/main/res/drawable-xxxhdpi/outline_image_white_24.png create mode 100644 plugin/lib/src/image_processor.dart diff --git a/plugin/android/build.gradle b/plugin/android/build.gradle index 16510a6b..f68b539c 100644 --- a/plugin/android/build.gradle +++ b/plugin/android/build.gradle @@ -55,4 +55,5 @@ dependencies { implementation "androidx.annotation:annotation:1.3.0" implementation "androidx.core:core-ktx:1.7.0" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'org.tensorflow:tensorflow-lite:2.8.0' } diff --git a/plugin/android/src/main/AndroidManifest.xml b/plugin/android/src/main/AndroidManifest.xml index 7b1d1a3b..d2bb947b 100644 --- a/plugin/android/src/main/AndroidManifest.xml +++ b/plugin/android/src/main/AndroidManifest.xml @@ -1,3 +1,11 @@ - + + + + + + + diff --git a/plugin/android/src/main/assets/zero_dce_lite_200x300_iter8_60.tflite b/plugin/android/src/main/assets/zero_dce_lite_200x300_iter8_60.tflite new file mode 100644 index 0000000000000000000000000000000000000000..972490cc0daedef9c038715d4592ebc7559a1b9b GIT binary patch literal 53992 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 literal 0 HcmV?d00001 diff --git a/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/BitmapUtil.kt b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/BitmapUtil.kt new file mode 100644 index 00000000..0637d46f --- /dev/null +++ b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/BitmapUtil.kt @@ -0,0 +1,162 @@ +package com.nkming.nc_photos.plugin + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.Uri +import android.util.Log + +fun Bitmap.aspectRatio() = width / height.toFloat() + +enum class BitmapResizeMethod { + FIT, + FILL, +} + +interface BitmapUtil { + companion object { + fun loadImageFixed( + context: Context, uri: Uri, targetW: Int, targetH: Int + ): Bitmap { + val opt = loadImageBounds(context, uri) + val subsample = calcBitmapSubsample( + opt.outWidth, opt.outHeight, targetW, targetH, + BitmapResizeMethod.FILL + ) + if (subsample > 1) { + Log.d( + TAG, + "Subsample image to fixed: $subsample ${opt.outWidth}x${opt.outHeight} -> ${targetW}x$targetH" + ) + } + val outOpt = BitmapFactory.Options().apply { + inSampleSize = subsample + } + val bitmap = loadImage(context, uri, outOpt) + if (subsample > 1) { + Log.d( + TAG, "Bitmap subsampled: ${bitmap.width}x${bitmap.height}" + ) + } + return Bitmap.createScaledBitmap(bitmap, targetW, targetH, true) + } + + /** + * Load a bitmap + * + * If @c resizeMethod == FIT, make sure the size of the bitmap can fit + * inside the bound defined by @c targetW and @c targetH, i.e., + * bitmap.w <= @c targetW and bitmap.h <= @c targetH + * + * If @c resizeMethod == FILL, make sure the size of the bitmap can + * completely fill the bound defined by @c targetW and @c targetH, i.e., + * bitmap.w >= @c targetW and bitmap.h >= @c targetH + * + * If bitmap is smaller than the bound and @c shouldUpscale == true, it + * will be upscaled + * + * @param context + * @param uri + * @param targetW + * @param targetH + * @param resizeMethod + * @param isAllowSwapSide + * @param shouldUpscale + * @return + */ + fun loadImage( + context: Context, + uri: Uri, + targetW: Int, + targetH: Int, + resizeMethod: BitmapResizeMethod, + isAllowSwapSide: Boolean = false, + shouldUpscale: Boolean = true, + ): Bitmap { + val opt = loadImageBounds(context, uri) + val shouldSwapSide = isAllowSwapSide && + opt.outWidth != opt.outHeight && + (opt.outWidth >= opt.outHeight) != (targetW >= targetH) + val dstW = if (shouldSwapSide) targetH else targetW + val dstH = if (shouldSwapSide) targetW else targetH + val subsample = calcBitmapSubsample( + opt.outWidth, opt.outHeight, dstW, dstH, resizeMethod + ) + if (subsample > 1) { + Log.d( + TAG, + "Subsample image to ${resizeMethod.name}: $subsample ${opt.outWidth}x${opt.outHeight} -> ${dstW}x$dstH" + + (if (shouldSwapSide) " (swapped)" else "") + ) + } + val outOpt = BitmapFactory.Options().apply { + inSampleSize = subsample + } + val bitmap = loadImage(context, uri, outOpt) + if (subsample > 1) { + Log.d( + TAG, "Bitmap subsampled: ${bitmap.width}x${bitmap.height}" + ) + } + if (bitmap.width < dstW && bitmap.height < dstH && !shouldUpscale) { + return bitmap + } + return when (resizeMethod) { + BitmapResizeMethod.FIT -> Bitmap.createScaledBitmap( + bitmap, + minOf(dstW, (dstH * bitmap.aspectRatio()).toInt()), + minOf(dstH, (dstW / bitmap.aspectRatio()).toInt()), + true + ) + + BitmapResizeMethod.FILL -> Bitmap.createScaledBitmap( + bitmap, + maxOf(dstW, (dstH * bitmap.aspectRatio()).toInt()), + maxOf(dstH, (dstW / bitmap.aspectRatio()).toInt()), + true + ) + } + } + + private fun loadImageBounds( + context: Context, uri: Uri + ): BitmapFactory.Options { + context.contentResolver.openInputStream(uri)!!.use { + val opt = BitmapFactory.Options().apply { + inJustDecodeBounds = true + } + BitmapFactory.decodeStream(it, null, opt) + return opt + } + } + + private fun loadImage( + context: Context, uri: Uri, opt: BitmapFactory.Options + ): Bitmap { + context.contentResolver.openInputStream(uri)!!.use { + return BitmapFactory.decodeStream(it, null, opt)!! + } + } + + private fun calcBitmapSubsample( + originalW: Int, + originalH: Int, + targetW: Int, + targetH: Int, + resizeMethod: BitmapResizeMethod + ): Int { + return when (resizeMethod) { + BitmapResizeMethod.FIT -> maxOf( + originalW / targetW, + originalH / targetH + ) + BitmapResizeMethod.FILL -> minOf( + originalW / targetW, + originalH / targetH + ) + } + } + + private const val TAG = "BitmapUtil" + } +} diff --git a/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/Event.kt b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/Event.kt new file mode 100644 index 00000000..651aca66 --- /dev/null +++ b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/Event.kt @@ -0,0 +1,15 @@ +package com.nkming.nc_photos.plugin + +import android.net.Uri + +interface MessageEvent + +data class ImageProcessorCompletedEvent( + val image: Uri, + val result: Uri, +) : MessageEvent + +data class ImageProcessorFailedEvent( + val image: Uri, + val exception: Throwable, +) : MessageEvent diff --git a/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/ImageProcessorChannelHandler.kt b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/ImageProcessorChannelHandler.kt new file mode 100644 index 00000000..a299d5d8 --- /dev/null +++ b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/ImageProcessorChannelHandler.kt @@ -0,0 +1,61 @@ +package com.nkming.nc_photos.plugin + +import android.content.Context +import android.content.Intent +import androidx.core.content.ContextCompat +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel + +class ImageProcessorChannelHandler(context: Context) : + MethodChannel.MethodCallHandler, EventChannel.StreamHandler { + companion object { + const val METHOD_CHANNEL = "${K.LIB_ID}/image_processor_method" + + private const val TAG = "ImageProcessorChannelHandler" + } + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + "zeroDce" -> { + try { + zeroDce( + call.argument("image")!!, + call.argument("filename")!!, + result + ) + } catch (e: Throwable) { + result.error("systemException", e.toString(), null) + } + } + + else -> result.notImplemented() + } + } + + override fun onListen(arguments: Any?, events: EventChannel.EventSink) { + eventSink = events + } + + override fun onCancel(arguments: Any?) { + eventSink = null + } + + private fun zeroDce( + image: String, filename: String, result: MethodChannel.Result + ) { + val intent = Intent(context, ImageProcessorService::class.java).apply { + putExtra( + ImageProcessorService.EXTRA_METHOD, + ImageProcessorService.METHOD_ZERO_DCE + ) + putExtra(ImageProcessorService.EXTRA_IMAGE, image) + putExtra(ImageProcessorService.EXTRA_FILENAME, filename) + } + ContextCompat.startForegroundService(context, intent) + result.success(null) + } + + private val context = context + private var eventSink: EventChannel.EventSink? = null +} diff --git a/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/ImageProcessorService.kt b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/ImageProcessorService.kt new file mode 100644 index 00000000..dfca302d --- /dev/null +++ b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/ImageProcessorService.kt @@ -0,0 +1,250 @@ +package com.nkming.nc_photos.plugin + +import android.annotation.SuppressLint +import android.app.Notification +import android.app.PendingIntent +import android.app.Service +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.net.Uri +import android.os.AsyncTask +import android.os.Bundle +import android.os.IBinder +import android.os.PowerManager +import android.util.Log +import androidx.core.app.NotificationChannelCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import com.nkming.nc_photos.plugin.image_processor.ZeroDce + +class ImageProcessorService : Service() { + companion object { + const val EXTRA_METHOD = "method" + const val METHOD_ZERO_DCE = "zero-dce" + const val EXTRA_IMAGE = "image" + const val EXTRA_FILENAME = "filename" + + private const val NOTIFICATION_ID = + K.IMAGE_PROCESSOR_SERVICE_NOTIFICATION_ID + private const val RESULT_NOTIFICATION_ID = + K.IMAGE_PROCESSOR_SERVICE_RESULT_NOTIFICATION_ID + private const val RESULT_FAILED_NOTIFICATION_ID = + K.IMAGE_PROCESSOR_SERVICE_RESULT_FAILED_NOTIFICATION_ID + private const val CHANNEL_ID = "ImageProcessorService" + + const val TAG = "ImageProcessorService" + } + + override fun onBind(intent: Intent?): IBinder? = null + + @SuppressLint("WakelockTimeout") + override fun onCreate() { + Log.i(TAG, "[onCreate] Service created") + super.onCreate() + wakeLock.acquire() + createNotificationChannel() + } + + override fun onDestroy() { + Log.i(TAG, "[onDestroy] Service destroyed") + wakeLock.release() + super.onDestroy() + } + + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + assert(intent.hasExtra(EXTRA_METHOD)) + assert(intent.hasExtra(EXTRA_IMAGE)) + if (!isForeground) { + try { + startForeground(NOTIFICATION_ID, buildNotification()) + isForeground = true + } catch (e: Throwable) { + // ??? + Log.e(TAG, "[onStartCommand] Failed while startForeground", e) + } + } + + val method = intent.getStringExtra(EXTRA_METHOD) + when (method) { + METHOD_ZERO_DCE -> onZeroDce(startId, intent.extras!!) + else -> { + Log.e(TAG, "Unknown method: $method") + // we can't call stopSelf here as it'll stop the service even if + // there are commands running in the bg + addCommand( + ImageProcessorCommand(startId, "null", Uri.EMPTY, "") + ) + } + } + return START_REDELIVER_INTENT + } + + private fun onZeroDce(startId: Int, extras: Bundle) { + val imageUri = Uri.parse(extras.getString(EXTRA_IMAGE)!!) + val filename = extras.getString(EXTRA_FILENAME)!! + addCommand( + ImageProcessorCommand(startId, METHOD_ZERO_DCE, imageUri, filename) + ) + } + + private fun createNotificationChannel() { + val channel = NotificationChannelCompat.Builder( + CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_LOW + ).run { + setName("Image processing") + setDescription("Enhance images in the background") + build() + } + notificationManager.createNotificationChannel(channel) + } + + private fun buildNotification(content: String? = null): Notification { + return NotificationCompat.Builder(this, CHANNEL_ID).run { + setSmallIcon(R.drawable.outline_auto_fix_high_white_24) + setContentTitle("Processing image") + if (content != null) setContentText(content) + build() + } + } + + private fun buildResultNotification(result: Uri): Notification { + val intent = Intent().apply { + `package` = packageName + component = ComponentName( + "com.nkming.nc_photos", "com.nkming.nc_photos.MainActivity" + ) + action = K.ACTION_SHOW_IMAGE_PROCESSOR_RESULT + putExtra(K.EXTRA_IMAGE_RESULT_URI, result) + } + val pi = PendingIntent.getActivity( + this, 0, intent, + PendingIntent.FLAG_UPDATE_CURRENT or getPendingIntentFlagImmutable() + ) + return NotificationCompat.Builder(this, CHANNEL_ID).run { + setSmallIcon(R.drawable.outline_image_white_24) + setContentTitle("Successfully enhanced image") + setContentText("Tap to view the result") + setContentIntent(pi) + setAutoCancel(true) + build() + } + } + + private fun buildResultFailedNotification( + exception: Throwable + ): Notification { + return NotificationCompat.Builder(this, CHANNEL_ID).run { + setSmallIcon(R.drawable.outline_image_white_24) + setContentTitle("Failed enhancing image") + setContentText(exception.message) + build() + } + } + + private fun addCommand(cmd: ImageProcessorCommand) { + cmds.add(cmd) + if (cmdTask == null) { + runCommand() + } + } + + @SuppressLint("StaticFieldLeak") + private fun runCommand() { + val cmd = cmds.first() + notificationManager.notify( + NOTIFICATION_ID, buildNotification(cmd.filename) + ) + cmdTask = object : ImageProcessorCommandTask(applicationContext) { + override fun onPostExecute(result: MessageEvent) { + notifyResult(result) + cmds.removeFirst() + stopSelf(cmd.startId) + if (cmds.isNotEmpty()) { + runCommand() + } else { + cmdTask = null + } + } + }.apply { + @Suppress("Deprecation") + executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, cmd) + } + } + + private fun notifyResult(event: MessageEvent) { + if (event is ImageProcessorCompletedEvent) { + notificationManager.notify( + RESULT_NOTIFICATION_ID, buildResultNotification(event.result) + ) + } else if (event is ImageProcessorFailedEvent) { + notificationManager.notify( + RESULT_FAILED_NOTIFICATION_ID, + buildResultFailedNotification(event.exception) + ) + } + } + + private var isForeground = false + private val cmds = mutableListOf() + private var cmdTask: ImageProcessorCommandTask? = null + + private val notificationManager by lazy { + NotificationManagerCompat.from(this) + } + private val wakeLock: PowerManager.WakeLock by lazy { + (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock( + PowerManager.PARTIAL_WAKE_LOCK, "nc-photos:ImageProcessorService" + ).apply { + setReferenceCounted(false) + } + } +} + +private data class ImageProcessorCommand( + val startId: Int, + val method: String, + val uri: Uri, + val filename: String, + val args: Map = mapOf(), +) + +@Suppress("Deprecation") +private open class ImageProcessorCommandTask(context: Context) : + AsyncTask() { + companion object { + private const val TAG = "ImageProcessorCommandTask" + } + + override fun doInBackground( + vararg params: ImageProcessorCommand? + ): MessageEvent { + val cmd = params[0]!! + return try { + val output = when (cmd.method) { + ImageProcessorService.METHOD_ZERO_DCE -> ZeroDce(context).infer( + cmd.uri + ) + else -> throw IllegalArgumentException( + "Unknown method: ${cmd.method}" + ) + } + val uri = saveBitmap(output, cmd.filename) + ImageProcessorCompletedEvent(cmd.uri, uri) + } catch (e: Throwable) { + ImageProcessorFailedEvent(cmd.uri, e) + } + } + + private fun saveBitmap(bitmap: Bitmap, filename: String): Uri { + return MediaStoreUtil.writeFileToDownload( + context, { + bitmap.compress(Bitmap.CompressFormat.JPEG, 85, it) + }, filename, "Photos (for Nextcloud)/Enhanced Photos" + ) + } + + @SuppressLint("StaticFieldLeak") + private val context = context +} diff --git a/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/K.kt b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/K.kt index 97656f61..f2a5d9d1 100644 --- a/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/K.kt +++ b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/K.kt @@ -4,12 +4,18 @@ interface K { companion object { const val DOWNLOAD_NOTIFICATION_ID_MIN = 1000 const val DOWNLOAD_NOTIFICATION_ID_MAX = 2000 + const val IMAGE_PROCESSOR_SERVICE_NOTIFICATION_ID = 5000 + const val IMAGE_PROCESSOR_SERVICE_RESULT_NOTIFICATION_ID = 5001 + const val IMAGE_PROCESSOR_SERVICE_RESULT_FAILED_NOTIFICATION_ID = 5002 const val PERMISSION_REQUEST_CODE = 11011 const val LIB_ID = "com.nkming.nc_photos.plugin" const val ACTION_DOWNLOAD_CANCEL = "${LIB_ID}.ACTION_DOWNLOAD_CANCEL" + const val ACTION_SHOW_IMAGE_PROCESSOR_RESULT = + "${LIB_ID}.ACTION_SHOW_IMAGE_PROCESSOR_RESULT" const val EXTRA_NOTIFICATION_ID = "${LIB_ID}.EXTRA_NOTIFICATION_ID" + const val EXTRA_IMAGE_RESULT_URI = "${LIB_ID}.EXTRA_IMAGE_RESULT_URI" } } diff --git a/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/NcPhotosPlugin.kt b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/NcPhotosPlugin.kt index 63734a8f..6ced3ad2 100644 --- a/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/NcPhotosPlugin.kt +++ b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/NcPhotosPlugin.kt @@ -8,6 +8,12 @@ import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.MethodChannel class NcPhotosPlugin : FlutterPlugin, ActivityAware { + companion object { + const val ACTION_SHOW_IMAGE_PROCESSOR_RESULT = + K.ACTION_SHOW_IMAGE_PROCESSOR_RESULT + const val EXTRA_IMAGE_RESULT_URI = K.EXTRA_IMAGE_RESULT_URI + } + override fun onAttachedToEngine( @NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding ) { @@ -46,6 +52,16 @@ class NcPhotosPlugin : FlutterPlugin, ActivityAware { MediaStoreChannelHandler.METHOD_CHANNEL ) mediaStoreMethodChannel.setMethodCallHandler(mediaStoreChannelHandler) + + imageProcessorMethodChannel = MethodChannel( + flutterPluginBinding.binaryMessenger, + ImageProcessorChannelHandler.METHOD_CHANNEL + ) + imageProcessorMethodChannel.setMethodCallHandler( + ImageProcessorChannelHandler( + flutterPluginBinding.applicationContext + ) + ) } override fun onDetachedFromEngine( @@ -57,6 +73,7 @@ class NcPhotosPlugin : FlutterPlugin, ActivityAware { nativeEventChannel.setStreamHandler(null) nativeEventMethodChannel.setMethodCallHandler(null) mediaStoreMethodChannel.setMethodCallHandler(null) + imageProcessorMethodChannel.setMethodCallHandler(null) } override fun onAttachedToActivity(binding: ActivityPluginBinding) { @@ -82,6 +99,7 @@ class NcPhotosPlugin : FlutterPlugin, ActivityAware { private lateinit var nativeEventChannel: EventChannel private lateinit var nativeEventMethodChannel: MethodChannel private lateinit var mediaStoreMethodChannel: MethodChannel + private lateinit var imageProcessorMethodChannel: MethodChannel private lateinit var lockChannelHandler: LockChannelHandler private lateinit var mediaStoreChannelHandler: MediaStoreChannelHandler diff --git a/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/Util.kt b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/Util.kt new file mode 100644 index 00000000..79100b89 --- /dev/null +++ b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/Util.kt @@ -0,0 +1,14 @@ +package com.nkming.nc_photos.plugin + +import android.app.PendingIntent +import android.os.Build + +fun getPendingIntentFlagImmutable(): Int { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + PendingIntent.FLAG_IMMUTABLE else 0 +} + +fun getPendingIntentFlagMutable(): Int { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + PendingIntent.FLAG_MUTABLE else 0 +} diff --git a/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/image_processor/TfLiteHelper.kt b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/image_processor/TfLiteHelper.kt new file mode 100644 index 00000000..15337768 --- /dev/null +++ b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/image_processor/TfLiteHelper.kt @@ -0,0 +1,92 @@ +package com.nkming.nc_photos.plugin.image_processor + +import android.content.Context +import android.graphics.Bitmap +import java.io.FileInputStream +import java.nio.ByteBuffer +import java.nio.FloatBuffer +import java.nio.IntBuffer +import java.nio.channels.FileChannel +import kotlin.math.abs + +interface TfLiteHelper { + companion object { + /** + * Load a TFLite model from the assets dir + * + * @param context + * @param name Name of the model file + * @return + */ + fun loadModelFromAsset(context: Context, name: String): ByteBuffer { + val fd = context.assets.openFd(name) + val istream = FileInputStream(fd.fileDescriptor) + val channel = istream.channel + return channel.map( + FileChannel.MapMode.READ_ONLY, fd.startOffset, fd.declaredLength + ) + } + + /** + * Convert an ARGB_8888 Android bitmap to a float RGB buffer + * + * @param bitmap + * @return + */ + fun bitmapToRgbFloatArray(bitmap: Bitmap): FloatBuffer { + val buffer = IntBuffer.allocate(bitmap.width * bitmap.height) + bitmap.copyPixelsToBuffer(buffer) + val input = FloatBuffer.allocate(bitmap.width * bitmap.height * 3) + buffer.array().forEach { + input.put((it and 0xFF) / 255.0f) + input.put((it shr 8 and 0xFF) / 255.0f) + input.put((it shr 16 and 0xFF) / 255.0f) + } + input.rewind() + return input + } + + /** + * Convert a float RGB buffer to an ARGB_8888 Android bitmap + * + * @param output + * @param width + * @param height + * @return + */ + fun rgbFloatArrayToBitmap( + output: FloatBuffer, width: Int, height: Int + ): Bitmap { + val buffer = IntBuffer.allocate(width * height) + var i = 0 + var pixel = 0 + output.array().forEach { + val value = (abs(it * 255f)).toInt().coerceIn(0, 255) + when (i++) { + 0 -> { + // A + pixel = 0xFF shl 24 + // R + pixel = pixel or value + } + 1 -> { + // G + pixel = pixel or (value shl 8) + } + 2 -> { + // B + pixel = pixel or (value shl 16) + + buffer.put(pixel) + i = 0 + } + } + } + buffer.rewind() + val outputBitmap = + Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + outputBitmap.copyPixelsFromBuffer(buffer) + return outputBitmap + } + } +} diff --git a/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/image_processor/ZeroDce.kt b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/image_processor/ZeroDce.kt new file mode 100644 index 00000000..21f9af55 --- /dev/null +++ b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/image_processor/ZeroDce.kt @@ -0,0 +1,77 @@ +package com.nkming.nc_photos.plugin.image_processor + +import android.content.Context +import android.graphics.Bitmap +import android.net.Uri +import android.util.Log +import com.nkming.nc_photos.plugin.BitmapResizeMethod +import com.nkming.nc_photos.plugin.BitmapUtil +import org.tensorflow.lite.Interpreter +import java.nio.FloatBuffer +import kotlin.math.pow + +class ZeroDce(context: Context) { + companion object { + private const val TAG = "ZeroDce" + private const val MODEL = "zero_dce_lite_200x300_iter8_60.tflite" + private const val WIDTH = 300 + private const val HEIGHT = 200 + private const val ITERATION = 8 + } + + fun infer(imageUri: Uri): Bitmap { + val alphaMaps = inferAlphaMaps(imageUri) + return enhance(imageUri, alphaMaps, ITERATION) + } + + private fun inferAlphaMaps(imageUri: Uri): Bitmap { + 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 inputs = arrayOf(TfLiteHelper.bitmapToRgbFloatArray(inputBitmap)) + val outputs = mapOf( + 0 to FloatBuffer.allocate(inputs[0].capacity()), + 1 to FloatBuffer.allocate(inputs[0].capacity()) + ) + Log.i(TAG, "Inferring") + interpreter.runForMultipleInputsOutputs(inputs, outputs) + + return TfLiteHelper.rgbFloatArrayToBitmap( + outputs[1]!!, inputBitmap.width, inputBitmap.height + ) + } + + private fun enhance( + imageUri: Uri, alphaMaps: Bitmap, iteration: Int + ): Bitmap { + Log.i(TAG, "Enhancing image, iteration: $iteration") + // downscale original to prevent OOM + val resized = BitmapUtil.loadImage( + context, imageUri, 1920, 1080, BitmapResizeMethod.FIT, + isAllowSwapSide = true, shouldUpscale = false + ) + // resize aMaps + val resizedFilter = Bitmap.createScaledBitmap( + alphaMaps, resized.width, resized.height, true + ) + + val imgBuf = TfLiteHelper.bitmapToRgbFloatArray(resized) + val filterBuf = TfLiteHelper.bitmapToRgbFloatArray(resizedFilter) + for (i in 0 until iteration) { + val src = imgBuf.array() + val filter = filterBuf.array() + for (j in src.indices) { + src[j] = src[j] + -filter[j] * (src[j].pow(2f) - src[j]) + } + } + return TfLiteHelper.rgbFloatArrayToBitmap( + imgBuf, resized.width, resized.height + ) + } + + private val context = context +} diff --git a/plugin/android/src/main/res/drawable-hdpi/outline_auto_fix_high_white_24.png b/plugin/android/src/main/res/drawable-hdpi/outline_auto_fix_high_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..28e5c12f68c622ccf8d9b36c057829ffd1d6a902 GIT binary patch literal 400 zcmV;B0dM|^P)29op}P8B)Ab0Sw)_}=mN7{x=H(Yur#f= zpSN}KPR;45QIWqMbaH33Xcd8;=;h3qFv(}S_&j9JXtHNdGn+Aa1it8JFjD}g zsT3AAE~oLz84aa3V-Der1LMSA)dtm8TCL4immiZA4#U#RAK#Fe>;Ui>mEMpEj6&%# z9v>B`Xi<^pp)La(3=zyAD@VUKFvDPk2f$jds7;vF53|G6>y5!Il9?_eB7(uQ%Fy_I z3e%a}Aaavb6axk|Fd}T(*1_}vQtpQca~DLe6N~CG=Lo=&xpKD`W;!g1j%DnR6Ec1_ z6W&(~02XwUv+=nPB3Iel4-*$xdB?gOBO-#0tp+~{epMxp}{SW`Y`#%#|2K^UAVr%~Y43w=S!yus67yk{RY@Ywuz_Ne-JCbM+ zRO_Sv;$YrNc(L&eB0{`DtDqW>{b&DQfKmeOfrJ!M2K_HY(zyHoFSNp}o=AiK+hS-% z1%LiK5ob_3!O}5(Xk`(q75>z!j;K~aQnAV&1vmfzC?zg%DzM%Z00000NkvXXu0mjf D<7;XZ literal 0 HcmV?d00001 diff --git a/plugin/android/src/main/res/drawable-mdpi/outline_auto_fix_high_white_24.png b/plugin/android/src/main/res/drawable-mdpi/outline_auto_fix_high_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..958060fa4325be5dce9c092631bb85d5911ded33 GIT binary patch literal 254 zcmV4!ss!U7kcVH8EHWymf3MJNPnh!YGiGsVwB5MqbbNoId+gr6pOL`5|^=qj%1LVMuO z4qVo7#}3?qoWl}6dnJxMfqV&fUEr(u;;mAWdgFhL57_WHx_|5~y#N3J07*qoM6N<$ Ef_VXFVgLXD literal 0 HcmV?d00001 diff --git a/plugin/android/src/main/res/drawable-mdpi/outline_image_white_24.png b/plugin/android/src/main/res/drawable-mdpi/outline_image_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..e465a85463e362acf0d25f92a0a499104cc4e7f1 GIT binary patch literal 177 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_+i3Qrfu5R22v2@-1_>_7hh`2UZM z$N$>~nA-gE=df|Q%D_?)+n{H^ic#^D)1Hq)$ literal 0 HcmV?d00001 diff --git a/plugin/android/src/main/res/drawable-xhdpi/outline_auto_fix_high_white_24.png b/plugin/android/src/main/res/drawable-xhdpi/outline_auto_fix_high_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..39c1301589305fac6c4564fe5438328c6b0fc6a7 GIT binary patch literal 410 zcmV;L0cHM)P) zOKQU~5QYWnHHu9GIYjP}g>RKbAh|#v>BCnDwAU#34)LOGjQP5VNiEy4C3$2@sdgGh z-!JARDfQw_cQ{*Lo`W}Cn?kJ)*)g~&KfAz6XAI>le+yKRevs?oIZ=JNzm+fe9fG!Eyt_Qn_AJ1SV6Veee~;rq2Uw>62l zwv)iOBtFFPEeUyihlDh~ArS&!Nvz1(kN7G4uV1yk0a3L_0cTI6BT+!uhg$0OBz zf_X4%qm)t%czq@CDu;j}UTD;eCYP9ypJ)nbbB_%2IK{SzU=x`Hl-d7XRQye36D%@y tp|u=#MG;spzt-!!?F)$YE0iE;@don@_h1X_l~e!#002ovPDHLkV1l(1b^ZVV literal 0 HcmV?d00001 diff --git a/plugin/android/src/main/res/drawable-xxhdpi/outline_auto_fix_high_white_24.png b/plugin/android/src/main/res/drawable-xxhdpi/outline_auto_fix_high_white_24.png new file mode 100644 index 0000000000000000000000000000000000000000..828ce9570056556bd1e3273a4cc4a6dcd9a9c08c GIT binary patch literal 654 zcmV;90&)F`P)eLN2o7XPQaM|F(xQ9I}s0y z91lc=W{Da-Zb-%rJ!%?D`HX2I$CNXzy4O{GhX_qZy%-~kw#3OHSv;Ugpq)gQkheOXdhc4}e}pI6`m@z_l&TS3^#)cr&O1XKU%oV$7Cp z&L^$4(`2x8_wjoFF;M<2PvLw^aHh(Y)#8#kwU@_SDqO`|=Jd&nz9}bA=NQjWf)n%I zjAOKh%HkBPG6hbjI7hn=mCw=Y)a<+9XzXSDbivWs?Ri@Hxu_hD#(9Xz$`AhLaB1ww z9!L4aNc-&uIYHuRZKTYz@(Sr=8p#_cgLLYVlS;xXCx_&NBO&?XWRQGvQb@izYosc0 ogb;!~e(=Z@ZLx_>Y~r273o)(x$7D6yi?CW)SOx@*jj63Xfe0Q!Q`seF5PPqXKbKIvLDHCQ)Y{TU1*2dZd2K?N zB(pjpGs{4JZ#m0m9zJH9Op++2lyZp*E&7ZI(TF}RD$8)H{5<1{%15!W?B=BR7!I5?_LIOaHz=0tqCLKmrLQkU#>h9f(>~AWr$Ysvsq7) zXOJ<4>zVa!o>{CD7&JG9#&kX97JKIbK?_qDb2NE!Nl{6o}^@ z+}wX4>+8?X%D0m9*pG7~R1!i60aKVt?jo>)mE$mL##@@2q2RiMg{VkD-6FfkDgM*_P5Fzd;EdxkHEijuM&^ z`Sm44etij%UtdDx*Ow6aTU8}Q{)tcwi%N+62G@or0{f4#j$1L`LjJ|adKvi)^<%o> zm_H)FKf8F=G424rbh8)vXjuXV*f1Jvk>9X{p7M_{6Zwrw=qVo-BEMY;(>>)6BftAq za~iuE6ZzeIz@wAvo?$og3*;YjT0-O($Zu6b4>+{FJboe=ih*+jxad>|qtpVbUp_aW+?Y_=Gh#Hi{>FFyUBy^^Ek2_Ja>T%0_gaOfcAR!7XW{SIa{srM;iFn)pWV zoj$Glo#om0yYEfD{a^j%$=plD!kkN|1bJzSN;EGy@gUKvpGTAJzgf*B-I_;d(z{c> zKAxZCygXFs#+Ia-hO@8s{oZHFu)k}s!>L&x8H1)WUkYQqG?m$AHACFfhDkm4_5XhT zWa3b0V8A3CbQvWIr4CrwT|US3;*sEo7sWyj%Hlg`$6kJ;l(73j+3snK8OtBHi*8mm zpRtD%J0{N=uH4E>{9gt$MZoD47^KrF#qV++A22Z=$HKJ0#$#BkwU?FkR=PL|+5+rrs+m{&ZvC|SZk zP0MP{gk?u{5~lutcR-<`^Uvbf_Os1d7R**(v?4>@g>6Bn_o7t^>Mrazluz;X{uk@g zVq_6;U|>vQv#9@Y_2=T_=j8vD{r$1`v%bTX1KJwr_Ond6&;4#+8#w4gAJ;N!{a^5s U&3v0aFt!;yUHx3vIVCg!06`$^I{*Lx literal 0 HcmV?d00001 diff --git a/plugin/lib/nc_photos_plugin.dart b/plugin/lib/nc_photos_plugin.dart index 8ecc08c5..f3ad3482 100644 --- a/plugin/lib/nc_photos_plugin.dart +++ b/plugin/lib/nc_photos_plugin.dart @@ -1,6 +1,7 @@ library nc_photos_plugin; export 'src/exception.dart'; +export 'src/image_processor.dart'; export 'src/lock.dart'; export 'src/media_store.dart'; export 'src/native_event.dart'; diff --git a/plugin/lib/src/image_processor.dart b/plugin/lib/src/image_processor.dart new file mode 100644 index 00000000..f47568b0 --- /dev/null +++ b/plugin/lib/src/image_processor.dart @@ -0,0 +1,15 @@ +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:nc_photos_plugin/src/k.dart' as k; + +class ImageProcessor { + static Future zeroDce(String image, String filename) => + _methodChannel.invokeMethod("zeroDce", { + "image": image, + "filename": filename, + }); + + static const _methodChannel = + MethodChannel("${k.libId}/image_processor_method"); +}