Add image enhancement algorithm: ZeroDCE

This commit is contained in:
Ming Ming 2022-05-04 01:19:47 +08:00
parent 4beb93c552
commit e343e59741
24 changed files with 722 additions and 2 deletions

View file

@ -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'
}

View file

@ -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>

View file

@ -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"
}
}

View file

@ -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

View file

@ -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
}

View file

@ -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
}

View file

@ -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"
}
}

View file

@ -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

View file

@ -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
}

View file

@ -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
}
}
}

View file

@ -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
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 749 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 B

View file

@ -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';

View 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");
}