mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-23 01:06:21 +01:00
Merge branch 'photo-enhancement-style-transfer' into dev
This commit is contained in:
commit
3caa2ceecb
26 changed files with 760 additions and 17 deletions
|
@ -7,3 +7,4 @@ const enhanceUrl = "https://bit.ly/3lF5OiT";
|
||||||
const enhanceZeroDceUrl = "https://bit.ly/3wKJcm9";
|
const enhanceZeroDceUrl = "https://bit.ly/3wKJcm9";
|
||||||
const enhanceDeepLabPortraitBlurUrl = "https://bit.ly/3wIuXy6";
|
const enhanceDeepLabPortraitBlurUrl = "https://bit.ly/3wIuXy6";
|
||||||
const enhanceEsrganUrl = "https://bit.ly/3wO0NJP";
|
const enhanceEsrganUrl = "https://bit.ly/3wO0NJP";
|
||||||
|
const enhanceStyleTransferUrl = "https://bit.ly/3agpTcF";
|
||||||
|
|
|
@ -1213,6 +1213,10 @@
|
||||||
"@enhanceSuperResolution4xTitle": {
|
"@enhanceSuperResolution4xTitle": {
|
||||||
"description": "Upscale an image. The algorithm implemented in the app will upscale to 4x the original resolution (eg, 100x100 to 400x400)"
|
"description": "Upscale an image. The algorithm implemented in the app will upscale to 4x the original resolution (eg, 100x100 to 400x400)"
|
||||||
},
|
},
|
||||||
|
"enhanceStyleTransferTitle": "Style transfer",
|
||||||
|
"@enhanceStyleTransferTitle": {
|
||||||
|
"description": "Transfer the image style from a reference image to 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": {
|
||||||
|
|
|
@ -99,6 +99,7 @@
|
||||||
"enhancePortraitBlurTitle",
|
"enhancePortraitBlurTitle",
|
||||||
"enhancePortraitBlurParamBlurLabel",
|
"enhancePortraitBlurParamBlurLabel",
|
||||||
"enhanceSuperResolution4xTitle",
|
"enhanceSuperResolution4xTitle",
|
||||||
|
"enhanceStyleTransferTitle",
|
||||||
"errorAlbumDowngrade"
|
"errorAlbumDowngrade"
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -216,6 +217,7 @@
|
||||||
"enhancePortraitBlurTitle",
|
"enhancePortraitBlurTitle",
|
||||||
"enhancePortraitBlurParamBlurLabel",
|
"enhancePortraitBlurParamBlurLabel",
|
||||||
"enhanceSuperResolution4xTitle",
|
"enhanceSuperResolution4xTitle",
|
||||||
|
"enhanceStyleTransferTitle",
|
||||||
"errorAlbumDowngrade"
|
"errorAlbumDowngrade"
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -388,6 +390,7 @@
|
||||||
"enhancePortraitBlurTitle",
|
"enhancePortraitBlurTitle",
|
||||||
"enhancePortraitBlurParamBlurLabel",
|
"enhancePortraitBlurParamBlurLabel",
|
||||||
"enhanceSuperResolution4xTitle",
|
"enhanceSuperResolution4xTitle",
|
||||||
|
"enhanceStyleTransferTitle",
|
||||||
"errorAlbumDowngrade"
|
"errorAlbumDowngrade"
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -410,11 +413,13 @@
|
||||||
"deletePermanentlyLocalConfirmationDialogContent",
|
"deletePermanentlyLocalConfirmationDialogContent",
|
||||||
"enhancePortraitBlurTitle",
|
"enhancePortraitBlurTitle",
|
||||||
"enhancePortraitBlurParamBlurLabel",
|
"enhancePortraitBlurParamBlurLabel",
|
||||||
"enhanceSuperResolution4xTitle"
|
"enhanceSuperResolution4xTitle",
|
||||||
|
"enhanceStyleTransferTitle"
|
||||||
],
|
],
|
||||||
|
|
||||||
"fi": [
|
"fi": [
|
||||||
"enhanceSuperResolution4xTitle"
|
"enhanceSuperResolution4xTitle",
|
||||||
|
"enhanceStyleTransferTitle"
|
||||||
],
|
],
|
||||||
|
|
||||||
"fr": [
|
"fr": [
|
||||||
|
@ -436,7 +441,8 @@
|
||||||
"deletePermanentlyLocalConfirmationDialogContent",
|
"deletePermanentlyLocalConfirmationDialogContent",
|
||||||
"enhancePortraitBlurTitle",
|
"enhancePortraitBlurTitle",
|
||||||
"enhancePortraitBlurParamBlurLabel",
|
"enhancePortraitBlurParamBlurLabel",
|
||||||
"enhanceSuperResolution4xTitle"
|
"enhanceSuperResolution4xTitle",
|
||||||
|
"enhanceStyleTransferTitle"
|
||||||
],
|
],
|
||||||
|
|
||||||
"pl": [
|
"pl": [
|
||||||
|
@ -475,7 +481,8 @@
|
||||||
"deletePermanentlyLocalConfirmationDialogContent",
|
"deletePermanentlyLocalConfirmationDialogContent",
|
||||||
"enhancePortraitBlurTitle",
|
"enhancePortraitBlurTitle",
|
||||||
"enhancePortraitBlurParamBlurLabel",
|
"enhancePortraitBlurParamBlurLabel",
|
||||||
"enhanceSuperResolution4xTitle"
|
"enhanceSuperResolution4xTitle",
|
||||||
|
"enhanceStyleTransferTitle"
|
||||||
],
|
],
|
||||||
|
|
||||||
"pt": [
|
"pt": [
|
||||||
|
@ -493,7 +500,8 @@
|
||||||
"deletePermanentlyLocalConfirmationDialogContent",
|
"deletePermanentlyLocalConfirmationDialogContent",
|
||||||
"enhancePortraitBlurTitle",
|
"enhancePortraitBlurTitle",
|
||||||
"enhancePortraitBlurParamBlurLabel",
|
"enhancePortraitBlurParamBlurLabel",
|
||||||
"enhanceSuperResolution4xTitle"
|
"enhanceSuperResolution4xTitle",
|
||||||
|
"enhanceStyleTransferTitle"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ru": [
|
"ru": [
|
||||||
|
@ -511,7 +519,8 @@
|
||||||
"deletePermanentlyLocalConfirmationDialogContent",
|
"deletePermanentlyLocalConfirmationDialogContent",
|
||||||
"enhancePortraitBlurTitle",
|
"enhancePortraitBlurTitle",
|
||||||
"enhancePortraitBlurParamBlurLabel",
|
"enhancePortraitBlurParamBlurLabel",
|
||||||
"enhanceSuperResolution4xTitle"
|
"enhanceSuperResolution4xTitle",
|
||||||
|
"enhanceStyleTransferTitle"
|
||||||
],
|
],
|
||||||
|
|
||||||
"zh": [
|
"zh": [
|
||||||
|
@ -529,7 +538,8 @@
|
||||||
"deletePermanentlyLocalConfirmationDialogContent",
|
"deletePermanentlyLocalConfirmationDialogContent",
|
||||||
"enhancePortraitBlurTitle",
|
"enhancePortraitBlurTitle",
|
||||||
"enhancePortraitBlurParamBlurLabel",
|
"enhancePortraitBlurParamBlurLabel",
|
||||||
"enhanceSuperResolution4xTitle"
|
"enhanceSuperResolution4xTitle",
|
||||||
|
"enhanceStyleTransferTitle"
|
||||||
],
|
],
|
||||||
|
|
||||||
"zh_Hant": [
|
"zh_Hant": [
|
||||||
|
@ -547,6 +557,7 @@
|
||||||
"deletePermanentlyLocalConfirmationDialogContent",
|
"deletePermanentlyLocalConfirmationDialogContent",
|
||||||
"enhancePortraitBlurTitle",
|
"enhancePortraitBlurTitle",
|
||||||
"enhancePortraitBlurParamBlurLabel",
|
"enhancePortraitBlurParamBlurLabel",
|
||||||
"enhanceSuperResolution4xTitle"
|
"enhanceSuperResolution4xTitle",
|
||||||
|
"enhanceStyleTransferTitle"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:memory_info/memory_info.dart';
|
||||||
|
import 'package:nc_photos/double_extension.dart';
|
||||||
|
|
||||||
/// System info for Android
|
/// System info for Android
|
||||||
///
|
///
|
||||||
|
@ -7,23 +10,38 @@ import 'package:device_info_plus/device_info_plus.dart';
|
||||||
class AndroidInfo {
|
class AndroidInfo {
|
||||||
factory AndroidInfo() => _inst;
|
factory AndroidInfo() => _inst;
|
||||||
|
|
||||||
AndroidInfo._({
|
const AndroidInfo._({
|
||||||
required this.sdkInt,
|
required this.sdkInt,
|
||||||
|
required this.totalMemMb,
|
||||||
});
|
});
|
||||||
|
|
||||||
static Future<void> init() async {
|
static Future<void> init() async {
|
||||||
final info = await DeviceInfoPlugin().androidInfo;
|
final info = await DeviceInfoPlugin().androidInfo;
|
||||||
final sdkInt = info.version.sdkInt!;
|
final sdkInt = info.version.sdkInt!;
|
||||||
|
|
||||||
|
final memInfo = await MemoryInfoPlugin().memoryInfo;
|
||||||
|
final totalMemMb = memInfo.totalMem!.toDouble();
|
||||||
|
|
||||||
_inst = AndroidInfo._(
|
_inst = AndroidInfo._(
|
||||||
sdkInt: sdkInt,
|
sdkInt: sdkInt,
|
||||||
|
totalMemMb: totalMemMb,
|
||||||
);
|
);
|
||||||
|
_log.info("[init] $_inst");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() => "$runtimeType {"
|
||||||
|
"sdkInt: $sdkInt, "
|
||||||
|
"totalMemMb: ${totalMemMb.toStringAsFixedTruncated(2)}, "
|
||||||
|
"}";
|
||||||
|
|
||||||
static late final AndroidInfo _inst;
|
static late final AndroidInfo _inst;
|
||||||
|
|
||||||
/// Corresponding to Build.VERSION.SDK_INT
|
/// Corresponding to Build.VERSION.SDK_INT
|
||||||
final int sdkInt;
|
final int sdkInt;
|
||||||
|
final double totalMemMb;
|
||||||
|
|
||||||
|
static final _log = Logger("mobile.android.android_info.AndroidInfo");
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class AndroidVersion {
|
abstract class AndroidVersion {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
|
||||||
/// Standard activity result: operation canceled.
|
/// Standard activity result: operation canceled.
|
||||||
const resultCanceled = 0;
|
const resultCanceled = 0;
|
||||||
|
|
||||||
|
@ -6,3 +8,7 @@ const resultOk = -1;
|
||||||
|
|
||||||
/// Start of user-defined activity results.
|
/// Start of user-defined activity results.
|
||||||
const resultFirstUser = 1;
|
const resultFirstUser = 1;
|
||||||
|
|
||||||
|
const ACTION_GET_CONTENT = "android.intent.action.GET_CONTENT";
|
||||||
|
const CATEGORY_OPENABLE = "android.intent.category.OPENABLE";
|
||||||
|
const EXTRA_LOCAL_ONLY = "android.intent.extra.LOCAL_ONLY";
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:android_intent_plus/android_intent.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
|
@ -6,14 +9,18 @@ import 'package:nc_photos/app_localizations.dart';
|
||||||
import 'package:nc_photos/entity/file.dart';
|
import 'package:nc_photos/entity/file.dart';
|
||||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||||
import 'package:nc_photos/help_utils.dart';
|
import 'package:nc_photos/help_utils.dart';
|
||||||
|
import 'package:nc_photos/iterable_extension.dart';
|
||||||
import 'package:nc_photos/k.dart' as k;
|
import 'package:nc_photos/k.dart' as k;
|
||||||
import 'package:nc_photos/mobile/android/android_info.dart';
|
import 'package:nc_photos/mobile/android/android_info.dart';
|
||||||
|
import 'package:nc_photos/mobile/android/content_uri_image_provider.dart';
|
||||||
|
import 'package:nc_photos/mobile/android/k.dart' as android;
|
||||||
import 'package:nc_photos/mobile/android/permission_util.dart';
|
import 'package:nc_photos/mobile/android/permission_util.dart';
|
||||||
import 'package:nc_photos/object_extension.dart';
|
import 'package:nc_photos/object_extension.dart';
|
||||||
import 'package:nc_photos/platform/k.dart' as platform_k;
|
import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||||
import 'package:nc_photos/pref.dart';
|
import 'package:nc_photos/pref.dart';
|
||||||
import 'package:nc_photos/snack_bar_manager.dart';
|
import 'package:nc_photos/snack_bar_manager.dart';
|
||||||
import 'package:nc_photos/theme.dart';
|
import 'package:nc_photos/theme.dart';
|
||||||
|
import 'package:nc_photos/widget/selectable.dart';
|
||||||
import 'package:nc_photos/widget/settings.dart';
|
import 'package:nc_photos/widget/settings.dart';
|
||||||
import 'package:nc_photos/widget/stateful_slider.dart';
|
import 'package:nc_photos/widget/stateful_slider.dart';
|
||||||
import 'package:nc_photos_plugin/nc_photos_plugin.dart';
|
import 'package:nc_photos_plugin/nc_photos_plugin.dart';
|
||||||
|
@ -86,6 +93,22 @@ class EnhanceHandler {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case _Algorithm.arbitraryStyleTransfer:
|
||||||
|
await ImageProcessor.arbitraryStyleTransfer(
|
||||||
|
"${account.url}/${file.path}",
|
||||||
|
file.filename,
|
||||||
|
math.min(
|
||||||
|
Pref().getEnhanceMaxWidthOr(), isAtLeast5GbRam() ? 1600 : 1280),
|
||||||
|
math.min(
|
||||||
|
Pref().getEnhanceMaxHeightOr(), isAtLeast5GbRam() ? 1200 : 960),
|
||||||
|
args["styleUri"],
|
||||||
|
args["weight"],
|
||||||
|
headers: {
|
||||||
|
"Authorization": Api.getAuthorizationHeaderValue(account),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,6 +217,12 @@ class EnhanceHandler {
|
||||||
link: enhanceEsrganUrl,
|
link: enhanceEsrganUrl,
|
||||||
algorithm: _Algorithm.esrgan,
|
algorithm: _Algorithm.esrgan,
|
||||||
),
|
),
|
||||||
|
if (platform_k.isAndroid && isAtLeast4GbRam())
|
||||||
|
_Option(
|
||||||
|
title: L10n.global().enhanceStyleTransferTitle,
|
||||||
|
link: enhanceStyleTransferUrl,
|
||||||
|
algorithm: _Algorithm.arbitraryStyleTransfer,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
Future<Map<String, dynamic>?> _getArgs(
|
Future<Map<String, dynamic>?> _getArgs(
|
||||||
|
@ -207,6 +236,9 @@ class EnhanceHandler {
|
||||||
|
|
||||||
case _Algorithm.esrgan:
|
case _Algorithm.esrgan:
|
||||||
return {};
|
return {};
|
||||||
|
|
||||||
|
case _Algorithm.arbitraryStyleTransfer:
|
||||||
|
return _getArbitraryStyleTransferArgs(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -314,6 +346,32 @@ class EnhanceHandler {
|
||||||
return radius?.run((it) => {"radius": it});
|
return radius?.run((it) => {"radius": it});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>?> _getArbitraryStyleTransferArgs(
|
||||||
|
BuildContext context) async {
|
||||||
|
final result = await showDialog<_StylePickerResult>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => const _StylePicker(),
|
||||||
|
);
|
||||||
|
if (result == null) {
|
||||||
|
// user canceled
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
"styleUri": result.styleUri,
|
||||||
|
"weight": result.weight,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isAtLeast4GbRam() {
|
||||||
|
// We can't compare with 4096 directly as some RAM are preserved
|
||||||
|
return AndroidInfo().totalMemMb > 3584;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isAtLeast5GbRam() {
|
||||||
|
return AndroidInfo().totalMemMb > 4608;
|
||||||
|
}
|
||||||
|
|
||||||
final Account account;
|
final Account account;
|
||||||
final File file;
|
final File file;
|
||||||
|
|
||||||
|
@ -324,6 +382,7 @@ enum _Algorithm {
|
||||||
zeroDce,
|
zeroDce,
|
||||||
deepLab3Portrait,
|
deepLab3Portrait,
|
||||||
esrgan,
|
esrgan,
|
||||||
|
arbitraryStyleTransfer,
|
||||||
}
|
}
|
||||||
|
|
||||||
class _Option {
|
class _Option {
|
||||||
|
@ -339,3 +398,186 @@ class _Option {
|
||||||
final String? link;
|
final String? link;
|
||||||
final _Algorithm algorithm;
|
final _Algorithm algorithm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _StylePickerResult {
|
||||||
|
const _StylePickerResult(this.styleUri, this.weight);
|
||||||
|
|
||||||
|
final String styleUri;
|
||||||
|
final double weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StylePicker extends StatefulWidget {
|
||||||
|
const _StylePicker({
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
createState() => _StylePickerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StylePickerState extends State<_StylePicker> {
|
||||||
|
@override
|
||||||
|
build(BuildContext context) {
|
||||||
|
return AppTheme(
|
||||||
|
child: AlertDialog(
|
||||||
|
title: Text("Pick a style"),
|
||||||
|
contentPadding: const EdgeInsets.fromLTRB(24.0, 20.0, 24.0, 0),
|
||||||
|
content: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (_selected != null) ...[
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 128,
|
||||||
|
height: 128,
|
||||||
|
child: Image(
|
||||||
|
image: ResizeImage.resizeIfNeeded(
|
||||||
|
128, null, ContentUriImage(_getSelectedUri())),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
Wrap(
|
||||||
|
runSpacing: 8,
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
..._bundledStyles.mapWithIndex((i, e) => _buildItem(
|
||||||
|
i,
|
||||||
|
Image(
|
||||||
|
image: ResizeImage.resizeIfNeeded(
|
||||||
|
_thumbSize, null, ContentUriImage(e)),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
if (_customUri != null)
|
||||||
|
_buildItem(
|
||||||
|
_bundledStyles.length,
|
||||||
|
Image(
|
||||||
|
image: ResizeImage.resizeIfNeeded(
|
||||||
|
_thumbSize, null, ContentUriImage(_customUri!)),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
InkWell(
|
||||||
|
onTap: _onCustomTap,
|
||||||
|
child: SizedBox(
|
||||||
|
width: _thumbSize.toDouble(),
|
||||||
|
height: _thumbSize.toDouble(),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.file_open_outlined,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.auto_fix_normal,
|
||||||
|
color: AppTheme.getSecondaryTextColor(context),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: StatefulSlider(
|
||||||
|
initialValue: _weight,
|
||||||
|
min: .01,
|
||||||
|
onChangeEnd: (value) {
|
||||||
|
_weight = value;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Icon(
|
||||||
|
Icons.auto_fix_high,
|
||||||
|
color: AppTheme.getSecondaryTextColor(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (_selected == null) {
|
||||||
|
SnackBarManager().showSnackBar(const SnackBar(
|
||||||
|
content: Text("Please pick a style"),
|
||||||
|
duration: k.snackBarDurationNormal,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
final result = _StylePickerResult(_getSelectedUri(), _weight);
|
||||||
|
Navigator.of(context).pop(result);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text(L10n.global().enhanceButtonLabel),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildItem(int index, Widget child) {
|
||||||
|
return SizedBox(
|
||||||
|
width: _thumbSize.toDouble(),
|
||||||
|
height: _thumbSize.toDouble(),
|
||||||
|
child: Selectable(
|
||||||
|
isSelected: _selected == index,
|
||||||
|
iconSize: 24,
|
||||||
|
child: child,
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
_selected = index;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onCustomTap() async {
|
||||||
|
const intent = AndroidIntent(
|
||||||
|
action: android.ACTION_GET_CONTENT,
|
||||||
|
type: "image/*",
|
||||||
|
category: android.CATEGORY_OPENABLE,
|
||||||
|
arguments: {
|
||||||
|
android.EXTRA_LOCAL_ONLY: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final result = await intent.launchForResult();
|
||||||
|
_log.info("[onCustomTap] Intent result: $result");
|
||||||
|
if (result?.resultCode == android.resultOk) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_customUri = result!.data;
|
||||||
|
_selected = _bundledStyles.length;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getSelectedUri() {
|
||||||
|
return _selected! < _bundledStyles.length
|
||||||
|
? _bundledStyles[_selected!]
|
||||||
|
: _customUri!;
|
||||||
|
}
|
||||||
|
|
||||||
|
int? _selected;
|
||||||
|
String? _customUri;
|
||||||
|
double _weight = .85;
|
||||||
|
|
||||||
|
static const _thumbSize = 56;
|
||||||
|
static const _bundledStyles = [
|
||||||
|
"file:///android_asset/tf/arbitrary-style-transfer/1.jpg",
|
||||||
|
"file:///android_asset/tf/arbitrary-style-transfer/2.jpg",
|
||||||
|
"file:///android_asset/tf/arbitrary-style-transfer/3.jpg",
|
||||||
|
"file:///android_asset/tf/arbitrary-style-transfer/4.jpg",
|
||||||
|
"file:///android_asset/tf/arbitrary-style-transfer/5.jpg",
|
||||||
|
"file:///android_asset/tf/arbitrary-style-transfer/6.jpg",
|
||||||
|
];
|
||||||
|
|
||||||
|
static final _log =
|
||||||
|
Logger("widget.handler.enhance_handler._StylePickerState");
|
||||||
|
}
|
||||||
|
|
|
@ -18,10 +18,12 @@ packages:
|
||||||
android_intent_plus:
|
android_intent_plus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: android_intent_plus
|
path: "packages/android_intent_plus"
|
||||||
url: "https://pub.dartlang.org"
|
ref: "android_intent_plus-v3.1.1-nc-photos-1"
|
||||||
source: hosted
|
resolved-ref: f257f10641e907a94b83ff0e060e80590bf1dae5
|
||||||
version: "3.1.0"
|
url: "https://gitlab.com/nc-photos/plus_plugins"
|
||||||
|
source: git
|
||||||
|
version: "3.1.1"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -605,6 +607,20 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.3"
|
version: "0.1.3"
|
||||||
|
memory_info:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: memory_info
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.2"
|
||||||
|
memory_info_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: memory_info_platform_interface
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.1"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -28,7 +28,11 @@ dependencies:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
# android only
|
# android only
|
||||||
android_intent_plus: ^3.0.1
|
android_intent_plus:
|
||||||
|
git:
|
||||||
|
url: https://gitlab.com/nc-photos/plus_plugins
|
||||||
|
ref: android_intent_plus-v3.1.1-nc-photos-1
|
||||||
|
path: packages/android_intent_plus
|
||||||
battery_plus: ^2.1.3
|
battery_plus: ^2.1.3
|
||||||
bloc: ^7.0.0
|
bloc: ^7.0.0
|
||||||
cached_network_image: ^3.0.0
|
cached_network_image: ^3.0.0
|
||||||
|
@ -71,6 +75,7 @@ dependencies:
|
||||||
intl: ^0.17.0
|
intl: ^0.17.0
|
||||||
kiwi: ^4.0.1
|
kiwi: ^4.0.1
|
||||||
logging: ^1.0.1
|
logging: ^1.0.1
|
||||||
|
memory_info: ^0.0.2
|
||||||
mime: ^1.0.1
|
mime: ^1.0.1
|
||||||
mutex: ^3.0.0
|
mutex: ^3.0.0
|
||||||
native_device_orientation: ^1.0.0
|
native_device_orientation: ^1.0.0
|
||||||
|
|
Binary file not shown.
Binary file not shown.
BIN
plugin/android/src/main/assets/tf/arbitrary-style-transfer/1.jpg
Normal file
BIN
plugin/android/src/main/assets/tf/arbitrary-style-transfer/1.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 58 KiB |
BIN
plugin/android/src/main/assets/tf/arbitrary-style-transfer/2.jpg
Normal file
BIN
plugin/android/src/main/assets/tf/arbitrary-style-transfer/2.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 49 KiB |
BIN
plugin/android/src/main/assets/tf/arbitrary-style-transfer/3.jpg
Normal file
BIN
plugin/android/src/main/assets/tf/arbitrary-style-transfer/3.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
BIN
plugin/android/src/main/assets/tf/arbitrary-style-transfer/4.jpg
Normal file
BIN
plugin/android/src/main/assets/tf/arbitrary-style-transfer/4.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 50 KiB |
BIN
plugin/android/src/main/assets/tf/arbitrary-style-transfer/5.jpg
Normal file
BIN
plugin/android/src/main/assets/tf/arbitrary-style-transfer/5.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
BIN
plugin/android/src/main/assets/tf/arbitrary-style-transfer/6.jpg
Normal file
BIN
plugin/android/src/main/assets/tf/arbitrary-style-transfer/6.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.9 KiB |
|
@ -33,6 +33,7 @@ add_library( # Sets the name of the library.
|
||||||
SHARED
|
SHARED
|
||||||
|
|
||||||
# Provides a relative path to your source file(s).
|
# Provides a relative path to your source file(s).
|
||||||
|
arbitrary_style_transfer.cpp
|
||||||
deep_lap_3.cpp
|
deep_lap_3.cpp
|
||||||
esrgan.cpp
|
esrgan.cpp
|
||||||
exception.cpp
|
exception.cpp
|
||||||
|
|
223
plugin/android/src/main/cpp/arbitrary_style_transfer.cpp
Normal file
223
plugin/android/src/main/cpp/arbitrary_style_transfer.cpp
Normal file
|
@ -0,0 +1,223 @@
|
||||||
|
#include "base_resample.h"
|
||||||
|
#include "exception.h"
|
||||||
|
#include "log.h"
|
||||||
|
#include "stopwatch.h"
|
||||||
|
#include "tflite_wrapper.h"
|
||||||
|
#include "util.h"
|
||||||
|
#include <android/asset_manager.h>
|
||||||
|
#include <android/asset_manager_jni.h>
|
||||||
|
#include <cassert>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <exception>
|
||||||
|
#include <jni.h>
|
||||||
|
#include <tensorflow/lite/c/c_api.h>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
using namespace plugin;
|
||||||
|
using namespace std;
|
||||||
|
using namespace tflite;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr const char *PREDICT_MODEL =
|
||||||
|
"tf/arbitrary-style-transfer-inceptionv3_dr_predict_1.tflite";
|
||||||
|
constexpr const char *TRANSFER_MODEL =
|
||||||
|
"tf/arbitrary-style-transfer-inceptionv3_dr_transfer_1.tflite";
|
||||||
|
|
||||||
|
class ArbitraryStyleTransfer {
|
||||||
|
public:
|
||||||
|
explicit ArbitraryStyleTransfer(AAssetManager *const aam);
|
||||||
|
|
||||||
|
std::vector<uint8_t> infer(const uint8_t *image, const size_t width,
|
||||||
|
const size_t height, const uint8_t *style,
|
||||||
|
const float weight);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<float> predict(const uint8_t *image, const size_t width,
|
||||||
|
const size_t height, const uint8_t *style,
|
||||||
|
const float weight);
|
||||||
|
std::vector<uint8_t> transfer(const uint8_t *image, const size_t width,
|
||||||
|
const size_t height,
|
||||||
|
const std::vector<float> &bottleneck);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param style The style image MUST be 256*256
|
||||||
|
*/
|
||||||
|
std::vector<float> predictStyle(const uint8_t *style);
|
||||||
|
|
||||||
|
std::vector<float> blendBottleneck(const std::vector<float> &style,
|
||||||
|
const std::vector<float> &image,
|
||||||
|
const float styleWeight);
|
||||||
|
|
||||||
|
Model predictModel;
|
||||||
|
Model transferModel;
|
||||||
|
|
||||||
|
static constexpr const char *TAG = "ArbitraryStyleTransfer";
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
extern "C" JNIEXPORT jbyteArray JNICALL
|
||||||
|
Java_com_nkming_nc_1photos_plugin_image_1processor_ArbitraryStyleTransfer_inferNative(
|
||||||
|
JNIEnv *env, jobject *thiz, jobject assetManager, jbyteArray image,
|
||||||
|
jint width, jint height, jbyteArray style, jfloat weight) {
|
||||||
|
try {
|
||||||
|
initOpenMp();
|
||||||
|
auto aam = AAssetManager_fromJava(env, assetManager);
|
||||||
|
ArbitraryStyleTransfer model(aam);
|
||||||
|
RaiiContainer<jbyte> cImage(
|
||||||
|
[&]() { return env->GetByteArrayElements(image, nullptr); },
|
||||||
|
[&](jbyte *obj) {
|
||||||
|
env->ReleaseByteArrayElements(image, obj, JNI_ABORT);
|
||||||
|
});
|
||||||
|
RaiiContainer<jbyte> cStyle(
|
||||||
|
[&]() { return env->GetByteArrayElements(style, nullptr); },
|
||||||
|
[&](jbyte *obj) {
|
||||||
|
env->ReleaseByteArrayElements(style, obj, JNI_ABORT);
|
||||||
|
});
|
||||||
|
const auto result =
|
||||||
|
model.infer(reinterpret_cast<uint8_t *>(cImage.get()), width, height,
|
||||||
|
reinterpret_cast<uint8_t *>(cStyle.get()), weight);
|
||||||
|
auto resultAry = env->NewByteArray(result.size());
|
||||||
|
env->SetByteArrayRegion(resultAry, 0, result.size(),
|
||||||
|
reinterpret_cast<const int8_t *>(result.data()));
|
||||||
|
return resultAry;
|
||||||
|
} catch (const exception &e) {
|
||||||
|
throwJavaException(env, e.what());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
ArbitraryStyleTransfer::ArbitraryStyleTransfer(AAssetManager *const aam)
|
||||||
|
: predictModel(Asset(aam, PREDICT_MODEL)),
|
||||||
|
transferModel(Asset(aam, TRANSFER_MODEL)) {}
|
||||||
|
|
||||||
|
vector<uint8_t> ArbitraryStyleTransfer::infer(const uint8_t *image,
|
||||||
|
const size_t width,
|
||||||
|
const size_t height,
|
||||||
|
const uint8_t *style,
|
||||||
|
const float weight) {
|
||||||
|
const auto bottleneck = predict(image, width, height, style, weight);
|
||||||
|
return transfer(image, width, height, bottleneck);
|
||||||
|
}
|
||||||
|
|
||||||
|
vector<float> ArbitraryStyleTransfer::predict(const uint8_t *image,
|
||||||
|
const size_t width,
|
||||||
|
const size_t height,
|
||||||
|
const uint8_t *style,
|
||||||
|
const float weight) {
|
||||||
|
auto style_bottleneck = predictStyle(style);
|
||||||
|
vector<uint8_t> imageStyleBitmap(256 * 256 * 3);
|
||||||
|
base::ResampleImage24(image, width, height, imageStyleBitmap.data(), 256, 256,
|
||||||
|
base::KernelTypeLanczos3);
|
||||||
|
auto image_bottleneck = predictStyle(imageStyleBitmap.data());
|
||||||
|
return blendBottleneck(style_bottleneck, image_bottleneck, weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
vector<uint8_t>
|
||||||
|
ArbitraryStyleTransfer::transfer(const uint8_t *image, const size_t width,
|
||||||
|
const size_t height,
|
||||||
|
const vector<float> &bottleneck) {
|
||||||
|
vector<uint8_t> resizedImage;
|
||||||
|
auto inputWidth = width;
|
||||||
|
auto inputHeight = height;
|
||||||
|
const uint8_t *inputImage = image;
|
||||||
|
if (width % 4 != 0 || height % 4 != 0) {
|
||||||
|
LOGI(TAG, "[transfer] Resize bitmap to multiple of 4");
|
||||||
|
inputWidth = width - width % 4;
|
||||||
|
inputHeight = height - height % 4;
|
||||||
|
resizedImage.resize(inputWidth * inputHeight * 3);
|
||||||
|
base::ResampleImage24(image, width, height, resizedImage.data(), inputWidth,
|
||||||
|
inputHeight, base::KernelTypeLanczos3);
|
||||||
|
inputImage = resizedImage.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
InterpreterOptions options;
|
||||||
|
options.setNumThreads(getNumberOfProcessors());
|
||||||
|
Interpreter interpreter(transferModel, options);
|
||||||
|
const int dims[] = {1, static_cast<int>(inputHeight),
|
||||||
|
static_cast<int>(inputWidth), 3};
|
||||||
|
interpreter.resizeInputTensor(1, dims, 4);
|
||||||
|
interpreter.allocateTensors();
|
||||||
|
|
||||||
|
LOGI(TAG, "[transfer] Copy bias");
|
||||||
|
auto inputTensor0 = interpreter.getInputTensor(0);
|
||||||
|
assert(TfLiteTensorByteSize(inputTensor0) ==
|
||||||
|
bottleneck.size() * sizeof(float));
|
||||||
|
TfLiteTensorCopyFromBuffer(inputTensor0, bottleneck.data(),
|
||||||
|
bottleneck.size() * sizeof(float));
|
||||||
|
|
||||||
|
LOGI(TAG, "[transfer] Convert bitmap to input");
|
||||||
|
auto input = rgb8ToRgbFloat(inputImage, inputWidth * inputHeight * 3, true);
|
||||||
|
auto inputTensor1 = interpreter.getInputTensor(1);
|
||||||
|
assert(TfLiteTensorByteSize(inputTensor1) == input.size() * sizeof(float));
|
||||||
|
TfLiteTensorCopyFromBuffer(inputTensor1, input.data(),
|
||||||
|
input.size() * sizeof(float));
|
||||||
|
input.clear();
|
||||||
|
|
||||||
|
LOGI(TAG, "[transfer] Inferring");
|
||||||
|
Stopwatch stopwatch;
|
||||||
|
interpreter.invoke();
|
||||||
|
LOGI(TAG, "[transfer] Elapsed: %.3fs", stopwatch.getMs() / 1000.0f);
|
||||||
|
|
||||||
|
auto outputTensor = interpreter.getOutputTensor(0);
|
||||||
|
vector<float> output(inputWidth * inputHeight * 3);
|
||||||
|
assert(TfLiteTensorByteSize(outputTensor) == output.size() * sizeof(float));
|
||||||
|
TfLiteTensorCopyToBuffer(outputTensor, output.data(),
|
||||||
|
output.size() * sizeof(float));
|
||||||
|
auto outputRgb8 = rgbFloatToRgb8(output.data(), output.size(), true);
|
||||||
|
output.clear();
|
||||||
|
if (!resizedImage.empty()) {
|
||||||
|
// resize it back to the original resolution
|
||||||
|
vector<uint8_t> temp(width * height * 3);
|
||||||
|
base::ResampleImage24(outputRgb8.data(), inputWidth, inputHeight,
|
||||||
|
temp.data(), width, height, base::KernelTypeBicubic);
|
||||||
|
return temp;
|
||||||
|
} else {
|
||||||
|
return outputRgb8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vector<float> ArbitraryStyleTransfer::predictStyle(const uint8_t *style) {
|
||||||
|
InterpreterOptions options;
|
||||||
|
options.setNumThreads(getNumberOfProcessors());
|
||||||
|
Interpreter interpreter(predictModel, options);
|
||||||
|
interpreter.allocateTensors();
|
||||||
|
|
||||||
|
LOGI(TAG, "[predictStyle] Convert bitmap to input");
|
||||||
|
const auto input = rgb8ToRgbFloat(style, 256 * 256 * 3, true);
|
||||||
|
auto inputTensor = interpreter.getInputTensor(0);
|
||||||
|
assert(TfLiteTensorByteSize(inputTensor) == input.size() * sizeof(float));
|
||||||
|
TfLiteTensorCopyFromBuffer(inputTensor, input.data(),
|
||||||
|
input.size() * sizeof(float));
|
||||||
|
|
||||||
|
LOGI(TAG, "[predictStyle] Inferring");
|
||||||
|
Stopwatch stopwatch;
|
||||||
|
interpreter.invoke();
|
||||||
|
LOGI(TAG, "[predictStyle] Elapsed: %.3fs", stopwatch.getMs() / 1000.0f);
|
||||||
|
|
||||||
|
auto outputTensor = interpreter.getOutputTensor(0);
|
||||||
|
vector<float> output(100);
|
||||||
|
assert(TfLiteTensorByteSize(outputTensor) == output.size() * sizeof(float));
|
||||||
|
TfLiteTensorCopyToBuffer(outputTensor, output.data(),
|
||||||
|
output.size() * sizeof(float));
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
vector<float>
|
||||||
|
ArbitraryStyleTransfer::blendBottleneck(const vector<float> &style,
|
||||||
|
const vector<float> &image,
|
||||||
|
const float styleWeight) {
|
||||||
|
assert(style.size() == 100);
|
||||||
|
assert(image.size() == 100);
|
||||||
|
vector<float> product(100);
|
||||||
|
for (int i = 0; i < 100; ++i) {
|
||||||
|
product[i] = styleWeight * style[i] + (1 - styleWeight) * image[i];
|
||||||
|
}
|
||||||
|
return product;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
16
plugin/android/src/main/cpp/arbitrary_style_transfer.h
Normal file
16
plugin/android/src/main/cpp/arbitrary_style_transfer.h
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <jni.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
JNIEXPORT jbyteArray JNICALL
|
||||||
|
Java_com_nkming_nc_1photos_plugin_image_1processor_ArbitraryStyleTransfer_inferNative(
|
||||||
|
JNIEnv *env, jobject *thiz, jobject assetManager, jbyteArray image,
|
||||||
|
jint width, jint height, jbyteArray style, jfloat weight);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
|
@ -4,9 +4,31 @@ import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import java.io.InputStream
|
||||||
|
import kotlin.contracts.ExperimentalContracts
|
||||||
|
import kotlin.contracts.InvocationKind
|
||||||
|
import kotlin.contracts.contract
|
||||||
|
|
||||||
fun Bitmap.aspectRatio() = width / height.toFloat()
|
fun Bitmap.aspectRatio() = width / height.toFloat()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recycle the bitmap after @c block returns.
|
||||||
|
*
|
||||||
|
* @param block
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalContracts::class)
|
||||||
|
inline fun <T> Bitmap.use(block: (Bitmap) -> T): T {
|
||||||
|
contract {
|
||||||
|
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return block(this)
|
||||||
|
} finally {
|
||||||
|
recycle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum class BitmapResizeMethod {
|
enum class BitmapResizeMethod {
|
||||||
FIT,
|
FIT,
|
||||||
FILL,
|
FILL,
|
||||||
|
@ -113,10 +135,20 @@ interface BitmapUtil {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun openUriInputStream(
|
||||||
|
context: Context, uri: Uri
|
||||||
|
): InputStream? {
|
||||||
|
return if (UriUtil.isAssetUri(uri)) {
|
||||||
|
context.assets.open(UriUtil.getAssetUriPath(uri))
|
||||||
|
} else {
|
||||||
|
context.contentResolver.openInputStream(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun loadImageBounds(
|
private fun loadImageBounds(
|
||||||
context: Context, uri: Uri
|
context: Context, uri: Uri
|
||||||
): BitmapFactory.Options {
|
): BitmapFactory.Options {
|
||||||
context.contentResolver.openInputStream(uri)!!.use {
|
openUriInputStream(context, uri)!!.use {
|
||||||
val opt = BitmapFactory.Options().apply {
|
val opt = BitmapFactory.Options().apply {
|
||||||
inJustDecodeBounds = true
|
inJustDecodeBounds = true
|
||||||
}
|
}
|
||||||
|
@ -128,7 +160,7 @@ interface BitmapUtil {
|
||||||
private fun loadImage(
|
private fun loadImage(
|
||||||
context: Context, uri: Uri, opt: BitmapFactory.Options
|
context: Context, uri: Uri, opt: BitmapFactory.Options
|
||||||
): Bitmap {
|
): Bitmap {
|
||||||
context.contentResolver.openInputStream(uri)!!.use {
|
openUriInputStream(context, uri)!!.use {
|
||||||
return BitmapFactory.decodeStream(it, null, opt)!!
|
return BitmapFactory.decodeStream(it, null, opt)!!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,10 +31,15 @@ class ContentUriChannelHandler(context: Context) :
|
||||||
private fun readUri(uri: String, result: MethodChannel.Result) {
|
private fun readUri(uri: String, result: MethodChannel.Result) {
|
||||||
val uriTyped = Uri.parse(uri)
|
val uriTyped = Uri.parse(uri)
|
||||||
try {
|
try {
|
||||||
val bytes =
|
val bytes = if (UriUtil.isAssetUri(uriTyped)) {
|
||||||
|
context.assets.open(UriUtil.getAssetUriPath(uriTyped)).use {
|
||||||
|
it.readBytes()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
context.contentResolver.openInputStream(uriTyped)!!.use {
|
context.contentResolver.openInputStream(uriTyped)!!.use {
|
||||||
it.readBytes()
|
it.readBytes()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
result.success(bytes)
|
result.success(bytes)
|
||||||
} catch (e: FileNotFoundException) {
|
} catch (e: FileNotFoundException) {
|
||||||
result.error("fileNotFoundException", e.toString(), null)
|
result.error("fileNotFoundException", e.toString(), null)
|
||||||
|
|
|
@ -2,6 +2,7 @@ package com.nkming.nc_photos.plugin
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import io.flutter.plugin.common.EventChannel
|
import io.flutter.plugin.common.EventChannel
|
||||||
import io.flutter.plugin.common.MethodCall
|
import io.flutter.plugin.common.MethodCall
|
||||||
|
@ -64,6 +65,23 @@ class ImageProcessorChannelHandler(context: Context) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"arbitraryStyleTransfer" -> {
|
||||||
|
try {
|
||||||
|
arbitraryStyleTransfer(
|
||||||
|
call.argument("fileUrl")!!,
|
||||||
|
call.argument("headers"),
|
||||||
|
call.argument("filename")!!,
|
||||||
|
call.argument("maxWidth")!!,
|
||||||
|
call.argument("maxHeight")!!,
|
||||||
|
call.argument("styleUri")!!,
|
||||||
|
call.argument("weight")!!,
|
||||||
|
result
|
||||||
|
)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
result.error("systemException", e.toString(), null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
else -> result.notImplemented()
|
else -> result.notImplemented()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -105,6 +123,21 @@ class ImageProcessorChannelHandler(context: Context) :
|
||||||
ImageProcessorService.METHOD_ESRGAN, result
|
ImageProcessorService.METHOD_ESRGAN, result
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun arbitraryStyleTransfer(
|
||||||
|
fileUrl: String, headers: Map<String, String>?, filename: String,
|
||||||
|
maxWidth: Int, maxHeight: Int, styleUri: String, weight: Float,
|
||||||
|
result: MethodChannel.Result
|
||||||
|
) = method(
|
||||||
|
fileUrl, headers, filename, maxWidth, maxHeight,
|
||||||
|
ImageProcessorService.METHOD_ARBITRARY_STYLE_TRANSFER, result,
|
||||||
|
onIntent = {
|
||||||
|
it.putExtra(
|
||||||
|
ImageProcessorService.EXTRA_STYLE_URI, Uri.parse(styleUri)
|
||||||
|
)
|
||||||
|
it.putExtra(ImageProcessorService.EXTRA_WEIGHT, weight)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
private fun method(
|
private fun method(
|
||||||
fileUrl: String, headers: Map<String, String>?, filename: String,
|
fileUrl: String, headers: Map<String, String>?, filename: String,
|
||||||
maxWidth: Int, maxHeight: Int, method: String,
|
maxWidth: Int, maxHeight: Int, method: String,
|
||||||
|
|
|
@ -17,6 +17,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.ArbitraryStyleTransfer
|
||||||
import com.nkming.nc_photos.plugin.image_processor.DeepLab3Portrait
|
import com.nkming.nc_photos.plugin.image_processor.DeepLab3Portrait
|
||||||
import com.nkming.nc_photos.plugin.image_processor.Esrgan
|
import com.nkming.nc_photos.plugin.image_processor.Esrgan
|
||||||
import com.nkming.nc_photos.plugin.image_processor.ZeroDce
|
import com.nkming.nc_photos.plugin.image_processor.ZeroDce
|
||||||
|
@ -30,6 +31,7 @@ class ImageProcessorService : Service() {
|
||||||
const val METHOD_ZERO_DCE = "zero-dce"
|
const val METHOD_ZERO_DCE = "zero-dce"
|
||||||
const val METHOD_DEEP_LAP_PORTRAIT = "DeepLab3Portrait"
|
const val METHOD_DEEP_LAP_PORTRAIT = "DeepLab3Portrait"
|
||||||
const val METHOD_ESRGAN = "Esrgan"
|
const val METHOD_ESRGAN = "Esrgan"
|
||||||
|
const val METHOD_ARBITRARY_STYLE_TRANSFER = "ArbitraryStyleTransfer"
|
||||||
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"
|
||||||
|
@ -37,6 +39,8 @@ class ImageProcessorService : Service() {
|
||||||
const val EXTRA_MAX_HEIGHT = "maxHeight"
|
const val EXTRA_MAX_HEIGHT = "maxHeight"
|
||||||
const val EXTRA_RADIUS = "radius"
|
const val EXTRA_RADIUS = "radius"
|
||||||
const val EXTRA_ITERATION = "iteration"
|
const val EXTRA_ITERATION = "iteration"
|
||||||
|
const val EXTRA_STYLE_URI = "styleUri"
|
||||||
|
const val EXTRA_WEIGHT = "weight"
|
||||||
|
|
||||||
private const val ACTION_CANCEL = "cancel"
|
private const val ACTION_CANCEL = "cancel"
|
||||||
|
|
||||||
|
@ -109,6 +113,9 @@ class ImageProcessorService : Service() {
|
||||||
startId, intent.extras!!
|
startId, intent.extras!!
|
||||||
)
|
)
|
||||||
METHOD_ESRGAN -> onEsrgan(startId, intent.extras!!)
|
METHOD_ESRGAN -> onEsrgan(startId, intent.extras!!)
|
||||||
|
METHOD_ARBITRARY_STYLE_TRANSFER -> onArbitraryStyleTransfer(
|
||||||
|
startId, intent.extras!!
|
||||||
|
)
|
||||||
else -> {
|
else -> {
|
||||||
logE(TAG, "Unknown method: $method")
|
logE(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
|
||||||
|
@ -142,6 +149,16 @@ class ImageProcessorService : Service() {
|
||||||
return onMethod(startId, extras, METHOD_ESRGAN)
|
return onMethod(startId, extras, METHOD_ESRGAN)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onArbitraryStyleTransfer(startId: Int, extras: Bundle) {
|
||||||
|
return onMethod(
|
||||||
|
startId, extras, METHOD_ARBITRARY_STYLE_TRANSFER,
|
||||||
|
args = mapOf(
|
||||||
|
"styleUri" to extras.getParcelable<Uri>(EXTRA_STYLE_URI),
|
||||||
|
"weight" to extras.getFloat(EXTRA_WEIGHT),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle methods without arguments
|
* Handle methods without arguments
|
||||||
*
|
*
|
||||||
|
@ -530,6 +547,12 @@ private open class ImageProcessorCommandTask(context: Context) :
|
||||||
context, cmd.maxWidth, cmd.maxHeight
|
context, cmd.maxWidth, cmd.maxHeight
|
||||||
).infer(fileUri)
|
).infer(fileUri)
|
||||||
|
|
||||||
|
ImageProcessorService.METHOD_ARBITRARY_STYLE_TRANSFER -> ArbitraryStyleTransfer(
|
||||||
|
context, cmd.maxWidth, cmd.maxHeight,
|
||||||
|
cmd.args["styleUri"] as Uri,
|
||||||
|
cmd.args["weight"] as Float
|
||||||
|
).infer(fileUri)
|
||||||
|
|
||||||
else -> throw IllegalArgumentException(
|
else -> throw IllegalArgumentException(
|
||||||
"Unknown method: ${cmd.method}"
|
"Unknown method: ${cmd.method}"
|
||||||
)
|
)
|
||||||
|
|
|
@ -28,6 +28,24 @@ interface UriUtil {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asset URI is a non-standard Uri that points to an asset file.
|
||||||
|
*
|
||||||
|
* An asset URI is formatted as file:///android_asset/path/to/file
|
||||||
|
*
|
||||||
|
* @param uri
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
fun isAssetUri(uri: Uri): Boolean {
|
||||||
|
return uri.scheme == "file" && uri.path?.startsWith(
|
||||||
|
"/android_asset/"
|
||||||
|
) == true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAssetUriPath(uri: Uri): String {
|
||||||
|
return uri.path!!.substring("/android_asset/".length)
|
||||||
|
}
|
||||||
|
|
||||||
private const val TAG = "UriUtil"
|
private const val TAG = "UriUtil"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
package com.nkming.nc_photos.plugin.image_processor
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.AssetManager
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.net.Uri
|
||||||
|
import com.nkming.nc_photos.plugin.BitmapResizeMethod
|
||||||
|
import com.nkming.nc_photos.plugin.BitmapUtil
|
||||||
|
import com.nkming.nc_photos.plugin.logI
|
||||||
|
import com.nkming.nc_photos.plugin.use
|
||||||
|
|
||||||
|
class ArbitraryStyleTransfer(
|
||||||
|
context: Context, maxWidth: Int, maxHeight: Int, styleUri: Uri,
|
||||||
|
weight: Float
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
const val TAG = "ArbitraryStyleTransfer"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun infer(imageUri: Uri): Bitmap {
|
||||||
|
val width: Int
|
||||||
|
val height: Int
|
||||||
|
val rgb8Image = BitmapUtil.loadImage(
|
||||||
|
context, imageUri, maxWidth, maxHeight, BitmapResizeMethod.FIT,
|
||||||
|
isAllowSwapSide = true, shouldUpscale = false
|
||||||
|
).use {
|
||||||
|
width = it.width
|
||||||
|
height = it.height
|
||||||
|
TfLiteHelper.bitmapToRgb8Array(it)
|
||||||
|
}
|
||||||
|
val rgb8Style = BitmapUtil.loadImage(
|
||||||
|
context, styleUri, 256, 256, BitmapResizeMethod.FILL,
|
||||||
|
isAllowSwapSide = false, shouldUpscale = true
|
||||||
|
).use {
|
||||||
|
val styleBitmap = if (it.width != 256 || it.height != 256) {
|
||||||
|
val x = (it.width - 256) / 2
|
||||||
|
val y = (it.height - 256) / 2
|
||||||
|
logI(
|
||||||
|
TAG,
|
||||||
|
"[infer] Resize and crop style image: ${it.width}x${it.height} -> 256x256 ($x, $y)"
|
||||||
|
)
|
||||||
|
// crop
|
||||||
|
Bitmap.createBitmap(it, x, y, 256, 256)
|
||||||
|
} else {
|
||||||
|
it
|
||||||
|
}
|
||||||
|
styleBitmap.use {
|
||||||
|
TfLiteHelper.bitmapToRgb8Array(styleBitmap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val am = context.assets
|
||||||
|
|
||||||
|
return inferNative(
|
||||||
|
am, rgb8Image, width, height, rgb8Style, weight
|
||||||
|
).let {
|
||||||
|
TfLiteHelper.rgb8ArrayToBitmap(it, width, height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private external fun inferNative(
|
||||||
|
am: AssetManager, image: ByteArray, width: Int, height: Int,
|
||||||
|
style: ByteArray, weight: Float
|
||||||
|
): ByteArray
|
||||||
|
|
||||||
|
private val context = context
|
||||||
|
private val maxWidth = maxWidth
|
||||||
|
private val maxHeight = maxHeight
|
||||||
|
private val styleUri = styleUri
|
||||||
|
private val weight = weight
|
||||||
|
}
|
|
@ -53,6 +53,25 @@ class ImageProcessor {
|
||||||
"maxHeight": maxHeight,
|
"maxHeight": maxHeight,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
static Future<void> arbitraryStyleTransfer(
|
||||||
|
String fileUrl,
|
||||||
|
String filename,
|
||||||
|
int maxWidth,
|
||||||
|
int maxHeight,
|
||||||
|
String styleUri,
|
||||||
|
double weight, {
|
||||||
|
Map<String, String>? headers,
|
||||||
|
}) =>
|
||||||
|
_methodChannel.invokeMethod("arbitraryStyleTransfer", <String, dynamic>{
|
||||||
|
"fileUrl": fileUrl,
|
||||||
|
"headers": headers,
|
||||||
|
"filename": filename,
|
||||||
|
"maxWidth": maxWidth,
|
||||||
|
"maxHeight": maxHeight,
|
||||||
|
"styleUri": styleUri,
|
||||||
|
"weight": weight,
|
||||||
|
});
|
||||||
|
|
||||||
static const _methodChannel =
|
static const _methodChannel =
|
||||||
MethodChannel("${k.libId}/image_processor_method");
|
MethodChannel("${k.libId}/image_processor_method");
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue