Allow setting resolution of enhanced photos

This commit is contained in:
Ming Ming 2022-05-13 20:01:10 +08:00
parent 08a7e6f09d
commit e7a89c93af
11 changed files with 336 additions and 21 deletions

View file

@ -365,6 +365,13 @@
},
"settingsShowDateInAlbumTitle": "Group photos by date",
"settingsShowDateInAlbumDescription": "Apply only when the album is sorted by time",
"settingsPhotoEnhancementTitle": "Photo enhancement",
"settingsPhotoEnhancementPageTitle": "Photo enhancement settings",
"@settingsPhotoEnhancementPageTitle": {
"description": "Dedicated page for photo enhancement settings"
},
"settingsEnhanceMaxResolutionTitle": "Max output resolution",
"settingsEnhanceMaxResolutionDescription": "Photos larger than the selected resolution will be downscaled.\n\nHigh resolution photos require significantly more memory and time to process. Please lower this setting if the app crashed while enhancing your photos.",
"settingsThemeTitle": "Theme",
"settingsThemeDescription": "Customize the appearance of the app",
"settingsThemePageTitle": "Theme settings",

View file

@ -16,6 +16,10 @@
"settingsAlbumPageTitle",
"settingsShowDateInAlbumTitle",
"settingsShowDateInAlbumDescription",
"settingsPhotoEnhancementTitle",
"settingsPhotoEnhancementPageTitle",
"settingsEnhanceMaxResolutionTitle",
"settingsEnhanceMaxResolutionDescription",
"settingsExperimentalTitle",
"settingsExperimentalDescription",
"settingsExperimentalPageTitle",
@ -109,6 +113,10 @@
"settingsAlbumPageTitle",
"settingsShowDateInAlbumTitle",
"settingsShowDateInAlbumDescription",
"settingsPhotoEnhancementTitle",
"settingsPhotoEnhancementPageTitle",
"settingsEnhanceMaxResolutionTitle",
"settingsEnhanceMaxResolutionDescription",
"settingsExperimentalTitle",
"settingsExperimentalDescription",
"settingsExperimentalPageTitle",
@ -224,6 +232,10 @@
"settingsAlbumPageTitle",
"settingsShowDateInAlbumTitle",
"settingsShowDateInAlbumDescription",
"settingsPhotoEnhancementTitle",
"settingsPhotoEnhancementPageTitle",
"settingsEnhanceMaxResolutionTitle",
"settingsEnhanceMaxResolutionDescription",
"settingsThemeTitle",
"settingsThemeDescription",
"settingsThemePageTitle",
@ -362,6 +374,10 @@
],
"es": [
"settingsPhotoEnhancementTitle",
"settingsPhotoEnhancementPageTitle",
"settingsEnhanceMaxResolutionTitle",
"settingsEnhanceMaxResolutionDescription",
"rootPickerSkipConfirmationDialogContent2",
"helpButtonLabel",
"backgroundServiceStopping",
@ -374,6 +390,10 @@
],
"fi": [
"settingsPhotoEnhancementTitle",
"settingsPhotoEnhancementPageTitle",
"settingsEnhanceMaxResolutionTitle",
"settingsEnhanceMaxResolutionDescription",
"enhanceTooltip",
"enhanceLowLightTitle",
"collectionEnhancedPhotosLabel",
@ -383,6 +403,10 @@
"fr": [
"collectionsTooltip",
"settingsPhotoEnhancementTitle",
"settingsPhotoEnhancementPageTitle",
"settingsEnhanceMaxResolutionTitle",
"settingsEnhanceMaxResolutionDescription",
"helpTooltip",
"helpButtonLabel",
"removeFromAlbumTooltip",
@ -394,6 +418,10 @@
],
"pl": [
"settingsPhotoEnhancementTitle",
"settingsPhotoEnhancementPageTitle",
"settingsEnhanceMaxResolutionTitle",
"settingsEnhanceMaxResolutionDescription",
"createCollectionTooltip",
"createCollectionDialogAlbumLabel",
"createCollectionDialogAlbumDescription",
@ -423,6 +451,10 @@
],
"pt": [
"settingsPhotoEnhancementTitle",
"settingsPhotoEnhancementPageTitle",
"settingsEnhanceMaxResolutionTitle",
"settingsEnhanceMaxResolutionDescription",
"enhanceTooltip",
"enhanceLowLightTitle",
"collectionEnhancedPhotosLabel",
@ -431,6 +463,10 @@
],
"ru": [
"settingsPhotoEnhancementTitle",
"settingsPhotoEnhancementPageTitle",
"settingsEnhanceMaxResolutionTitle",
"settingsEnhanceMaxResolutionDescription",
"enhanceTooltip",
"enhanceLowLightTitle",
"collectionEnhancedPhotosLabel",

View file

@ -172,6 +172,20 @@ class Pref {
value,
(key, value) => provider.setBool(key, value));
int? getEnhanceMaxWidth() => provider.getInt(PrefKey.enhanceMaxWidth);
int getEnhanceMaxWidthOr([int def = 2048]) => getEnhanceMaxWidth() ?? def;
Future<bool> setEnhanceMaxWidth(int value) => _set<int>(
PrefKey.enhanceMaxWidth,
value,
(key, value) => provider.setInt(key, value));
int? getEnhanceMaxHeight() => provider.getInt(PrefKey.enhanceMaxHeight);
int getEnhanceMaxHeightOr([int def = 1536]) => getEnhanceMaxHeight() ?? def;
Future<bool> setEnhanceMaxHeight(int value) => _set<int>(
PrefKey.enhanceMaxHeight,
value,
(key, value) => provider.setInt(key, value));
Future<bool> _set<T>(PrefKey key, T value,
Future<bool> Function(PrefKey key, T value) setFn) async {
if (await setFn(key, value)) {
@ -461,6 +475,8 @@ enum PrefKey {
isAlbumBrowserShowDate,
gpsMapProvider,
hasShownSharedAlbumInfo,
enhanceMaxWidth,
enhanceMaxHeight,
// account pref
isEnableFaceRecognitionApp,
@ -515,6 +531,10 @@ extension on PrefKey {
return "gpsMapProvider";
case PrefKey.hasShownSharedAlbumInfo:
return "hasShownSharedAlbumInfo";
case PrefKey.enhanceMaxWidth:
return "enhanceMaxWidth";
case PrefKey.enhanceMaxHeight:
return "enhanceMaxHeight";
// account pref
case PrefKey.isEnableFaceRecognitionApp:

View file

@ -11,6 +11,7 @@ import 'package:nc_photos/mobile/android/android_info.dart';
import 'package:nc_photos/mobile/android/permission_util.dart';
import 'package:nc_photos/object_extension.dart';
import 'package:nc_photos/platform/k.dart' as platform_k;
import 'package:nc_photos/pref.dart';
import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos_plugin/nc_photos_plugin.dart';
import 'package:url_launcher/url_launcher.dart';
@ -67,6 +68,8 @@ class EnhanceHandler {
await ImageProcessor.zeroDce(
"${account.url}/${file.path}",
file.filename,
Pref().getEnhanceMaxWidthOr(),
Pref().getEnhanceMaxHeightOr(),
headers: {
"Authorization": Api.getAuthorizationHeaderValue(account),
},
@ -77,6 +80,8 @@ class EnhanceHandler {
await ImageProcessor.deepLab3Portrait(
"${account.url}/${file.path}",
file.filename,
Pref().getEnhanceMaxWidthOr(),
Pref().getEnhanceMaxHeightOr(),
headers: {
"Authorization": Api.getAuthorizationHeaderValue(account),
},

View file

@ -25,6 +25,7 @@ import 'package:nc_photos/widget/root_picker.dart';
import 'package:nc_photos/widget/share_folder_picker.dart';
import 'package:nc_photos/widget/stateful_slider.dart';
import 'package:screen_brightness/screen_brightness.dart';
import 'package:tuple/tuple.dart';
import 'package:url_launcher/url_launcher.dart';
class SettingsArguments {
@ -137,6 +138,15 @@ class _SettingsState extends State<Settings> {
description: L10n.global().settingsAlbumDescription,
builder: () => _AlbumSettings(),
),
_buildSubSettings(
context,
leading: Icon(
Icons.auto_fix_high_outlined,
color: AppTheme.getUnfocusedIconColor(context),
),
label: L10n.global().settingsPhotoEnhancementTitle,
builder: () => _EnhancementSettings(),
),
_buildSubSettings(
context,
leading: Icon(
@ -956,6 +966,216 @@ class _AlbumSettingsState extends State<_AlbumSettings> {
static final _log = Logger("widget.settings._AlbumSettingsState");
}
class _EnhancementSettings extends StatefulWidget {
@override
createState() => _EnhancementSettingsState();
}
class _EnhancementSettingsState extends State<_EnhancementSettings> {
@override
initState() {
super.initState();
_maxWidth = Pref().getEnhanceMaxWidthOr();
_maxHeight = Pref().getEnhanceMaxHeightOr();
}
@override
build(BuildContext context) {
return AppTheme(
child: Scaffold(
body: Builder(
builder: (context) => _buildContent(context),
),
),
);
}
Widget _buildContent(BuildContext context) {
return CustomScrollView(
slivers: [
SliverAppBar(
pinned: true,
title: Text(L10n.global().settingsPhotoEnhancementPageTitle),
),
SliverList(
delegate: SliverChildListDelegate(
[
ListTile(
title: Text(L10n.global().settingsEnhanceMaxResolutionTitle),
subtitle: Text("${_maxWidth}x$_maxHeight"),
onTap: () => _onMaxResolutionTap(context),
),
],
),
),
],
);
}
Future<void> _onMaxResolutionTap(BuildContext context) async {
var width = _maxWidth;
var height = _maxHeight;
final result = await showDialog<bool>(
context: context,
builder: (_) => AppTheme(
child: AlertDialog(
title: Text(L10n.global().settingsEnhanceMaxResolutionTitle),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(L10n.global().settingsEnhanceMaxResolutionDescription),
const SizedBox(height: 16),
_EnhanceResolutionSlider(
initialWidth: _maxWidth,
initialHeight: _maxHeight,
onChanged: (value) {
width = value.item1;
height = value.item2;
},
)
],
),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.of(context).pop(true);
},
child: Text(MaterialLocalizations.of(context).okButtonLabel),
),
],
),
),
);
if (result != true || (width == _maxWidth && height == _maxHeight)) {
return;
}
_setMaxResolution(width, height);
}
Future<void> _setMaxResolution(int width, int height) async {
_log.info(
"[_setMaxResolution] ${_maxWidth}x$_maxHeight -> ${width}x$height");
final oldWidth = _maxWidth;
final oldHeight = _maxHeight;
setState(() {
_maxWidth = width;
_maxHeight = height;
});
if (!await Pref().setEnhanceMaxWidth(width) ||
!await Pref().setEnhanceMaxHeight(height)) {
_log.severe("[_setMaxResolution] Failed writing pref");
SnackBarManager().showSnackBar(SnackBar(
content: Text(L10n.global().writePreferenceFailureNotification),
duration: k.snackBarDurationNormal,
));
await Pref().setEnhanceMaxWidth(oldWidth);
setState(() {
_maxWidth = oldWidth;
_maxHeight = oldHeight;
});
}
}
late int _maxWidth;
late int _maxHeight;
static final _log = Logger("widget.settings._EnhancementSettingsState");
}
class _EnhanceResolutionSlider extends StatefulWidget {
const _EnhanceResolutionSlider({
Key? key,
required this.initialWidth,
required this.initialHeight,
this.onChanged,
}) : super(key: key);
@override
createState() => _EnhanceResolutionSliderState();
final int initialWidth;
final int initialHeight;
final ValueChanged<Tuple2<int, int>>? onChanged;
}
class _EnhanceResolutionSliderState extends State<_EnhanceResolutionSlider> {
@override
initState() {
super.initState();
_width = widget.initialWidth;
_height = widget.initialHeight;
}
@override
build(BuildContext context) {
return Column(
children: [
Align(
alignment: Alignment.center,
child: Text("${_width}x$_height"),
),
StatefulSlider(
initialValue: resolutionToSliderValue(_width).toDouble(),
min: -3,
max: 3,
divisions: 6,
onChangeEnd: (value) async {
final resolution = sliderValueToResolution(value.toInt());
setState(() {
_width = resolution.item1;
_height = resolution.item2;
});
widget.onChanged?.call(resolution);
},
),
],
);
}
static Tuple2<int, int> sliderValueToResolution(int value) {
switch (value) {
case -3:
return const Tuple2(1024, 768);
case -2:
return const Tuple2(1280, 960);
case -1:
return const Tuple2(1600, 1200);
case 1:
return const Tuple2(2560, 1920);
case 2:
return const Tuple2(3200, 2400);
case 3:
return const Tuple2(4096, 3072);
default:
return const Tuple2(2048, 1536);
}
}
static int resolutionToSliderValue(int width) {
switch (width) {
case 1024:
return -3;
case 1280:
return -2;
case 1600:
return -1;
case 2560:
return 1;
case 3200:
return 2;
case 4096:
return 3;
default:
return 0;
}
}
late int _width;
late int _height;
}
class _ThemeSettings extends StatefulWidget {
@override
createState() => _ThemeSettingsState();

View file

@ -7,6 +7,7 @@ class StatefulSlider extends StatefulWidget {
required this.initialValue,
this.min = 0.0,
this.max = 1.0,
this.divisions,
this.onChangeEnd,
}) : super(key: key);
@ -16,6 +17,7 @@ class StatefulSlider extends StatefulWidget {
final double initialValue;
final double min;
final double max;
final int? divisions;
final ValueChanged<double>? onChangeEnd;
}
@ -32,6 +34,7 @@ class _StatefulSliderState extends State<StatefulSlider> {
value: _value,
min: widget.min,
max: widget.max,
divisions: widget.divisions,
onChanged: (value) {
setState(() {
_value = value;

View file

@ -23,6 +23,8 @@ class ImageProcessorChannelHandler(context: Context) :
call.argument("fileUrl")!!,
call.argument("headers"),
call.argument("filename")!!,
call.argument("maxWidth")!!,
call.argument("maxHeight")!!,
result
)
} catch (e: Throwable) {
@ -36,6 +38,8 @@ class ImageProcessorChannelHandler(context: Context) :
call.argument("fileUrl")!!,
call.argument("headers"),
call.argument("filename")!!,
call.argument("maxWidth")!!,
call.argument("maxHeight")!!,
result
)
} catch (e: Throwable) {
@ -57,23 +61,24 @@ class ImageProcessorChannelHandler(context: Context) :
private fun zeroDce(
fileUrl: String, headers: Map<String, String>?, filename: String,
result: MethodChannel.Result
maxWidth: Int, maxHeight: Int, result: MethodChannel.Result
) = method(
fileUrl, headers, filename, ImageProcessorService.METHOD_ZERO_DCE,
result
fileUrl, headers, filename, maxWidth, maxHeight,
ImageProcessorService.METHOD_ZERO_DCE, result
)
private fun deepLab3Portrait(
fileUrl: String, headers: Map<String, String>?, filename: String,
result: MethodChannel.Result
maxWidth: Int, maxHeight: Int, result: MethodChannel.Result
) = method(
fileUrl, headers, filename,
fileUrl, headers, filename, maxWidth, maxHeight,
ImageProcessorService.METHOD_DEEL_LAP_PORTRAIT, result
)
private fun method(
fileUrl: String, headers: Map<String, String>?, filename: String,
method: String, result: MethodChannel.Result
maxWidth: Int, maxHeight: Int, method: String,
result: MethodChannel.Result
) {
val intent = Intent(context, ImageProcessorService::class.java).apply {
putExtra(ImageProcessorService.EXTRA_METHOD, method)
@ -82,6 +87,8 @@ class ImageProcessorChannelHandler(context: Context) :
ImageProcessorService.EXTRA_HEADERS,
headers?.let { HashMap(it) })
putExtra(ImageProcessorService.EXTRA_FILENAME, filename)
putExtra(ImageProcessorService.EXTRA_MAX_WIDTH, maxWidth)
putExtra(ImageProcessorService.EXTRA_MAX_HEIGHT, maxHeight)
}
ContextCompat.startForegroundService(context, intent)
result.success(null)

View file

@ -32,6 +32,8 @@ class ImageProcessorService : Service() {
const val EXTRA_FILE_URL = "fileUrl"
const val EXTRA_HEADERS = "headers"
const val EXTRA_FILENAME = "filename"
const val EXTRA_MAX_WIDTH = "maxWidth"
const val EXTRA_MAX_HEIGHT = "maxHeight"
private const val ACTION_CANCEL = "cancel"
@ -100,7 +102,9 @@ class ImageProcessorService : Service() {
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", "", null, ""))
addCommand(
ImageProcessorCommand(startId, "null", "", null, "", 0, 0)
)
}
}
}
@ -125,9 +129,11 @@ class ImageProcessorService : Service() {
val headers =
extras.getSerializable(EXTRA_HEADERS) as HashMap<String, String>?
val filename = extras.getString(EXTRA_FILENAME)!!
val maxWidth = extras.getInt(EXTRA_MAX_WIDTH)
val maxHeight = extras.getInt(EXTRA_MAX_HEIGHT)
addCommand(
ImageProcessorCommand(
startId, method, fileUrl, headers, filename
startId, method, fileUrl, headers, filename, maxWidth, maxHeight
)
)
}
@ -273,6 +279,8 @@ private data class ImageProcessorCommand(
val fileUrl: String,
val headers: Map<String, String>?,
val filename: String,
val maxWidth: Int,
val maxHeight: Int,
val args: Map<String, Any> = mapOf(),
)
@ -412,11 +420,13 @@ private open class ImageProcessorCommandTask(context: Context) :
return try {
val fileUri = Uri.fromFile(file)
val output = when (cmd.method) {
ImageProcessorService.METHOD_ZERO_DCE -> ZeroDce(context).infer(
ImageProcessorService.METHOD_ZERO_DCE -> ZeroDce(
context, cmd.maxWidth, cmd.maxHeight
).infer(
fileUri
)
ImageProcessorService.METHOD_DEEL_LAP_PORTRAIT -> DeepLab3Portrait(
context
context, cmd.maxWidth, cmd.maxHeight
).infer(fileUri)
else -> throw IllegalArgumentException(
"Unknown method: ${cmd.method}"

View file

@ -71,11 +71,9 @@ private class DeepLab3(context: Context) {
private val context = context
}
class DeepLab3Portrait(context: Context) {
class DeepLab3Portrait(context: Context, maxWidth: Int, maxHeight: Int) {
companion object {
private const val RADIUS = 16
private const val MAX_WIDTH = 2048
private const val MAX_HEIGHT = 1536
private const val TAG = "DeepLab3Portrait"
}
@ -115,7 +113,7 @@ class DeepLab3Portrait(context: Context) {
Log.i(TAG, "[enhance] Enhancing image")
// downscale original to prevent OOM
val orig = BitmapUtil.loadImage(
context, imageUri, MAX_WIDTH, MAX_HEIGHT, BitmapResizeMethod.FIT,
context, imageUri, maxWidth, maxHeight, BitmapResizeMethod.FIT,
isAllowSwapSide = true, shouldUpscale = false
)
val bg = Toolkit.blur(orig, radius)
@ -146,5 +144,7 @@ class DeepLab3Portrait(context: Context) {
}
private val context = context
private val maxWidth = maxWidth
private val maxHeight = maxHeight
private val deepLab = DeepLab3(context)
}

View file

@ -10,16 +10,13 @@ import org.tensorflow.lite.Interpreter
import java.nio.FloatBuffer
import kotlin.math.pow
class ZeroDce(context: Context) {
class ZeroDce(context: Context, maxWidth: Int, maxHeight: Int) {
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
private const val MAX_WIDTH = 2048
private const val MAX_HEIGHT = 1536
}
fun infer(imageUri: Uri): Bitmap {
@ -54,7 +51,7 @@ class ZeroDce(context: Context) {
Log.i(TAG, "Enhancing image, iteration: $iteration")
// downscale original to prevent OOM
val resized = BitmapUtil.loadImage(
context, imageUri, MAX_WIDTH, MAX_HEIGHT, BitmapResizeMethod.FIT,
context, imageUri, maxWidth, maxHeight, BitmapResizeMethod.FIT,
isAllowSwapSide = true, shouldUpscale = false
)
// resize aMaps
@ -77,4 +74,6 @@ class ZeroDce(context: Context) {
}
private val context = context
private val maxWidth = maxWidth
private val maxHeight = maxHeight
}

View file

@ -6,24 +6,32 @@ import 'package:nc_photos_plugin/src/k.dart' as k;
class ImageProcessor {
static Future<void> zeroDce(
String fileUrl,
String filename, {
String filename,
int maxWidth,
int maxHeight, {
Map<String, String>? headers,
}) =>
_methodChannel.invokeMethod("zeroDce", <String, dynamic>{
"fileUrl": fileUrl,
"headers": headers,
"filename": filename,
"maxWidth": maxWidth,
"maxHeight": maxHeight,
});
static Future<void> deepLab3Portrait(
String fileUrl,
String filename, {
String filename,
int maxWidth,
int maxHeight, {
Map<String, String>? headers,
}) =>
_methodChannel.invokeMethod("deepLab3Portrait", <String, dynamic>{
"fileUrl": fileUrl,
"headers": headers,
"filename": filename,
"maxWidth": maxWidth,
"maxHeight": maxHeight,
});
static const _methodChannel =