Add image enhancement algorithm: ZeroDCE
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -1,3 +1,11 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.nkming.nc_photos.plugin">
|
||||
<manifest package="com.nkming.nc_photos.plugin"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
<application>
|
||||
<service
|
||||
android:name=".ImageProcessorService"
|
||||
android:exported="false" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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<ImageProcessorCommand>()
|
||||
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<String, Any> = mapOf(),
|
||||
)
|
||||
|
||||
@Suppress("Deprecation")
|
||||
private open class ImageProcessorCommandTask(context: Context) :
|
||||
AsyncTask<ImageProcessorCommand, Unit, MessageEvent>() {
|
||||
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
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
After Width: | Height: | Size: 400 B |
After Width: | Height: | Size: 253 B |
After Width: | Height: | Size: 254 B |
After Width: | Height: | Size: 177 B |
After Width: | Height: | Size: 410 B |
After Width: | Height: | Size: 295 B |
After Width: | Height: | Size: 654 B |
After Width: | Height: | Size: 407 B |
After Width: | Height: | Size: 749 B |
After Width: | Height: | Size: 532 B |
|
@ -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';
|
||||
|
|
15
plugin/lib/src/image_processor.dart
Normal file
|
@ -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<void> zeroDce(String image, String filename) =>
|
||||
_methodChannel.invokeMethod("zeroDce", <String, dynamic>{
|
||||
"image": image,
|
||||
"filename": filename,
|
||||
});
|
||||
|
||||
static const _methodChannel =
|
||||
MethodChannel("${k.libId}/image_processor_method");
|
||||
}
|