mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-22 08:46:18 +01:00
Merge branch 'image-editor' into dev
This commit is contained in:
commit
2085b9197e
46 changed files with 3679 additions and 23 deletions
|
@ -8,3 +8,4 @@ const enhanceZeroDceUrl = "https://bit.ly/3wKJcm9";
|
|||
const enhanceDeepLabPortraitBlurUrl = "https://bit.ly/3wIuXy6";
|
||||
const enhanceEsrganUrl = "https://bit.ly/3wO0NJP";
|
||||
const enhanceStyleTransferUrl = "https://bit.ly/3agpTcF";
|
||||
const editPhotosUrl = "https://bit.ly/3v82oKA";
|
||||
|
|
|
@ -82,6 +82,22 @@ extension IterableExtension<T> on Iterable<T> {
|
|||
return toList();
|
||||
}
|
||||
}
|
||||
|
||||
/// The first index of [element] in this iterable
|
||||
///
|
||||
/// Searches the list from index start to the end of the list. The first time
|
||||
/// an object o is encountered so that o == element, the index of o is
|
||||
/// returned. Returns -1 if element is not found.
|
||||
int indexOf(T element, [int start = 0]) {
|
||||
var i = 0;
|
||||
for (final e in this) {
|
||||
if (e == element) {
|
||||
return i;
|
||||
}
|
||||
++i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
extension IterableFlattenExtension<T> on Iterable<Iterable<T>> {
|
||||
|
|
|
@ -1256,6 +1256,51 @@
|
|||
"@doubleTapExitNotification": {
|
||||
"description": "If double tap to exit is enabled in settings, shown when users tap the back button"
|
||||
},
|
||||
"imageEditDiscardDialogTitle": "Discard changes?",
|
||||
"@imageEditDiscardDialogTitle": {
|
||||
"description": "Warn before dismissing image editor (e.g., user pressing back button)"
|
||||
},
|
||||
"imageEditDiscardDialogContent": "Your changes are not saved",
|
||||
"discardButtonLabel": "DISCARD",
|
||||
"@discardButtonLabel": {
|
||||
"description": "Discard the current unsaved content"
|
||||
},
|
||||
"saveTooltip": "Save",
|
||||
"@saveTooltip": {
|
||||
"description": "Save the current content"
|
||||
},
|
||||
"imageEditColorBrightness": "Brightness",
|
||||
"@imageEditColorBrightness": {
|
||||
"description": "Adjust the brightness of an image"
|
||||
},
|
||||
"imageEditColorContrast": "Contrast",
|
||||
"@imageEditColorContrast": {
|
||||
"description": "Adjust the contrast of an image"
|
||||
},
|
||||
"imageEditColorWhitePoint": "White point",
|
||||
"@imageEditColorWhitePoint": {
|
||||
"description": "Adjust the white point of an image. Learn more about this adjustment: https://www.photoreview.com.au/tips/editing/advanced-levels-adjustments"
|
||||
},
|
||||
"imageEditColorBlackPoint": "Black point",
|
||||
"@imageEditColorBlackPoint": {
|
||||
"description": "Adjust the black point of an image"
|
||||
},
|
||||
"imageEditColorSaturation": "Saturation",
|
||||
"@imageEditColorSaturation": {
|
||||
"description": "Adjust the color saturation of an image"
|
||||
},
|
||||
"imageEditColorWarmth": "Warmth",
|
||||
"@imageEditColorWarmth": {
|
||||
"description": "This roughly equals to adjusting the color temperature of an image. The end result is to shift the image colors such that it looks 'warmer' or 'cooler'"
|
||||
},
|
||||
"imageEditColorTint": "Tint",
|
||||
"@imageEditColorTint": {
|
||||
"description": "Shift colors from a green to a magenta tint"
|
||||
},
|
||||
"imageEditTitle": "Preview edits",
|
||||
"@imageEditTitle": {
|
||||
"description": "Title of the image editor"
|
||||
},
|
||||
|
||||
"errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues",
|
||||
"@errorUnauthenticated": {
|
||||
|
|
|
@ -112,6 +112,18 @@
|
|||
"enhanceStyleTransferTitle",
|
||||
"enhanceStyleTransferStyleDialogTitle",
|
||||
"doubleTapExitNotification",
|
||||
"imageEditDiscardDialogTitle",
|
||||
"imageEditDiscardDialogContent",
|
||||
"discardButtonLabel",
|
||||
"saveTooltip",
|
||||
"imageEditColorBrightness",
|
||||
"imageEditColorContrast",
|
||||
"imageEditColorWhitePoint",
|
||||
"imageEditColorBlackPoint",
|
||||
"imageEditColorSaturation",
|
||||
"imageEditColorWarmth",
|
||||
"imageEditColorTint",
|
||||
"imageEditTitle",
|
||||
"errorAlbumDowngrade"
|
||||
],
|
||||
|
||||
|
@ -242,6 +254,18 @@
|
|||
"enhanceStyleTransferTitle",
|
||||
"enhanceStyleTransferStyleDialogTitle",
|
||||
"doubleTapExitNotification",
|
||||
"imageEditDiscardDialogTitle",
|
||||
"imageEditDiscardDialogContent",
|
||||
"discardButtonLabel",
|
||||
"saveTooltip",
|
||||
"imageEditColorBrightness",
|
||||
"imageEditColorContrast",
|
||||
"imageEditColorWhitePoint",
|
||||
"imageEditColorBlackPoint",
|
||||
"imageEditColorSaturation",
|
||||
"imageEditColorWarmth",
|
||||
"imageEditColorTint",
|
||||
"imageEditTitle",
|
||||
"errorAlbumDowngrade"
|
||||
],
|
||||
|
||||
|
@ -252,11 +276,50 @@
|
|||
"settingsAccountLabelDescription",
|
||||
"settingsDoubleTapExitTitle",
|
||||
"enhanceStyleTransferStyleDialogTitle",
|
||||
"doubleTapExitNotification"
|
||||
"doubleTapExitNotification",
|
||||
"imageEditDiscardDialogTitle",
|
||||
"imageEditDiscardDialogContent",
|
||||
"discardButtonLabel",
|
||||
"saveTooltip",
|
||||
"imageEditColorBrightness",
|
||||
"imageEditColorContrast",
|
||||
"imageEditColorWhitePoint",
|
||||
"imageEditColorBlackPoint",
|
||||
"imageEditColorSaturation",
|
||||
"imageEditColorWarmth",
|
||||
"imageEditColorTint",
|
||||
"imageEditTitle"
|
||||
],
|
||||
|
||||
"es": [
|
||||
"rootPickerSkipConfirmationDialogContent2"
|
||||
"rootPickerSkipConfirmationDialogContent2",
|
||||
"imageEditDiscardDialogTitle",
|
||||
"imageEditDiscardDialogContent",
|
||||
"discardButtonLabel",
|
||||
"saveTooltip",
|
||||
"imageEditColorBrightness",
|
||||
"imageEditColorContrast",
|
||||
"imageEditColorWhitePoint",
|
||||
"imageEditColorBlackPoint",
|
||||
"imageEditColorSaturation",
|
||||
"imageEditColorWarmth",
|
||||
"imageEditColorTint",
|
||||
"imageEditTitle"
|
||||
],
|
||||
|
||||
"fi": [
|
||||
"imageEditDiscardDialogTitle",
|
||||
"imageEditDiscardDialogContent",
|
||||
"discardButtonLabel",
|
||||
"saveTooltip",
|
||||
"imageEditColorBrightness",
|
||||
"imageEditColorContrast",
|
||||
"imageEditColorWhitePoint",
|
||||
"imageEditColorBlackPoint",
|
||||
"imageEditColorSaturation",
|
||||
"imageEditColorWarmth",
|
||||
"imageEditColorTint",
|
||||
"imageEditTitle"
|
||||
],
|
||||
|
||||
"fr": [
|
||||
|
@ -291,7 +354,19 @@
|
|||
"enhanceSuperResolution4xTitle",
|
||||
"enhanceStyleTransferTitle",
|
||||
"enhanceStyleTransferStyleDialogTitle",
|
||||
"doubleTapExitNotification"
|
||||
"doubleTapExitNotification",
|
||||
"imageEditDiscardDialogTitle",
|
||||
"imageEditDiscardDialogContent",
|
||||
"discardButtonLabel",
|
||||
"saveTooltip",
|
||||
"imageEditColorBrightness",
|
||||
"imageEditColorContrast",
|
||||
"imageEditColorWhitePoint",
|
||||
"imageEditColorBlackPoint",
|
||||
"imageEditColorSaturation",
|
||||
"imageEditColorWarmth",
|
||||
"imageEditColorTint",
|
||||
"imageEditTitle"
|
||||
],
|
||||
|
||||
"pl": [
|
||||
|
@ -343,7 +418,19 @@
|
|||
"enhanceSuperResolution4xTitle",
|
||||
"enhanceStyleTransferTitle",
|
||||
"enhanceStyleTransferStyleDialogTitle",
|
||||
"doubleTapExitNotification"
|
||||
"doubleTapExitNotification",
|
||||
"imageEditDiscardDialogTitle",
|
||||
"imageEditDiscardDialogContent",
|
||||
"discardButtonLabel",
|
||||
"saveTooltip",
|
||||
"imageEditColorBrightness",
|
||||
"imageEditColorContrast",
|
||||
"imageEditColorWhitePoint",
|
||||
"imageEditColorBlackPoint",
|
||||
"imageEditColorSaturation",
|
||||
"imageEditColorWarmth",
|
||||
"imageEditColorTint",
|
||||
"imageEditTitle"
|
||||
],
|
||||
|
||||
"pt": [
|
||||
|
@ -374,7 +461,19 @@
|
|||
"enhanceSuperResolution4xTitle",
|
||||
"enhanceStyleTransferTitle",
|
||||
"enhanceStyleTransferStyleDialogTitle",
|
||||
"doubleTapExitNotification"
|
||||
"doubleTapExitNotification",
|
||||
"imageEditDiscardDialogTitle",
|
||||
"imageEditDiscardDialogContent",
|
||||
"discardButtonLabel",
|
||||
"saveTooltip",
|
||||
"imageEditColorBrightness",
|
||||
"imageEditColorContrast",
|
||||
"imageEditColorWhitePoint",
|
||||
"imageEditColorBlackPoint",
|
||||
"imageEditColorSaturation",
|
||||
"imageEditColorWarmth",
|
||||
"imageEditColorTint",
|
||||
"imageEditTitle"
|
||||
],
|
||||
|
||||
"ru": [
|
||||
|
@ -405,7 +504,19 @@
|
|||
"enhanceSuperResolution4xTitle",
|
||||
"enhanceStyleTransferTitle",
|
||||
"enhanceStyleTransferStyleDialogTitle",
|
||||
"doubleTapExitNotification"
|
||||
"doubleTapExitNotification",
|
||||
"imageEditDiscardDialogTitle",
|
||||
"imageEditDiscardDialogContent",
|
||||
"discardButtonLabel",
|
||||
"saveTooltip",
|
||||
"imageEditColorBrightness",
|
||||
"imageEditColorContrast",
|
||||
"imageEditColorWhitePoint",
|
||||
"imageEditColorBlackPoint",
|
||||
"imageEditColorSaturation",
|
||||
"imageEditColorWarmth",
|
||||
"imageEditColorTint",
|
||||
"imageEditTitle"
|
||||
],
|
||||
|
||||
"zh": [
|
||||
|
@ -436,7 +547,19 @@
|
|||
"enhanceSuperResolution4xTitle",
|
||||
"enhanceStyleTransferTitle",
|
||||
"enhanceStyleTransferStyleDialogTitle",
|
||||
"doubleTapExitNotification"
|
||||
"doubleTapExitNotification",
|
||||
"imageEditDiscardDialogTitle",
|
||||
"imageEditDiscardDialogContent",
|
||||
"discardButtonLabel",
|
||||
"saveTooltip",
|
||||
"imageEditColorBrightness",
|
||||
"imageEditColorContrast",
|
||||
"imageEditColorWhitePoint",
|
||||
"imageEditColorBlackPoint",
|
||||
"imageEditColorSaturation",
|
||||
"imageEditColorWarmth",
|
||||
"imageEditColorTint",
|
||||
"imageEditTitle"
|
||||
],
|
||||
|
||||
"zh_Hant": [
|
||||
|
@ -467,6 +590,18 @@
|
|||
"enhanceSuperResolution4xTitle",
|
||||
"enhanceStyleTransferTitle",
|
||||
"enhanceStyleTransferStyleDialogTitle",
|
||||
"doubleTapExitNotification"
|
||||
"doubleTapExitNotification",
|
||||
"imageEditDiscardDialogTitle",
|
||||
"imageEditDiscardDialogContent",
|
||||
"discardButtonLabel",
|
||||
"saveTooltip",
|
||||
"imageEditColorBrightness",
|
||||
"imageEditColorContrast",
|
||||
"imageEditColorWhitePoint",
|
||||
"imageEditColorBlackPoint",
|
||||
"imageEditColorSaturation",
|
||||
"imageEditColorWarmth",
|
||||
"imageEditColorTint",
|
||||
"imageEditTitle"
|
||||
]
|
||||
}
|
||||
|
|
41
app/lib/pixel_image_provider.dart
Normal file
41
app/lib/pixel_image_provider.dart
Normal file
|
@ -0,0 +1,41 @@
|
|||
import 'dart:typed_data';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
/// ImageProvider for raw RGBA pixels
|
||||
class PixelImage extends ImageProvider<PixelImage> {
|
||||
PixelImage(
|
||||
this.rgba,
|
||||
this.width,
|
||||
this.height, {
|
||||
this.scale = 1.0,
|
||||
});
|
||||
|
||||
@override
|
||||
obtainKey(ImageConfiguration configuration) =>
|
||||
SynchronousFuture<PixelImage>(this);
|
||||
|
||||
@override
|
||||
load(PixelImage key, DecoderCallback decode) =>
|
||||
OneFrameImageStreamCompleter(_createImageInfo());
|
||||
|
||||
Future<ImageInfo> _createImageInfo() async {
|
||||
final codec = await ImageDescriptor.raw(
|
||||
await ImmutableBuffer.fromUint8List(rgba),
|
||||
width: width,
|
||||
height: height,
|
||||
pixelFormat: PixelFormat.rgba8888,
|
||||
).instantiateCodec();
|
||||
final frame = await codec.getNextFrame();
|
||||
return ImageInfo(image: frame.image, scale: scale);
|
||||
}
|
||||
|
||||
final Uint8List rgba;
|
||||
final int width;
|
||||
final int height;
|
||||
|
||||
/// The scale to place in the [ImageInfo] object of the image.
|
||||
final double scale;
|
||||
}
|
|
@ -110,8 +110,8 @@ class AppTheme extends StatelessWidget {
|
|||
|
||||
static Color getUnfocusedIconColor(BuildContext context) {
|
||||
return Theme.of(context).brightness == Brightness.light
|
||||
? Colors.black54
|
||||
: Colors.white70;
|
||||
? unfocusedIconColorLight
|
||||
: unfocusedIconColorDark;
|
||||
}
|
||||
|
||||
static ThemeData buildLightThemeData() {
|
||||
|
@ -181,6 +181,9 @@ class AppTheme extends StatelessWidget {
|
|||
static const primaryTextColorLight = Colors.black87;
|
||||
static final primaryTextColorDark = Colors.white.withOpacity(.87);
|
||||
|
||||
static const unfocusedIconColorLight = Colors.black54;
|
||||
static const unfocusedIconColorDark = Colors.white70;
|
||||
|
||||
static const widthLimitedContentMaxWidth = 550.0;
|
||||
|
||||
/// Make a TextButton look like a default FlatButton. See
|
||||
|
|
|
@ -355,8 +355,10 @@ class _EnhancedPhotoBrowserState extends State<EnhancedPhotoBrowser>
|
|||
}
|
||||
|
||||
void _reqQuery() {
|
||||
_bloc.add(const ScanLocalDirBlocQuery(
|
||||
["Download/Photos (for Nextcloud)/Enhanced Photos"]));
|
||||
_bloc.add(const ScanLocalDirBlocQuery([
|
||||
"Download/Photos (for Nextcloud)/Enhanced Photos",
|
||||
"Download/Photos (for Nextcloud)/Edited Photos",
|
||||
]));
|
||||
}
|
||||
|
||||
final _bloc = ScanLocalDirBloc();
|
||||
|
|
649
app/lib/widget/image_editor.dart
Normal file
649
app/lib/widget/image_editor.dart
Normal file
|
@ -0,0 +1,649 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/api/api.dart';
|
||||
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||
import 'package:nc_photos/app_localizations.dart';
|
||||
import 'package:nc_photos/cache_manager_util.dart';
|
||||
import 'package:nc_photos/double_extension.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/help_utils.dart' as help_util;
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:nc_photos/pixel_image_provider.dart';
|
||||
import 'package:nc_photos/theme.dart';
|
||||
import 'package:nc_photos/url_launcher_util.dart';
|
||||
import 'package:nc_photos/widget/handler/permission_handler.dart';
|
||||
import 'package:nc_photos/widget/stateful_slider.dart';
|
||||
import 'package:nc_photos_plugin/nc_photos_plugin.dart';
|
||||
|
||||
class ImageEditorArguments {
|
||||
const ImageEditorArguments(this.account, this.file);
|
||||
|
||||
final Account account;
|
||||
final File file;
|
||||
}
|
||||
|
||||
class ImageEditor extends StatefulWidget {
|
||||
static const routeName = "/image-editor";
|
||||
|
||||
static Route buildRoute(ImageEditorArguments args) => MaterialPageRoute(
|
||||
builder: (context) => ImageEditor.fromArgs(args),
|
||||
);
|
||||
|
||||
const ImageEditor({
|
||||
Key? key,
|
||||
required this.account,
|
||||
required this.file,
|
||||
}) : super(key: key);
|
||||
|
||||
ImageEditor.fromArgs(ImageEditorArguments args, {Key? key})
|
||||
: this(
|
||||
key: key,
|
||||
account: args.account,
|
||||
file: args.file,
|
||||
);
|
||||
|
||||
@override
|
||||
createState() => _ImageEditorState();
|
||||
|
||||
final Account account;
|
||||
final File file;
|
||||
}
|
||||
|
||||
class _ImageEditorState extends State<ImageEditor> {
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
_initImage();
|
||||
_ensurePermission();
|
||||
}
|
||||
|
||||
Future<void> _ensurePermission() async {
|
||||
if (!await const PermissionHandler().ensureStorageWritePermission()) {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
build(BuildContext context) => AppTheme(
|
||||
child: Scaffold(
|
||||
body: Builder(
|
||||
builder: _buildContent,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Future<void> _initImage() async {
|
||||
final fileInfo = await LargeImageCacheManager.inst
|
||||
.getFileFromCache(api_util.getFilePreviewUrl(
|
||||
widget.account,
|
||||
widget.file,
|
||||
width: k.photoLargeSize,
|
||||
height: k.photoLargeSize,
|
||||
a: true,
|
||||
));
|
||||
_src = await ImageLoader.loadUri(
|
||||
"file://${fileInfo!.file.path}",
|
||||
480,
|
||||
360,
|
||||
ImageLoaderResizeMethod.fit,
|
||||
isAllowSwapSide: true,
|
||||
);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isDoneInit = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildContent(BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
_onBackButton(context);
|
||||
return false;
|
||||
},
|
||||
child: ColoredBox(
|
||||
color: Colors.black,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildAppBar(context),
|
||||
Expanded(
|
||||
child: _isDoneInit
|
||||
? Image(
|
||||
image: (_dst ?? _src).run((obj) =>
|
||||
PixelImage(obj.pixel, obj.width, obj.height)),
|
||||
fit: BoxFit.contain,
|
||||
gaplessPlayback: true,
|
||||
)
|
||||
: Container(),
|
||||
),
|
||||
_buildFilterOption(context),
|
||||
_buildFilterBar(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAppBar(BuildContext context) => AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
foregroundColor: Colors.white.withOpacity(.87),
|
||||
leading: BackButton(onPressed: () => _onBackButton(context)),
|
||||
title: Text(L10n.global().imageEditTitle),
|
||||
actions: [
|
||||
if (_filters.isNotEmpty)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.save_outlined),
|
||||
tooltip: L10n.global().saveTooltip,
|
||||
onPressed: () => _onSavePressed(context),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.help_outline),
|
||||
tooltip: L10n.global().helpTooltip,
|
||||
onPressed: () {
|
||||
launch(help_util.editPhotosUrl);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildFilterBar(BuildContext context) {
|
||||
return Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 16),
|
||||
_FilterButton(
|
||||
icon: Icons.brightness_medium,
|
||||
label: L10n.global().imageEditColorBrightness,
|
||||
onPressed: _onBrightnessPressed,
|
||||
isSelected: _selectedFilter == _ColorFilterType.brightness,
|
||||
activationOrder:
|
||||
_filters.keys.indexOf(_ColorFilterType.brightness),
|
||||
),
|
||||
_FilterButton(
|
||||
icon: Icons.contrast,
|
||||
label: L10n.global().imageEditColorContrast,
|
||||
onPressed: _onContrastPressed,
|
||||
isSelected: _selectedFilter == _ColorFilterType.contrast,
|
||||
activationOrder:
|
||||
_filters.keys.indexOf(_ColorFilterType.contrast),
|
||||
),
|
||||
_FilterButton(
|
||||
icon: Icons.circle,
|
||||
label: L10n.global().imageEditColorWhitePoint,
|
||||
onPressed: _onWhitePointPressed,
|
||||
isSelected: _selectedFilter == _ColorFilterType.whitePoint,
|
||||
activationOrder:
|
||||
_filters.keys.indexOf(_ColorFilterType.whitePoint),
|
||||
),
|
||||
_FilterButton(
|
||||
icon: Icons.circle_outlined,
|
||||
label: L10n.global().imageEditColorBlackPoint,
|
||||
onPressed: _onBlackPointPressed,
|
||||
isSelected: _selectedFilter == _ColorFilterType.blackPoint,
|
||||
activationOrder:
|
||||
_filters.keys.indexOf(_ColorFilterType.blackPoint),
|
||||
),
|
||||
_FilterButton(
|
||||
icon: Icons.invert_colors,
|
||||
label: L10n.global().imageEditColorSaturation,
|
||||
onPressed: _onSaturationPressed,
|
||||
isSelected: _selectedFilter == _ColorFilterType.saturation,
|
||||
activationOrder:
|
||||
_filters.keys.indexOf(_ColorFilterType.saturation),
|
||||
),
|
||||
_FilterButton(
|
||||
icon: Icons.thermostat,
|
||||
label: L10n.global().imageEditColorWarmth,
|
||||
onPressed: _onWarmthPressed,
|
||||
isSelected: _selectedFilter == _ColorFilterType.warmth,
|
||||
activationOrder: _filters.keys.indexOf(_ColorFilterType.warmth),
|
||||
),
|
||||
_FilterButton(
|
||||
icon: Icons.colorize,
|
||||
label: L10n.global().imageEditColorTint,
|
||||
onPressed: _onTintPressed,
|
||||
isSelected: _selectedFilter == _ColorFilterType.tint,
|
||||
activationOrder: _filters.keys.indexOf(_ColorFilterType.tint),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterOption(BuildContext context) {
|
||||
Widget? child;
|
||||
switch (_selectedFilter) {
|
||||
case _ColorFilterType.brightness:
|
||||
child = _buildBrightnessOption(context);
|
||||
break;
|
||||
|
||||
case _ColorFilterType.contrast:
|
||||
child = _buildContrastOption(context);
|
||||
break;
|
||||
|
||||
case _ColorFilterType.whitePoint:
|
||||
child = _buildWhitePointOption(context);
|
||||
break;
|
||||
|
||||
case _ColorFilterType.blackPoint:
|
||||
child = _buildBlackPointOption(context);
|
||||
break;
|
||||
|
||||
case _ColorFilterType.saturation:
|
||||
child = _buildSaturationOption(context);
|
||||
break;
|
||||
|
||||
case _ColorFilterType.warmth:
|
||||
child = _buildWarmthOption(context);
|
||||
break;
|
||||
|
||||
case _ColorFilterType.tint:
|
||||
child = _buildTintOption(context);
|
||||
break;
|
||||
|
||||
case null:
|
||||
child = null;
|
||||
break;
|
||||
}
|
||||
return Container(
|
||||
height: 96,
|
||||
alignment: Alignment.center,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSliderOption(
|
||||
BuildContext context, {
|
||||
required Key key,
|
||||
required double min,
|
||||
required double max,
|
||||
required double initialValue,
|
||||
ValueChanged<double>? onChangeEnd,
|
||||
}) {
|
||||
return AppTheme.dark(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Stack(
|
||||
children: [
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Text(min.toStringAsFixedTruncated(1)),
|
||||
),
|
||||
if (min < 0 && max > 0)
|
||||
const Align(
|
||||
alignment: AlignmentDirectional.center,
|
||||
child: Text("0"),
|
||||
),
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: Text(max.toStringAsFixedTruncated(1)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
StatefulSlider(
|
||||
key: key,
|
||||
initialValue: initialValue.toDouble(),
|
||||
min: min.toDouble(),
|
||||
max: max.toDouble(),
|
||||
onChangeEnd: onChangeEnd,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBrightnessOption(BuildContext context) => _buildSliderOption(
|
||||
context,
|
||||
key: Key(_ColorFilterType.brightness.name),
|
||||
min: -100,
|
||||
max: 100,
|
||||
initialValue:
|
||||
(_filters[_ColorFilterType.brightness] as _BrightnessArguments)
|
||||
.value,
|
||||
onChangeEnd: (value) {
|
||||
(_filters[_ColorFilterType.brightness] as _BrightnessArguments)
|
||||
.value = value;
|
||||
_applyFilters();
|
||||
},
|
||||
);
|
||||
|
||||
Widget _buildContrastOption(BuildContext context) => _buildSliderOption(
|
||||
context,
|
||||
key: Key(_ColorFilterType.contrast.name),
|
||||
min: -100,
|
||||
max: 100,
|
||||
initialValue:
|
||||
(_filters[_ColorFilterType.contrast] as _ContrastArguments).value,
|
||||
onChangeEnd: (value) {
|
||||
(_filters[_ColorFilterType.contrast] as _ContrastArguments).value =
|
||||
value;
|
||||
_applyFilters();
|
||||
},
|
||||
);
|
||||
|
||||
Widget _buildWhitePointOption(BuildContext context) => _buildSliderOption(
|
||||
context,
|
||||
key: Key(_ColorFilterType.whitePoint.name),
|
||||
min: -100,
|
||||
max: 100,
|
||||
initialValue:
|
||||
(_filters[_ColorFilterType.whitePoint] as _WhitePointArguments)
|
||||
.value,
|
||||
onChangeEnd: (value) {
|
||||
(_filters[_ColorFilterType.whitePoint] as _WhitePointArguments)
|
||||
.value = value;
|
||||
_applyFilters();
|
||||
},
|
||||
);
|
||||
|
||||
Widget _buildBlackPointOption(BuildContext context) => _buildSliderOption(
|
||||
context,
|
||||
key: Key(_ColorFilterType.blackPoint.name),
|
||||
min: -100,
|
||||
max: 100,
|
||||
initialValue:
|
||||
(_filters[_ColorFilterType.blackPoint] as _BlackPointArguments)
|
||||
.value,
|
||||
onChangeEnd: (value) {
|
||||
(_filters[_ColorFilterType.blackPoint] as _BlackPointArguments)
|
||||
.value = value;
|
||||
_applyFilters();
|
||||
},
|
||||
);
|
||||
|
||||
Widget _buildSaturationOption(BuildContext context) => _buildSliderOption(
|
||||
context,
|
||||
key: Key(_ColorFilterType.saturation.name),
|
||||
min: -100,
|
||||
max: 100,
|
||||
initialValue:
|
||||
(_filters[_ColorFilterType.saturation] as _SaturationArguments)
|
||||
.value,
|
||||
onChangeEnd: (value) {
|
||||
(_filters[_ColorFilterType.saturation] as _SaturationArguments)
|
||||
.value = value;
|
||||
_applyFilters();
|
||||
},
|
||||
);
|
||||
|
||||
Widget _buildWarmthOption(BuildContext context) => _buildSliderOption(
|
||||
context,
|
||||
key: Key(_ColorFilterType.warmth.name),
|
||||
min: -100,
|
||||
max: 100,
|
||||
initialValue:
|
||||
(_filters[_ColorFilterType.warmth] as _WarmthArguments).value,
|
||||
onChangeEnd: (value) {
|
||||
(_filters[_ColorFilterType.warmth] as _WarmthArguments).value = value;
|
||||
_applyFilters();
|
||||
},
|
||||
);
|
||||
|
||||
Widget _buildTintOption(BuildContext context) => _buildSliderOption(
|
||||
context,
|
||||
key: Key(_ColorFilterType.tint.name),
|
||||
min: -100,
|
||||
max: 100,
|
||||
initialValue: (_filters[_ColorFilterType.tint] as _TintArguments).value,
|
||||
onChangeEnd: (value) {
|
||||
(_filters[_ColorFilterType.tint] as _TintArguments).value = value;
|
||||
_applyFilters();
|
||||
},
|
||||
);
|
||||
|
||||
Future<void> _onBackButton(BuildContext context) async {
|
||||
if (_filters.isEmpty) {
|
||||
Navigator.of(context).pop();
|
||||
return;
|
||||
}
|
||||
|
||||
final result = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(L10n.global().imageEditDiscardDialogTitle),
|
||||
content: Text(L10n.global().imageEditDiscardDialogContent),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: Text(L10n.global().discardButtonLabel),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (result == true) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onSavePressed(BuildContext context) async {
|
||||
await ImageProcessor.colorFilter(
|
||||
"${widget.account.url}/${widget.file.path}",
|
||||
widget.file.filename,
|
||||
4096,
|
||||
3072,
|
||||
_buildFilterList(),
|
||||
headers: {
|
||||
"Authorization": Api.getAuthorizationHeaderValue(widget.account),
|
||||
},
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
void _onFilterPressed(_ColorFilterType type, _FilterArguments defArgs) {
|
||||
if (_selectedFilter == type) {
|
||||
// deactivate filter
|
||||
setState(() {
|
||||
_selectedFilter = null;
|
||||
_filters.remove(type);
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_selectedFilter = type;
|
||||
_filters[type] ??= defArgs;
|
||||
});
|
||||
}
|
||||
_applyFilters();
|
||||
}
|
||||
|
||||
void _onBrightnessPressed() =>
|
||||
_onFilterPressed(_ColorFilterType.brightness, _BrightnessArguments(0));
|
||||
void _onContrastPressed() =>
|
||||
_onFilterPressed(_ColorFilterType.contrast, _ContrastArguments(0));
|
||||
void _onWhitePointPressed() =>
|
||||
_onFilterPressed(_ColorFilterType.whitePoint, _WhitePointArguments(0));
|
||||
void _onBlackPointPressed() =>
|
||||
_onFilterPressed(_ColorFilterType.blackPoint, _BlackPointArguments(0));
|
||||
void _onSaturationPressed() =>
|
||||
_onFilterPressed(_ColorFilterType.saturation, _SaturationArguments(0));
|
||||
void _onWarmthPressed() =>
|
||||
_onFilterPressed(_ColorFilterType.warmth, _WarmthArguments(0));
|
||||
void _onTintPressed() =>
|
||||
_onFilterPressed(_ColorFilterType.tint, _TintArguments(0));
|
||||
|
||||
List<ImageFilter> _buildFilterList() {
|
||||
return _filters.entries.map((e) {
|
||||
switch (e.key) {
|
||||
case _ColorFilterType.brightness:
|
||||
return (e.value as _BrightnessArguments)
|
||||
.run((arg) => ColorBrightnessFilter(arg.value / 100));
|
||||
|
||||
case _ColorFilterType.contrast:
|
||||
return (e.value as _ContrastArguments)
|
||||
.run((arg) => ColorContrastFilter(arg.value / 100));
|
||||
|
||||
case _ColorFilterType.whitePoint:
|
||||
return (e.value as _WhitePointArguments)
|
||||
.run((arg) => ColorWhitePointFilter(arg.value / 100));
|
||||
|
||||
case _ColorFilterType.blackPoint:
|
||||
return (e.value as _BlackPointArguments)
|
||||
.run((arg) => ColorBlackPointFilter(arg.value / 100));
|
||||
|
||||
case _ColorFilterType.saturation:
|
||||
return (e.value as _SaturationArguments)
|
||||
.run((arg) => ColorSaturationFilter(arg.value / 100));
|
||||
|
||||
case _ColorFilterType.warmth:
|
||||
return (e.value as _WarmthArguments)
|
||||
.run((arg) => ColorWarmthFilter(arg.value / 100));
|
||||
|
||||
case _ColorFilterType.tint:
|
||||
return (e.value as _TintArguments)
|
||||
.run((arg) => ColorTintFilter(arg.value / 100));
|
||||
}
|
||||
}).toList();
|
||||
}
|
||||
|
||||
Future<void> _applyFilters() async {
|
||||
final result = await ImageProcessor.filterPreview(_src, _buildFilterList());
|
||||
setState(() {
|
||||
_dst = result;
|
||||
});
|
||||
}
|
||||
|
||||
bool _isDoneInit = false;
|
||||
late final Rgba8Image _src;
|
||||
Rgba8Image? _dst;
|
||||
final _filters = <_ColorFilterType, _FilterArguments>{};
|
||||
_ColorFilterType? _selectedFilter;
|
||||
}
|
||||
|
||||
enum _ColorFilterType {
|
||||
brightness,
|
||||
contrast,
|
||||
whitePoint,
|
||||
blackPoint,
|
||||
saturation,
|
||||
warmth,
|
||||
tint,
|
||||
}
|
||||
|
||||
abstract class _FilterArguments {}
|
||||
|
||||
class _FilterDoubleArguments implements _FilterArguments {
|
||||
_FilterDoubleArguments(this.value);
|
||||
|
||||
double value;
|
||||
}
|
||||
|
||||
typedef _BrightnessArguments = _FilterDoubleArguments;
|
||||
typedef _ContrastArguments = _FilterDoubleArguments;
|
||||
typedef _WhitePointArguments = _FilterDoubleArguments;
|
||||
typedef _BlackPointArguments = _FilterDoubleArguments;
|
||||
typedef _SaturationArguments = _FilterDoubleArguments;
|
||||
typedef _WarmthArguments = _FilterDoubleArguments;
|
||||
typedef _TintArguments = _FilterDoubleArguments;
|
||||
|
||||
class _FilterButton extends StatelessWidget {
|
||||
const _FilterButton({
|
||||
Key? key,
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.onPressed,
|
||||
this.isSelected = false,
|
||||
this.activationOrder = -1,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
build(BuildContext context) {
|
||||
final color = !isSelected && isActivated
|
||||
? AppTheme.primarySwatchDark[900]!.withOpacity(0.4)
|
||||
: AppTheme.primarySwatchDark[500]!.withOpacity(0.7);
|
||||
return InkWell(
|
||||
onTap: onPressed,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 64,
|
||||
height: 64,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
AnimatedOpacity(
|
||||
opacity: isSelected || isActivated ? 1 : 0,
|
||||
duration: k.animationDurationNormal,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 32,
|
||||
color: AppTheme.unfocusedIconColorDark,
|
||||
),
|
||||
),
|
||||
if (isActivated)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Text(
|
||||
(activationOrder + 1).toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.unfocusedIconColorDark,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
label,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.unfocusedIconColorDark,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool get isActivated => activationOrder >= 0;
|
||||
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final VoidCallback? onPressed;
|
||||
final bool isSelected;
|
||||
final int activationOrder;
|
||||
}
|
|
@ -19,6 +19,7 @@ import 'package:nc_photos/widget/dynamic_album_browser.dart';
|
|||
import 'package:nc_photos/widget/enhanced_photo_browser.dart';
|
||||
import 'package:nc_photos/widget/favorite_browser.dart';
|
||||
import 'package:nc_photos/widget/home.dart';
|
||||
import 'package:nc_photos/widget/image_editor.dart';
|
||||
import 'package:nc_photos/widget/local_file_viewer.dart';
|
||||
import 'package:nc_photos/widget/people_browser.dart';
|
||||
import 'package:nc_photos/widget/person_browser.dart';
|
||||
|
@ -163,6 +164,7 @@ class _MyAppState extends State<MyApp>
|
|||
route ??= _handleEnhancedPhotoBrowserRoute(settings);
|
||||
route ??= _handleLocalFileViewerRoute(settings);
|
||||
route ??= _handleEnhancementSettingsRoute(settings);
|
||||
route ??= _handleImageEditorRoute(settings);
|
||||
return route;
|
||||
}
|
||||
|
||||
|
@ -539,6 +541,19 @@ class _MyAppState extends State<MyApp>
|
|||
return null;
|
||||
}
|
||||
|
||||
Route<dynamic>? _handleImageEditorRoute(RouteSettings settings) {
|
||||
try {
|
||||
if (settings.name == ImageEditor.routeName &&
|
||||
settings.arguments != null) {
|
||||
final args = settings.arguments as ImageEditorArguments;
|
||||
return ImageEditor.buildRoute(args);
|
||||
}
|
||||
} catch (e) {
|
||||
_log.severe("[_handleImageEditorRoute] Failed while handling route", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
||||
final _navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ import 'package:nc_photos/widget/disposable.dart';
|
|||
import 'package:nc_photos/widget/handler/enhance_handler.dart';
|
||||
import 'package:nc_photos/widget/handler/remove_selection_handler.dart';
|
||||
import 'package:nc_photos/widget/horizontal_page_viewer.dart';
|
||||
import 'package:nc_photos/widget/image_editor.dart';
|
||||
import 'package:nc_photos/widget/image_viewer.dart';
|
||||
import 'package:nc_photos/widget/slideshow_dialog.dart';
|
||||
import 'package:nc_photos/widget/slideshow_viewer.dart';
|
||||
|
@ -212,7 +213,15 @@ class _ViewerState extends State<Viewer>
|
|||
onPressed: () => _onSharePressed(context),
|
||||
),
|
||||
if (features.isSupportEnhancement &&
|
||||
EnhanceHandler.isSupportedFormat(file))
|
||||
EnhanceHandler.isSupportedFormat(file)) ...[
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.tune_outlined,
|
||||
color: Colors.white.withOpacity(.87),
|
||||
),
|
||||
tooltip: L10n.global().editTooltip,
|
||||
onPressed: () => _onEditPressed(context),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.auto_fix_high_outlined,
|
||||
|
@ -221,6 +230,7 @@ class _ViewerState extends State<Viewer>
|
|||
tooltip: L10n.global().enhanceTooltip,
|
||||
onPressed: () => _onEnhancePressed(context),
|
||||
),
|
||||
],
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.download_outlined,
|
||||
|
@ -570,6 +580,18 @@ class _ViewerState extends State<Viewer>
|
|||
).shareFiles(widget.account, [file]);
|
||||
}
|
||||
|
||||
void _onEditPressed(BuildContext context) {
|
||||
final file = widget.streamFiles[_viewerController.currentPage];
|
||||
if (!file_util.isSupportedImageFormat(file)) {
|
||||
_log.shout("[_onEditPressed] Video file not supported");
|
||||
return;
|
||||
}
|
||||
|
||||
_log.info("[_onEditPressed] Edit file: ${file.path}");
|
||||
Navigator.of(context).pushNamed(ImageEditor.routeName,
|
||||
arguments: ImageEditorArguments(widget.account, file));
|
||||
}
|
||||
|
||||
void _onEnhancePressed(BuildContext context) {
|
||||
final file = widget.streamFiles[_viewerController.currentPage];
|
||||
if (!file_util.isSupportedImageFormat(file)) {
|
||||
|
|
|
@ -33,6 +33,16 @@ add_library( # Sets the name of the library.
|
|||
SHARED
|
||||
|
||||
# Provides a relative path to your source file(s).
|
||||
filter/brightness.cpp
|
||||
filter/color_levels.cpp
|
||||
filter/contrast.cpp
|
||||
filter/curve.cpp
|
||||
filter/hslhsv.cpp
|
||||
filter/saturation.cpp
|
||||
filter/tint.cpp
|
||||
filter/warmth.cpp
|
||||
filter/yuv.cpp
|
||||
lib/spline/spline.cpp
|
||||
arbitrary_style_transfer.cpp
|
||||
deep_lap_3.cpp
|
||||
esrgan.cpp
|
||||
|
|
77
plugin/android/src/main/cpp/filter/brightness.cpp
Normal file
77
plugin/android/src/main/cpp/filter/brightness.cpp
Normal file
|
@ -0,0 +1,77 @@
|
|||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <jni.h>
|
||||
#include <vector>
|
||||
|
||||
#include "../exception.h"
|
||||
#include "../log.h"
|
||||
#include "../math_util.h"
|
||||
#include "../util.h"
|
||||
#include "./hslhsv.h"
|
||||
|
||||
using namespace plugin;
|
||||
using namespace std;
|
||||
|
||||
namespace {
|
||||
|
||||
class Brightness {
|
||||
public:
|
||||
std::vector<uint8_t> apply(const uint8_t *rgba8, const size_t width,
|
||||
const size_t height, const float weight);
|
||||
|
||||
private:
|
||||
static constexpr const char *TAG = "Brightness";
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" JNIEXPORT jbyteArray JNICALL
|
||||
Java_com_nkming_nc_1photos_plugin_image_1processor_Brightness_applyNative(
|
||||
JNIEnv *env, jobject *thiz, jbyteArray rgba8, jint width, jint height,
|
||||
jfloat weight) {
|
||||
try {
|
||||
initOpenMp();
|
||||
RaiiContainer<jbyte> cRgba8(
|
||||
[&]() { return env->GetByteArrayElements(rgba8, nullptr); },
|
||||
[&](jbyte *obj) {
|
||||
env->ReleaseByteArrayElements(rgba8, obj, JNI_ABORT);
|
||||
});
|
||||
const auto result = Brightness().apply(
|
||||
reinterpret_cast<uint8_t *>(cRgba8.get()), width, height, 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 {
|
||||
|
||||
vector<uint8_t> Brightness::apply(const uint8_t *rgba8, const size_t width,
|
||||
const size_t height, const float weight) {
|
||||
LOGI(TAG, "[apply] weight: %.2f", weight);
|
||||
if (weight == 0) {
|
||||
// shortcut
|
||||
return vector<uint8_t>(rgba8, rgba8 + width * height * 4);
|
||||
}
|
||||
|
||||
const float mul = 1 + weight / 2;
|
||||
vector<uint8_t> output(width * height * 4);
|
||||
#pragma omp parallel for
|
||||
for (size_t i = 0; i < width * height; ++i) {
|
||||
const auto p = i * 4;
|
||||
auto hsv = filter::rgb8ToHsv(rgba8 + p);
|
||||
hsv[2] = clamp(0.f, hsv[2] * mul, 1.f);
|
||||
const auto &newRgb = filter::hsvToRgb8(hsv.data());
|
||||
memcpy(output.data() + p, newRgb.data(), 3);
|
||||
output[p + 3] = rgba8[p + 3];
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
} // namespace
|
178
plugin/android/src/main/cpp/filter/color_levels.cpp
Normal file
178
plugin/android/src/main/cpp/filter/color_levels.cpp
Normal file
|
@ -0,0 +1,178 @@
|
|||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <jni.h>
|
||||
#include <vector>
|
||||
|
||||
#include "../exception.h"
|
||||
#include "../log.h"
|
||||
#include "../math_util.h"
|
||||
#include "../util.h"
|
||||
|
||||
using namespace plugin;
|
||||
using namespace std;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float INPUT_AMPLITUDE = .4f;
|
||||
constexpr uint8_t OUTPUT_AMPLITUDE = 100;
|
||||
|
||||
class WhitePoint {
|
||||
public:
|
||||
std::vector<uint8_t> apply(const uint8_t *rgba8, const size_t width,
|
||||
const size_t height, const float weight);
|
||||
|
||||
private:
|
||||
static uint8_t applyInputLevel(const uint8_t p, const float weight) {
|
||||
const auto pf = p / 255.f;
|
||||
const auto max = 1 - weight * INPUT_AMPLITUDE;
|
||||
return clamp<int>(0, clamp(0.f, pf, max) / max * 255.f, 255);
|
||||
}
|
||||
|
||||
static uint8_t applyOutputLevel(const uint8_t p, const float weight) {
|
||||
return clamp<int>(0, p / 255.f * (255 - weight * OUTPUT_AMPLITUDE), 255);
|
||||
}
|
||||
|
||||
static std::vector<uint8_t> buildLut(const float weight);
|
||||
|
||||
static constexpr const char *TAG = "WhitePoint";
|
||||
};
|
||||
|
||||
class BlackPoint {
|
||||
public:
|
||||
std::vector<uint8_t> apply(const uint8_t *rgba8, const size_t width,
|
||||
const size_t height, const float weight);
|
||||
|
||||
private:
|
||||
static inline uint8_t applyInputLevel(const uint8_t p, const float weight) {
|
||||
const auto pf = p / 255.f;
|
||||
const auto min = weight * INPUT_AMPLITUDE;
|
||||
return clamp<int>(0, (clamp(min, pf, 1.f) - min) / (1 - min) * 255.f, 255);
|
||||
}
|
||||
|
||||
static inline uint8_t applyOutputLevel(const uint8_t p, const float weight) {
|
||||
const auto x = weight * OUTPUT_AMPLITUDE;
|
||||
return clamp<int>(0, p / 255.f * (255 - x) + x, 255);
|
||||
}
|
||||
|
||||
static std::vector<uint8_t> buildLut(const float weight);
|
||||
|
||||
static constexpr const char *TAG = "BlackPoint";
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" JNIEXPORT jbyteArray JNICALL
|
||||
Java_com_nkming_nc_1photos_plugin_image_1processor_WhitePoint_applyNative(
|
||||
JNIEnv *env, jobject *thiz, jbyteArray rgba8, jint width, jint height,
|
||||
jfloat weight) {
|
||||
try {
|
||||
initOpenMp();
|
||||
RaiiContainer<jbyte> cRgba8(
|
||||
[&]() { return env->GetByteArrayElements(rgba8, nullptr); },
|
||||
[&](jbyte *obj) {
|
||||
env->ReleaseByteArrayElements(rgba8, obj, JNI_ABORT);
|
||||
});
|
||||
const auto result = WhitePoint().apply(
|
||||
reinterpret_cast<uint8_t *>(cRgba8.get()), width, height, 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;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jbyteArray JNICALL
|
||||
Java_com_nkming_nc_1photos_plugin_image_1processor_BlackPoint_applyNative(
|
||||
JNIEnv *env, jobject *thiz, jbyteArray rgba8, jint width, jint height,
|
||||
jfloat weight) {
|
||||
try {
|
||||
initOpenMp();
|
||||
RaiiContainer<jbyte> cRgba8(
|
||||
[&]() { return env->GetByteArrayElements(rgba8, nullptr); },
|
||||
[&](jbyte *obj) {
|
||||
env->ReleaseByteArrayElements(rgba8, obj, JNI_ABORT);
|
||||
});
|
||||
const auto result = BlackPoint().apply(
|
||||
reinterpret_cast<uint8_t *>(cRgba8.get()), width, height, 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 {
|
||||
|
||||
vector<uint8_t> WhitePoint::apply(const uint8_t *rgba8, const size_t width,
|
||||
const size_t height, const float weight) {
|
||||
LOGI(TAG, "[apply] weight: %.2f", weight);
|
||||
if (weight == 0) {
|
||||
// shortcut
|
||||
return vector<uint8_t>(rgba8, rgba8 + width * height * 4);
|
||||
}
|
||||
|
||||
const auto lut = buildLut(weight);
|
||||
vector<uint8_t> output(width * height * 4);
|
||||
#pragma omp parallel for
|
||||
for (size_t i = 0; i < width * height; ++i) {
|
||||
const auto p = i * 4;
|
||||
output[p + 0] = lut[rgba8[p + 0]];
|
||||
output[p + 1] = lut[rgba8[p + 1]];
|
||||
output[p + 2] = lut[rgba8[p + 2]];
|
||||
output[p + 3] = rgba8[p + 3];
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
vector<uint8_t> WhitePoint::buildLut(const float weight) {
|
||||
vector<uint8_t> product(256);
|
||||
const float weightAbs = std::abs(weight);
|
||||
const auto fn =
|
||||
weight > 0 ? &WhitePoint::applyInputLevel : &WhitePoint::applyOutputLevel;
|
||||
#pragma omp parallel for
|
||||
for (size_t i = 0; i < 256; ++i) {
|
||||
product[i] = fn(i, weightAbs);
|
||||
}
|
||||
return product;
|
||||
}
|
||||
|
||||
vector<uint8_t> BlackPoint::apply(const uint8_t *rgba8, const size_t width,
|
||||
const size_t height, const float weight) {
|
||||
LOGI(TAG, "[apply] weight: %.2f", weight);
|
||||
if (weight == 0) {
|
||||
// shortcut
|
||||
return vector<uint8_t>(rgba8, rgba8 + width * height * 4);
|
||||
}
|
||||
|
||||
const auto lut = buildLut(weight);
|
||||
vector<uint8_t> output(width * height * 4);
|
||||
#pragma omp parallel for
|
||||
for (size_t i = 0; i < width * height; ++i) {
|
||||
const auto p = i * 4;
|
||||
output[p + 0] = lut[rgba8[p + 0]];
|
||||
output[p + 1] = lut[rgba8[p + 1]];
|
||||
output[p + 2] = lut[rgba8[p + 2]];
|
||||
output[p + 3] = rgba8[p + 3];
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
vector<uint8_t> BlackPoint::buildLut(const float weight) {
|
||||
vector<uint8_t> product(256);
|
||||
const float weightAbs = std::abs(weight);
|
||||
const auto fn =
|
||||
weight > 0 ? &BlackPoint::applyInputLevel : &BlackPoint::applyOutputLevel;
|
||||
#pragma omp parallel for
|
||||
for (size_t i = 0; i < 256; ++i) {
|
||||
product[i] = fn(i, weightAbs);
|
||||
}
|
||||
return product;
|
||||
}
|
||||
|
||||
} // namespace
|
93
plugin/android/src/main/cpp/filter/contrast.cpp
Normal file
93
plugin/android/src/main/cpp/filter/contrast.cpp
Normal file
|
@ -0,0 +1,93 @@
|
|||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <jni.h>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "../exception.h"
|
||||
#include "../log.h"
|
||||
#include "../math_util.h"
|
||||
#include "../util.h"
|
||||
#include "./hslhsv.h"
|
||||
|
||||
using namespace plugin;
|
||||
using namespace std;
|
||||
|
||||
namespace {
|
||||
|
||||
class Contrast {
|
||||
public:
|
||||
std::vector<uint8_t> apply(const uint8_t *rgba8, const size_t width,
|
||||
const size_t height, const float weight);
|
||||
|
||||
private:
|
||||
static constexpr const char *TAG = "Contrast";
|
||||
};
|
||||
|
||||
inline uint8_t applySingle(const uint8_t p, const float mul) {
|
||||
return clamp(0, static_cast<int>((p - 127) * mul + 127), 0xFF);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> buildLut(const float mul);
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" JNIEXPORT jbyteArray JNICALL
|
||||
Java_com_nkming_nc_1photos_plugin_image_1processor_Contrast_applyNative(
|
||||
JNIEnv *env, jobject *thiz, jbyteArray rgba8, jint width, jint height,
|
||||
jfloat weight) {
|
||||
try {
|
||||
initOpenMp();
|
||||
RaiiContainer<jbyte> cRgba8(
|
||||
[&]() { return env->GetByteArrayElements(rgba8, nullptr); },
|
||||
[&](jbyte *obj) {
|
||||
env->ReleaseByteArrayElements(rgba8, obj, JNI_ABORT);
|
||||
});
|
||||
const auto result = Contrast().apply(
|
||||
reinterpret_cast<uint8_t *>(cRgba8.get()), width, height, 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 {
|
||||
|
||||
vector<uint8_t> Contrast::apply(const uint8_t *rgba8, const size_t width,
|
||||
const size_t height, const float weight) {
|
||||
LOGI(TAG, "[apply] weight: %.2f", weight);
|
||||
if (weight == 0) {
|
||||
// shortcut
|
||||
return vector<uint8_t>(rgba8, rgba8 + width * height * 4);
|
||||
}
|
||||
|
||||
const float mul = weight >= 0 ? weight + 1 : (weight + 1) * .4f + .6f;
|
||||
const auto lut = buildLut(mul);
|
||||
vector<uint8_t> output(width * height * 4);
|
||||
#pragma omp parallel for
|
||||
for (size_t i = 0; i < width * height; ++i) {
|
||||
const auto p = i * 4;
|
||||
output[p + 0] = lut[rgba8[p + 0]];
|
||||
output[p + 1] = lut[rgba8[p + 1]];
|
||||
output[p + 2] = lut[rgba8[p + 2]];
|
||||
output[p + 3] = rgba8[p + 3];
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
vector<uint8_t> buildLut(const float mul) {
|
||||
vector<uint8_t> product(256);
|
||||
#pragma omp parallel for
|
||||
for (size_t i = 0; i < 256; ++i) {
|
||||
product[i] = applySingle(i, mul);
|
||||
}
|
||||
return product;
|
||||
}
|
||||
|
||||
} // namespace
|
56
plugin/android/src/main/cpp/filter/curve.cpp
Normal file
56
plugin/android/src/main/cpp/filter/curve.cpp
Normal file
|
@ -0,0 +1,56 @@
|
|||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "../lib/spline/spline.h"
|
||||
#include "./curve.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace {
|
||||
|
||||
std::vector<uint8_t> buildLut(const vector<uint8_t> &from,
|
||||
const vector<uint8_t> &to);
|
||||
vector<double> transformPoints(const vector<uint8_t> &pts);
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace plugin {
|
||||
namespace filter {
|
||||
|
||||
Curve::Curve(const vector<uint8_t> &from, const vector<uint8_t> &to)
|
||||
: lut(buildLut(from, to)) {}
|
||||
|
||||
} // namespace filter
|
||||
} // namespace plugin
|
||||
|
||||
namespace {
|
||||
|
||||
std::vector<uint8_t> buildLut(const vector<uint8_t> &from,
|
||||
const vector<uint8_t> &to) {
|
||||
assert(from.size() >= 2);
|
||||
assert(from[0] == 0);
|
||||
assert(from[from.size() - 1] == 255);
|
||||
assert(to.size() >= 2);
|
||||
assert(to[0] == 0);
|
||||
assert(to[to.size() - 1] == 255);
|
||||
tk::spline spline(transformPoints(from), transformPoints(to),
|
||||
tk::spline::cspline_hermite);
|
||||
std::vector<uint8_t> lut;
|
||||
lut.reserve(256);
|
||||
for (int i = 0; i <= 0xFF; ++i) {
|
||||
lut.push_back(std::min(std::max(0, static_cast<int>(spline(i))), 0xFF));
|
||||
}
|
||||
return lut;
|
||||
}
|
||||
|
||||
vector<double> transformPoints(const vector<uint8_t> &pts) {
|
||||
vector<double> product;
|
||||
product.reserve(pts.size());
|
||||
for (const auto pt : pts) {
|
||||
product.push_back(pt);
|
||||
}
|
||||
return product;
|
||||
}
|
||||
|
||||
} // namespace
|
26
plugin/android/src/main/cpp/filter/curve.h
Normal file
26
plugin/android/src/main/cpp/filter/curve.h
Normal file
|
@ -0,0 +1,26 @@
|
|||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace plugin {
|
||||
namespace filter {
|
||||
|
||||
class Curve {
|
||||
public:
|
||||
/**
|
||||
* Construct a curve that fit values from @a from to @a to
|
||||
* @param from Control points, must be sorted, must begins with 0 and ends
|
||||
* with 255
|
||||
* @param to
|
||||
*/
|
||||
Curve(const std::vector<uint8_t> &from, const std::vector<uint8_t> &to);
|
||||
Curve(const Curve &) = default;
|
||||
Curve(Curve &&) = default;
|
||||
|
||||
uint8_t fit(const uint8_t from) const { return lut[from]; }
|
||||
|
||||
private:
|
||||
std::vector<uint8_t> lut;
|
||||
};
|
||||
|
||||
} // namespace filter
|
||||
} // namespace plugin
|
170
plugin/android/src/main/cpp/filter/hslhsv.cpp
Normal file
170
plugin/android/src/main/cpp/filter/hslhsv.cpp
Normal file
|
@ -0,0 +1,170 @@
|
|||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
#include "../math_util.h"
|
||||
#include "./hslhsv.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace plugin {
|
||||
namespace filter {
|
||||
|
||||
array<float, 3> rgb8ToHsl(const uint8_t *rgb8) {
|
||||
const auto max = std::max(std::max(rgb8[0], rgb8[1]), rgb8[2]);
|
||||
const auto min = std::min(std::min(rgb8[0], rgb8[1]), rgb8[2]);
|
||||
const auto chroma = max - min;
|
||||
|
||||
float rgbF[] = {rgb8[0] / 255.f, rgb8[1] / 255.f, rgb8[2] / 255.f};
|
||||
const auto maxF = max / 255.f;
|
||||
const auto minF = min / 255.f;
|
||||
const auto chromaF = maxF - minF;
|
||||
|
||||
const auto l = (maxF + minF) / 2;
|
||||
float h;
|
||||
if (chroma == 0) {
|
||||
h = 0;
|
||||
} else if (max == rgb8[0]) {
|
||||
h = fmodf(60 * (0 + (rgbF[1] - rgbF[2]) / chromaF) + 360, 360.f);
|
||||
} else if (max == rgb8[1]) {
|
||||
h = 60 * (2 + (rgbF[2] - rgbF[0]) / chromaF);
|
||||
} else if (max == rgb8[2]) {
|
||||
h = 60 * (4 + (rgbF[0] - rgbF[1]) / chromaF);
|
||||
}
|
||||
float s;
|
||||
if (std::abs(l - 0) < 1e-3 || std::abs(l - 1) < 1e-3) {
|
||||
s = 0;
|
||||
} else {
|
||||
s = chromaF / (1 - std::abs(2 * maxF - chromaF - 1));
|
||||
}
|
||||
return {h, s, l};
|
||||
}
|
||||
|
||||
array<float, 3> rgb8ToHsv(const uint8_t *rgb8) {
|
||||
const auto max = std::max(std::max(rgb8[0], rgb8[1]), rgb8[2]);
|
||||
const auto min = std::min(std::min(rgb8[0], rgb8[1]), rgb8[2]);
|
||||
const auto chroma = max - min;
|
||||
|
||||
float rgbF[] = {rgb8[0] / 255.f, rgb8[1] / 255.f, rgb8[2] / 255.f};
|
||||
const auto maxF = max / 255.f;
|
||||
const auto minF = min / 255.f;
|
||||
const auto chromaF = maxF - minF;
|
||||
|
||||
float h;
|
||||
if (chroma == 0) {
|
||||
h = 0;
|
||||
} else if (max == rgb8[0]) {
|
||||
h = fmodf(60 * (0 + (rgbF[1] - rgbF[2]) / chromaF) + 360, 360.f);
|
||||
} else if (max == rgb8[1]) {
|
||||
h = 60 * (2 + (rgbF[2] - rgbF[0]) / chromaF);
|
||||
} else if (max == rgb8[2]) {
|
||||
h = 60 * (4 + (rgbF[0] - rgbF[1]) / chromaF);
|
||||
}
|
||||
float s;
|
||||
if (max == 0) {
|
||||
s = 0;
|
||||
} else {
|
||||
s = chromaF / maxF;
|
||||
}
|
||||
return {h, s, maxF};
|
||||
}
|
||||
|
||||
array<uint8_t, 3> hslToRgb8(const float *hsl) {
|
||||
const auto chroma = hsl[1] * (1 - std::abs(2 * hsl[2] - 1));
|
||||
const auto h2 = hsl[0] / 60;
|
||||
const auto x = chroma * (1 - std::abs(fmodf(h2, 2) - 1));
|
||||
float r2, g2, b2;
|
||||
if (0 <= h2 && h2 < 1) {
|
||||
r2 = chroma;
|
||||
g2 = x;
|
||||
b2 = 0;
|
||||
} else if (1 <= h2 && h2 < 2) {
|
||||
r2 = x;
|
||||
g2 = chroma;
|
||||
b2 = 0;
|
||||
} else if (2 <= h2 && h2 < 3) {
|
||||
r2 = 0;
|
||||
g2 = chroma;
|
||||
b2 = x;
|
||||
} else if (3 <= h2 && h2 < 4) {
|
||||
r2 = 0;
|
||||
g2 = x;
|
||||
b2 = chroma;
|
||||
} else if (4 <= h2 && h2 < 5) {
|
||||
r2 = x;
|
||||
g2 = 0;
|
||||
b2 = chroma;
|
||||
} else {
|
||||
// 5 <= h2 && h2 < 6
|
||||
r2 = chroma;
|
||||
g2 = 0;
|
||||
b2 = x;
|
||||
}
|
||||
const auto m = hsl[2] - chroma / 2;
|
||||
return {static_cast<uint8_t>((r2 + m) * 255),
|
||||
static_cast<uint8_t>((g2 + m) * 255),
|
||||
static_cast<uint8_t>((b2 + m) * 255)};
|
||||
}
|
||||
|
||||
array<uint8_t, 3> hsvToRgb8(const float *hsv) {
|
||||
const auto chroma = hsv[2] * hsv[1];
|
||||
const auto h2 = hsv[0] / 60;
|
||||
const auto x = chroma * (1 - std::abs(fmodf(h2, 2) - 1));
|
||||
float r2, g2, b2;
|
||||
if (0 <= h2 && h2 < 1) {
|
||||
r2 = chroma;
|
||||
g2 = x;
|
||||
b2 = 0;
|
||||
} else if (1 <= h2 && h2 < 2) {
|
||||
r2 = x;
|
||||
g2 = chroma;
|
||||
b2 = 0;
|
||||
} else if (2 <= h2 && h2 < 3) {
|
||||
r2 = 0;
|
||||
g2 = chroma;
|
||||
b2 = x;
|
||||
} else if (3 <= h2 && h2 < 4) {
|
||||
r2 = 0;
|
||||
g2 = x;
|
||||
b2 = chroma;
|
||||
} else if (4 <= h2 && h2 < 5) {
|
||||
r2 = x;
|
||||
g2 = 0;
|
||||
b2 = chroma;
|
||||
} else {
|
||||
// 5 <= h2 && h2 < 6
|
||||
r2 = chroma;
|
||||
g2 = 0;
|
||||
b2 = x;
|
||||
}
|
||||
const auto m = hsv[2] - chroma;
|
||||
return {static_cast<uint8_t>((r2 + m) * 255),
|
||||
static_cast<uint8_t>((g2 + m) * 255),
|
||||
static_cast<uint8_t>((b2 + m) * 255)};
|
||||
}
|
||||
|
||||
std::array<float, 3> hslToHsv(const float *hsl) {
|
||||
const auto v = hsl[2] + hsl[1] * std::min(hsl[2], 1 - hsl[2]);
|
||||
float s;
|
||||
if (std::abs(v - 0) < 1e-3) {
|
||||
s = 0;
|
||||
} else {
|
||||
s = 2 * (1 - hsl[2] / v);
|
||||
}
|
||||
return {hsl[0], s, v};
|
||||
}
|
||||
|
||||
std::array<float, 3> hsvToHsl(const float *hsv) {
|
||||
const auto l = hsv[2] * (1 - hsv[1] / 2);
|
||||
float s;
|
||||
if (std::abs(l - 0) < 1e-3 || std::abs(l - 1) < 1e-3) {
|
||||
s = 0;
|
||||
} else {
|
||||
s = (hsv[2] - l) / std::min(l, 1 - l);
|
||||
}
|
||||
return {hsv[0], s, l};
|
||||
}
|
||||
|
||||
} // namespace filter
|
||||
} // namespace plugin
|
19
plugin/android/src/main/cpp/filter/hslhsv.h
Normal file
19
plugin/android/src/main/cpp/filter/hslhsv.h
Normal file
|
@ -0,0 +1,19 @@
|
|||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
namespace plugin {
|
||||
namespace filter {
|
||||
|
||||
std::array<float, 3> rgb8ToHsl(const uint8_t *rgb8);
|
||||
std::array<float, 3> rgb8ToHsv(const uint8_t *rgb8);
|
||||
|
||||
std::array<uint8_t, 3> hslToRgb8(const float *hsl);
|
||||
std::array<uint8_t, 3> hsvToRgb8(const float *hsv);
|
||||
|
||||
std::array<float, 3> hslToHsv(const float *hsl);
|
||||
std::array<float, 3> hsvToHsl(const float *hsv);
|
||||
|
||||
} // namespace filter
|
||||
} // namespace plugin
|
77
plugin/android/src/main/cpp/filter/saturation.cpp
Normal file
77
plugin/android/src/main/cpp/filter/saturation.cpp
Normal file
|
@ -0,0 +1,77 @@
|
|||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <jni.h>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "../exception.h"
|
||||
#include "../log.h"
|
||||
#include "../math_util.h"
|
||||
#include "../util.h"
|
||||
#include "./hslhsv.h"
|
||||
|
||||
using namespace plugin;
|
||||
using namespace std;
|
||||
|
||||
namespace {
|
||||
|
||||
class Saturation {
|
||||
public:
|
||||
std::vector<uint8_t> apply(const uint8_t *rgba8, const size_t width,
|
||||
const size_t height, const float weight);
|
||||
|
||||
private:
|
||||
static constexpr const char *TAG = "Saturation";
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" JNIEXPORT jbyteArray JNICALL
|
||||
Java_com_nkming_nc_1photos_plugin_image_1processor_Saturation_applyNative(
|
||||
JNIEnv *env, jobject *thiz, jbyteArray rgba8, jint width, jint height,
|
||||
jfloat value) {
|
||||
try {
|
||||
initOpenMp();
|
||||
RaiiContainer<jbyte> cRgba8(
|
||||
[&]() { return env->GetByteArrayElements(rgba8, nullptr); },
|
||||
[&](jbyte *obj) {
|
||||
env->ReleaseByteArrayElements(rgba8, obj, JNI_ABORT);
|
||||
});
|
||||
const auto result = Saturation().apply(
|
||||
reinterpret_cast<uint8_t *>(cRgba8.get()), width, height, value);
|
||||
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 {
|
||||
|
||||
vector<uint8_t> Saturation::apply(const uint8_t *rgba8, const size_t width,
|
||||
const size_t height, const float weight) {
|
||||
LOGI(TAG, "[apply] weight: %.2f", weight);
|
||||
if (weight == 0) {
|
||||
// shortcut
|
||||
return vector<uint8_t>(rgba8, rgba8 + width * height * 4);
|
||||
}
|
||||
|
||||
vector<uint8_t> output(width * height * 4);
|
||||
#pragma omp parallel for
|
||||
for (size_t i = 0; i < width * height; ++i) {
|
||||
const auto p = i * 4;
|
||||
auto hsl = filter::rgb8ToHsl(rgba8 + p);
|
||||
hsl[1] = clamp(0.f, hsl[1] * (1 + weight), 1.f);
|
||||
const auto &newRgb = filter::hslToRgb8(hsl.data());
|
||||
memcpy(output.data() + p, newRgb.data(), 3);
|
||||
output[p + 3] = rgba8[p + 3];
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
} // namespace
|
77
plugin/android/src/main/cpp/filter/tint.cpp
Normal file
77
plugin/android/src/main/cpp/filter/tint.cpp
Normal file
|
@ -0,0 +1,77 @@
|
|||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <jni.h>
|
||||
#include <vector>
|
||||
|
||||
#include "../exception.h"
|
||||
#include "../log.h"
|
||||
#include "../math_util.h"
|
||||
#include "../util.h"
|
||||
#include "./yuv.h"
|
||||
|
||||
using namespace plugin;
|
||||
using namespace std;
|
||||
|
||||
namespace {
|
||||
|
||||
class Tint {
|
||||
public:
|
||||
std::vector<uint8_t> apply(const uint8_t *rgba8, const size_t width,
|
||||
const size_t height, const float weight);
|
||||
|
||||
private:
|
||||
static constexpr const char *TAG = "Tint";
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" JNIEXPORT jbyteArray JNICALL
|
||||
Java_com_nkming_nc_1photos_plugin_image_1processor_Tint_applyNative(
|
||||
JNIEnv *env, jobject *thiz, jbyteArray rgba8, jint width, jint height,
|
||||
jfloat weight) {
|
||||
try {
|
||||
initOpenMp();
|
||||
RaiiContainer<jbyte> cRgba8(
|
||||
[&]() { return env->GetByteArrayElements(rgba8, nullptr); },
|
||||
[&](jbyte *obj) {
|
||||
env->ReleaseByteArrayElements(rgba8, obj, JNI_ABORT);
|
||||
});
|
||||
const auto result = Tint().apply(reinterpret_cast<uint8_t *>(cRgba8.get()),
|
||||
width, height, 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 {
|
||||
|
||||
vector<uint8_t> Tint::apply(const uint8_t *rgba8, const size_t width,
|
||||
const size_t height, const float weight) {
|
||||
LOGI(TAG, "[apply] weight: %.2f", weight);
|
||||
if (weight == 0) {
|
||||
// shortcut
|
||||
return vector<uint8_t>(rgba8, rgba8 + width * height * 4);
|
||||
}
|
||||
|
||||
vector<uint8_t> output(width * height * 4);
|
||||
#pragma omp parallel for
|
||||
for (size_t i = 0; i < width * height; ++i) {
|
||||
const auto p = i * 4;
|
||||
auto yuv = filter::rgb8ToYuv(rgba8 + p);
|
||||
// +-0.1
|
||||
yuv[1] = clamp(0.f, yuv[1] + 0.1f * weight, 1.f);
|
||||
yuv[2] = clamp(0.f, yuv[2] + 0.1f * weight, 1.f);
|
||||
const auto &newRgb = filter::yuvToRgb8(yuv.data());
|
||||
memcpy(output.data() + p, newRgb.data(), 3);
|
||||
output[p + 3] = rgba8[p + 3];
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
} // namespace
|
118
plugin/android/src/main/cpp/filter/warmth.cpp
Normal file
118
plugin/android/src/main/cpp/filter/warmth.cpp
Normal file
|
@ -0,0 +1,118 @@
|
|||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <jni.h>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "../exception.h"
|
||||
#include "../log.h"
|
||||
#include "../util.h"
|
||||
#include "./curve.h"
|
||||
|
||||
using namespace plugin;
|
||||
using namespace std;
|
||||
|
||||
namespace {
|
||||
|
||||
class Warmth {
|
||||
public:
|
||||
std::vector<uint8_t> apply(const uint8_t *rgba8, const size_t width,
|
||||
const size_t height, const float weight);
|
||||
|
||||
private:
|
||||
static filter::Curve getRCurve(const float weight);
|
||||
static unique_ptr<filter::Curve> getGCurve(const float weight);
|
||||
static filter::Curve getBCurve(const float weight);
|
||||
|
||||
static constexpr const char *TAG = "Warmth";
|
||||
};
|
||||
|
||||
inline uint8_t weighted(const uint8_t from, const uint8_t to,
|
||||
const float weight) {
|
||||
return (to - from) * weight + from;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" JNIEXPORT jbyteArray JNICALL
|
||||
Java_com_nkming_nc_1photos_plugin_image_1processor_Warmth_applyNative(
|
||||
JNIEnv *env, jobject *thiz, jbyteArray rgba8, jint width, jint height,
|
||||
jfloat weight) {
|
||||
try {
|
||||
initOpenMp();
|
||||
RaiiContainer<jbyte> cRgba8(
|
||||
[&]() { return env->GetByteArrayElements(rgba8, nullptr); },
|
||||
[&](jbyte *obj) {
|
||||
env->ReleaseByteArrayElements(rgba8, obj, JNI_ABORT);
|
||||
});
|
||||
const auto result = Warmth().apply(
|
||||
reinterpret_cast<uint8_t *>(cRgba8.get()), width, height, 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 {
|
||||
|
||||
vector<uint8_t> Warmth::apply(const uint8_t *rgba8, const size_t width,
|
||||
const size_t height, const float weight) {
|
||||
LOGI(TAG, "[apply] weight: %.2f", weight);
|
||||
if (weight == 0) {
|
||||
// shortcut
|
||||
return vector<uint8_t>(rgba8, rgba8 + width * height * 4);
|
||||
}
|
||||
const auto rCurve = getRCurve(weight);
|
||||
const auto gCurve = getGCurve(weight);
|
||||
const auto bCurve = getBCurve(weight);
|
||||
|
||||
vector<uint8_t> output(width * height * 4);
|
||||
#pragma omp parallel for
|
||||
for (size_t i = 0; i < width * height; ++i) {
|
||||
const auto p = i * 4;
|
||||
output[p + 0] = rCurve.fit(rgba8[p + 0]);
|
||||
output[p + 1] = gCurve ? gCurve->fit(rgba8[p + 1]) : rgba8[p + 1];
|
||||
output[p + 2] = bCurve.fit(rgba8[p + 2]);
|
||||
output[p + 3] = rgba8[p + 3];
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
filter::Curve Warmth::getRCurve(const float weight) {
|
||||
if (weight >= 0) {
|
||||
return filter::Curve({0, 78, 195, 255}, {0, weighted(78, 100, weight),
|
||||
weighted(195, 220, weight), 255});
|
||||
} else {
|
||||
return filter::Curve({0, 95, 220, 255},
|
||||
{0, weighted(95, 60, std::abs(weight)),
|
||||
weighted(220, 185, std::abs(weight)), 255});
|
||||
}
|
||||
}
|
||||
|
||||
unique_ptr<filter::Curve> Warmth::getGCurve(const float weight) {
|
||||
if (weight >= 0) {
|
||||
return make_unique<filter::Curve>(
|
||||
vector<uint8_t>{0, 135, 255},
|
||||
vector<uint8_t>{0, weighted(135, 125, weight), 255});
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
filter::Curve Warmth::getBCurve(const float weight) {
|
||||
if (weight >= 0) {
|
||||
return filter::Curve({0, 95, 220, 255}, {0, weighted(95, 60, weight),
|
||||
weighted(220, 185, weight), 255});
|
||||
} else {
|
||||
return filter::Curve({0, 78, 195, 255},
|
||||
{0, weighted(78, 100, std::abs(weight)),
|
||||
weighted(195, 220, std::abs(weight)), 255});
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
40
plugin/android/src/main/cpp/filter/yuv.cpp
Normal file
40
plugin/android/src/main/cpp/filter/yuv.cpp
Normal file
|
@ -0,0 +1,40 @@
|
|||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
#include "../math_util.h"
|
||||
#include "./yuv.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace plugin {
|
||||
namespace filter {
|
||||
|
||||
array<float, 3> rgb8ToYuv(const uint8_t *rgb8) {
|
||||
const float rgbF[] = {rgb8[0] / 255.f, rgb8[1] / 255.f, rgb8[2] / 255.f};
|
||||
return {
|
||||
clamp(0.f, rgbF[0] * .299f + rgbF[1] * .587f + rgbF[2] * .114f, 1.f),
|
||||
clamp(0.f,
|
||||
rgbF[0] * -.168736f + rgbF[1] * -.331264f + rgbF[2] * .5f + .5f,
|
||||
1.f),
|
||||
clamp(0.f,
|
||||
rgbF[0] * .5f + rgbF[1] * -.418688f + rgbF[2] * -.081312f + .5f,
|
||||
1.f),
|
||||
};
|
||||
}
|
||||
|
||||
array<uint8_t, 3> yuvToRgb8(const float *yuv) {
|
||||
const float yuv_[] = {yuv[0], yuv[1] - .5f, yuv[2] - .5f};
|
||||
const float rgbF[] = {
|
||||
yuv_[0] + yuv_[2] * 1.4f,
|
||||
yuv_[0] + yuv_[1] * -.343f + yuv_[2] * -.711f,
|
||||
yuv_[0] + yuv_[1] * 1.765f,
|
||||
};
|
||||
return {
|
||||
static_cast<uint8_t>(clamp<int>(0, rgbF[0] * 255, 255)),
|
||||
static_cast<uint8_t>(clamp<int>(0, rgbF[1] * 255, 255)),
|
||||
static_cast<uint8_t>(clamp<int>(0, rgbF[2] * 255, 255)),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace filter
|
||||
} // namespace plugin
|
26
plugin/android/src/main/cpp/filter/yuv.h
Normal file
26
plugin/android/src/main/cpp/filter/yuv.h
Normal file
|
@ -0,0 +1,26 @@
|
|||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
namespace plugin {
|
||||
namespace filter {
|
||||
|
||||
/**
|
||||
* Map a RGB color to full range YCbCr
|
||||
*
|
||||
* @param rgb8
|
||||
* @return Returned values are in the range of 0 to 1
|
||||
*/
|
||||
std::array<float, 3> rgb8ToYuv(const uint8_t *rgb8);
|
||||
|
||||
/**
|
||||
* Map a full range YCbCr color to RGB
|
||||
*
|
||||
* @param yuv
|
||||
* @return
|
||||
*/
|
||||
std::array<uint8_t, 3> yuvToRgb8(const float *yuv);
|
||||
|
||||
} // namespace filter
|
||||
} // namespace plugin
|
340
plugin/android/src/main/cpp/lib/spline/LICENSE
Normal file
340
plugin/android/src/main/cpp/lib/spline/LICENSE
Normal file
|
@ -0,0 +1,340 @@
|
|||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
{description}
|
||||
Copyright (C) {year} {fullname}
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
{signature of Ty Coon}, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
||||
|
763
plugin/android/src/main/cpp/lib/spline/spline.cpp
Normal file
763
plugin/android/src/main/cpp/lib/spline/spline.cpp
Normal file
|
@ -0,0 +1,763 @@
|
|||
#include "./spline.h"
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
// band matrix solver
|
||||
class band_matrix {
|
||||
private:
|
||||
std::vector<std::vector<double>> m_upper; // upper band
|
||||
std::vector<std::vector<double>> m_lower; // lower band
|
||||
public:
|
||||
band_matrix(){}; // constructor
|
||||
band_matrix(int dim, int n_u, int n_l); // constructor
|
||||
~band_matrix(){}; // destructor
|
||||
void resize(int dim, int n_u, int n_l); // init with dim,n_u,n_l
|
||||
int dim() const; // matrix dimension
|
||||
int num_upper() const { return (int)m_upper.size() - 1; }
|
||||
int num_lower() const { return (int)m_lower.size() - 1; }
|
||||
// access operator
|
||||
double &operator()(int i, int j); // write
|
||||
double operator()(int i, int j) const; // read
|
||||
// we can store an additional diagonal (in m_lower)
|
||||
double &saved_diag(int i);
|
||||
double saved_diag(int i) const;
|
||||
void lu_decompose();
|
||||
std::vector<double> r_solve(const std::vector<double> &b) const;
|
||||
std::vector<double> l_solve(const std::vector<double> &b) const;
|
||||
std::vector<double> lu_solve(const std::vector<double> &b,
|
||||
bool is_lu_decomposed = false);
|
||||
};
|
||||
|
||||
double get_eps();
|
||||
|
||||
std::vector<double> solve_cubic(double a, double b, double c, double d,
|
||||
int newton_iter = 0);
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace tk {
|
||||
|
||||
void spline::set_boundary(spline::bd_type left, double left_value,
|
||||
spline::bd_type right, double right_value) {
|
||||
assert(m_x.size() == 0); // set_points() must not have happened yet
|
||||
m_left = left;
|
||||
m_right = right;
|
||||
m_left_value = left_value;
|
||||
m_right_value = right_value;
|
||||
}
|
||||
|
||||
void spline::set_coeffs_from_b() {
|
||||
assert(m_x.size() == m_y.size());
|
||||
assert(m_x.size() == m_b.size());
|
||||
assert(m_x.size() > 2);
|
||||
size_t n = m_b.size();
|
||||
if (m_c.size() != n)
|
||||
m_c.resize(n);
|
||||
if (m_d.size() != n)
|
||||
m_d.resize(n);
|
||||
|
||||
for (size_t i = 0; i < n - 1; i++) {
|
||||
const double h = m_x[i + 1] - m_x[i];
|
||||
// from continuity and differentiability condition
|
||||
m_c[i] =
|
||||
(3.0 * (m_y[i + 1] - m_y[i]) / h - (2.0 * m_b[i] + m_b[i + 1])) / h;
|
||||
// from differentiability condition
|
||||
m_d[i] = ((m_b[i + 1] - m_b[i]) / (3.0 * h) - 2.0 / 3.0 * m_c[i]) / h;
|
||||
}
|
||||
|
||||
// for left extrapolation coefficients
|
||||
m_c0 = (m_left == first_deriv) ? 0.0 : m_c[0];
|
||||
}
|
||||
|
||||
void spline::set_points(const std::vector<double> &x,
|
||||
const std::vector<double> &y, spline_type type) {
|
||||
assert(x.size() == y.size());
|
||||
assert(x.size() >= 3);
|
||||
// not-a-knot with 3 points has many solutions
|
||||
if (m_left == not_a_knot || m_right == not_a_knot)
|
||||
assert(x.size() >= 4);
|
||||
m_type = type;
|
||||
m_made_monotonic = false;
|
||||
m_x = x;
|
||||
m_y = y;
|
||||
int n = (int)x.size();
|
||||
// check strict monotonicity of input vector x
|
||||
for (int i = 0; i < n - 1; i++) {
|
||||
assert(m_x[i] < m_x[i + 1]);
|
||||
}
|
||||
|
||||
if (type == linear) {
|
||||
// linear interpolation
|
||||
m_d.resize(n);
|
||||
m_c.resize(n);
|
||||
m_b.resize(n);
|
||||
for (int i = 0; i < n - 1; i++) {
|
||||
m_d[i] = 0.0;
|
||||
m_c[i] = 0.0;
|
||||
m_b[i] = (m_y[i + 1] - m_y[i]) / (m_x[i + 1] - m_x[i]);
|
||||
}
|
||||
// ignore boundary conditions, set slope equal to the last segment
|
||||
m_b[n - 1] = m_b[n - 2];
|
||||
m_c[n - 1] = 0.0;
|
||||
m_d[n - 1] = 0.0;
|
||||
} else if (type == cspline) {
|
||||
// classical cubic splines which are C^2 (twice cont differentiable)
|
||||
// this requires solving an equation system
|
||||
|
||||
// setting up the matrix and right hand side of the equation system
|
||||
// for the parameters b[]
|
||||
int n_upper = (m_left == spline::not_a_knot) ? 2 : 1;
|
||||
int n_lower = (m_right == spline::not_a_knot) ? 2 : 1;
|
||||
band_matrix A(n, n_upper, n_lower);
|
||||
std::vector<double> rhs(n);
|
||||
for (int i = 1; i < n - 1; i++) {
|
||||
A(i, i - 1) = 1.0 / 3.0 * (x[i] - x[i - 1]);
|
||||
A(i, i) = 2.0 / 3.0 * (x[i + 1] - x[i - 1]);
|
||||
A(i, i + 1) = 1.0 / 3.0 * (x[i + 1] - x[i]);
|
||||
rhs[i] = (y[i + 1] - y[i]) / (x[i + 1] - x[i]) -
|
||||
(y[i] - y[i - 1]) / (x[i] - x[i - 1]);
|
||||
}
|
||||
// boundary conditions
|
||||
if (m_left == spline::second_deriv) {
|
||||
// 2*c[0] = f''
|
||||
A(0, 0) = 2.0;
|
||||
A(0, 1) = 0.0;
|
||||
rhs[0] = m_left_value;
|
||||
} else if (m_left == spline::first_deriv) {
|
||||
// b[0] = f', needs to be re-expressed in terms of c:
|
||||
// (2c[0]+c[1])(x[1]-x[0]) = 3 ((y[1]-y[0])/(x[1]-x[0]) - f')
|
||||
A(0, 0) = 2.0 * (x[1] - x[0]);
|
||||
A(0, 1) = 1.0 * (x[1] - x[0]);
|
||||
rhs[0] = 3.0 * ((y[1] - y[0]) / (x[1] - x[0]) - m_left_value);
|
||||
} else if (m_left == spline::not_a_knot) {
|
||||
// f'''(x[1]) exists, i.e. d[0]=d[1], or re-expressed in c:
|
||||
// -h1*c[0] + (h0+h1)*c[1] - h0*c[2] = 0
|
||||
A(0, 0) = -(x[2] - x[1]);
|
||||
A(0, 1) = x[2] - x[0];
|
||||
A(0, 2) = -(x[1] - x[0]);
|
||||
rhs[0] = 0.0;
|
||||
} else {
|
||||
assert(false);
|
||||
}
|
||||
if (m_right == spline::second_deriv) {
|
||||
// 2*c[n-1] = f''
|
||||
A(n - 1, n - 1) = 2.0;
|
||||
A(n - 1, n - 2) = 0.0;
|
||||
rhs[n - 1] = m_right_value;
|
||||
} else if (m_right == spline::first_deriv) {
|
||||
// b[n-1] = f', needs to be re-expressed in terms of c:
|
||||
// (c[n-2]+2c[n-1])(x[n-1]-x[n-2])
|
||||
// = 3 (f' - (y[n-1]-y[n-2])/(x[n-1]-x[n-2]))
|
||||
A(n - 1, n - 1) = 2.0 * (x[n - 1] - x[n - 2]);
|
||||
A(n - 1, n - 2) = 1.0 * (x[n - 1] - x[n - 2]);
|
||||
rhs[n - 1] =
|
||||
3.0 * (m_right_value - (y[n - 1] - y[n - 2]) / (x[n - 1] - x[n - 2]));
|
||||
} else if (m_right == spline::not_a_knot) {
|
||||
// f'''(x[n-2]) exists, i.e. d[n-3]=d[n-2], or re-expressed in c:
|
||||
// -h_{n-2}*c[n-3] + (h_{n-3}+h_{n-2})*c[n-2] - h_{n-3}*c[n-1] = 0
|
||||
A(n - 1, n - 3) = -(x[n - 1] - x[n - 2]);
|
||||
A(n - 1, n - 2) = x[n - 1] - x[n - 3];
|
||||
A(n - 1, n - 1) = -(x[n - 2] - x[n - 3]);
|
||||
rhs[0] = 0.0;
|
||||
} else {
|
||||
assert(false);
|
||||
}
|
||||
|
||||
// solve the equation system to obtain the parameters c[]
|
||||
m_c = A.lu_solve(rhs);
|
||||
|
||||
// calculate parameters b[] and d[] based on c[]
|
||||
m_d.resize(n);
|
||||
m_b.resize(n);
|
||||
for (int i = 0; i < n - 1; i++) {
|
||||
m_d[i] = 1.0 / 3.0 * (m_c[i + 1] - m_c[i]) / (x[i + 1] - x[i]);
|
||||
m_b[i] = (y[i + 1] - y[i]) / (x[i + 1] - x[i]) -
|
||||
1.0 / 3.0 * (2.0 * m_c[i] + m_c[i + 1]) * (x[i + 1] - x[i]);
|
||||
}
|
||||
// for the right extrapolation coefficients (zero cubic term)
|
||||
// f_{n-1}(x) = y_{n-1} + b*(x-x_{n-1}) + c*(x-x_{n-1})^2
|
||||
double h = x[n - 1] - x[n - 2];
|
||||
// m_c[n-1] is determined by the boundary condition
|
||||
m_d[n - 1] = 0.0;
|
||||
m_b[n - 1] = 3.0 * m_d[n - 2] * h * h + 2.0 * m_c[n - 2] * h +
|
||||
m_b[n - 2]; // = f'_{n-2}(x_{n-1})
|
||||
if (m_right == first_deriv)
|
||||
m_c[n - 1] = 0.0; // force linear extrapolation
|
||||
|
||||
} else if (type == cspline_hermite) {
|
||||
// hermite cubic splines which are C^1 (cont. differentiable)
|
||||
// and derivatives are specified on each grid point
|
||||
// (here we use 3-point finite differences)
|
||||
m_b.resize(n);
|
||||
m_c.resize(n);
|
||||
m_d.resize(n);
|
||||
// set b to match 1st order derivative finite difference
|
||||
for (int i = 1; i < n - 1; i++) {
|
||||
const double h = m_x[i + 1] - m_x[i];
|
||||
const double hl = m_x[i] - m_x[i - 1];
|
||||
m_b[i] = -h / (hl * (hl + h)) * m_y[i - 1] +
|
||||
(h - hl) / (hl * h) * m_y[i] + hl / (h * (hl + h)) * m_y[i + 1];
|
||||
}
|
||||
// boundary conditions determine b[0] and b[n-1]
|
||||
if (m_left == first_deriv) {
|
||||
m_b[0] = m_left_value;
|
||||
} else if (m_left == second_deriv) {
|
||||
const double h = m_x[1] - m_x[0];
|
||||
m_b[0] = 0.5 *
|
||||
(-m_b[1] - 0.5 * m_left_value * h + 3.0 * (m_y[1] - m_y[0]) / h);
|
||||
} else if (m_left == not_a_knot) {
|
||||
// f''' continuous at x[1]
|
||||
const double h0 = m_x[1] - m_x[0];
|
||||
const double h1 = m_x[2] - m_x[1];
|
||||
m_b[0] = -m_b[1] + 2.0 * (m_y[1] - m_y[0]) / h0 +
|
||||
h0 * h0 / (h1 * h1) *
|
||||
(m_b[1] + m_b[2] - 2.0 * (m_y[2] - m_y[1]) / h1);
|
||||
} else {
|
||||
assert(false);
|
||||
}
|
||||
if (m_right == first_deriv) {
|
||||
m_b[n - 1] = m_right_value;
|
||||
m_c[n - 1] = 0.0;
|
||||
} else if (m_right == second_deriv) {
|
||||
const double h = m_x[n - 1] - m_x[n - 2];
|
||||
m_b[n - 1] = 0.5 * (-m_b[n - 2] + 0.5 * m_right_value * h +
|
||||
3.0 * (m_y[n - 1] - m_y[n - 2]) / h);
|
||||
m_c[n - 1] = 0.5 * m_right_value;
|
||||
} else if (m_right == not_a_knot) {
|
||||
// f''' continuous at x[n-2]
|
||||
const double h0 = m_x[n - 2] - m_x[n - 3];
|
||||
const double h1 = m_x[n - 1] - m_x[n - 2];
|
||||
m_b[n - 1] =
|
||||
-m_b[n - 2] + 2.0 * (m_y[n - 1] - m_y[n - 2]) / h1 +
|
||||
h1 * h1 / (h0 * h0) *
|
||||
(m_b[n - 3] + m_b[n - 2] - 2.0 * (m_y[n - 2] - m_y[n - 3]) / h0);
|
||||
// f'' continuous at x[n-1]: c[n-1] = 3*d[n-2]*h[n-2] + c[n-1]
|
||||
m_c[n - 1] = (m_b[n - 2] + 2.0 * m_b[n - 1]) / h1 -
|
||||
3.0 * (m_y[n - 1] - m_y[n - 2]) / (h1 * h1);
|
||||
} else {
|
||||
assert(false);
|
||||
}
|
||||
m_d[n - 1] = 0.0;
|
||||
|
||||
// parameters c and d are determined by continuity and differentiability
|
||||
set_coeffs_from_b();
|
||||
|
||||
} else {
|
||||
assert(false);
|
||||
}
|
||||
|
||||
// for left extrapolation coefficients
|
||||
m_c0 = (m_left == first_deriv) ? 0.0 : m_c[0];
|
||||
}
|
||||
|
||||
bool spline::make_monotonic() {
|
||||
assert(m_x.size() == m_y.size());
|
||||
assert(m_x.size() == m_b.size());
|
||||
assert(m_x.size() > 2);
|
||||
bool modified = false;
|
||||
const int n = (int)m_x.size();
|
||||
// make sure: input data monotonic increasing --> b_i>=0
|
||||
// input data monotonic decreasing --> b_i<=0
|
||||
for (int i = 0; i < n; i++) {
|
||||
int im1 = std::max(i - 1, 0);
|
||||
int ip1 = std::min(i + 1, n - 1);
|
||||
if (((m_y[im1] <= m_y[i]) && (m_y[i] <= m_y[ip1]) && m_b[i] < 0.0) ||
|
||||
((m_y[im1] >= m_y[i]) && (m_y[i] >= m_y[ip1]) && m_b[i] > 0.0)) {
|
||||
modified = true;
|
||||
m_b[i] = 0.0;
|
||||
}
|
||||
}
|
||||
// if input data is monotonic (b[i], b[i+1], avg have all the same sign)
|
||||
// ensure a sufficient criteria for monotonicity is satisfied:
|
||||
// sqrt(b[i]^2+b[i+1]^2) <= 3 |avg|, with avg=(y[i+1]-y[i])/h,
|
||||
for (int i = 0; i < n - 1; i++) {
|
||||
double h = m_x[i + 1] - m_x[i];
|
||||
double avg = (m_y[i + 1] - m_y[i]) / h;
|
||||
if (avg == 0.0 && (m_b[i] != 0.0 || m_b[i + 1] != 0.0)) {
|
||||
modified = true;
|
||||
m_b[i] = 0.0;
|
||||
m_b[i + 1] = 0.0;
|
||||
} else if ((m_b[i] >= 0.0 && m_b[i + 1] >= 0.0 && avg > 0.0) ||
|
||||
(m_b[i] <= 0.0 && m_b[i + 1] <= 0.0 && avg < 0.0)) {
|
||||
// input data is monotonic
|
||||
double r =
|
||||
sqrt(m_b[i] * m_b[i] + m_b[i + 1] * m_b[i + 1]) / std::fabs(avg);
|
||||
if (r > 3.0) {
|
||||
// sufficient criteria for monotonicity: r<=3
|
||||
// adjust b[i] and b[i+1]
|
||||
modified = true;
|
||||
m_b[i] *= (3.0 / r);
|
||||
m_b[i + 1] *= (3.0 / r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (modified == true) {
|
||||
set_coeffs_from_b();
|
||||
m_made_monotonic = true;
|
||||
}
|
||||
|
||||
return modified;
|
||||
}
|
||||
|
||||
// return the closest idx so that m_x[idx] <= x (return 0 if x<m_x[0])
|
||||
size_t spline::find_closest(double x) const {
|
||||
std::vector<double>::const_iterator it;
|
||||
it = std::upper_bound(m_x.begin(), m_x.end(), x); // *it > x
|
||||
size_t idx = std::max(int(it - m_x.begin()) - 1, 0); // m_x[idx] <= x
|
||||
return idx;
|
||||
}
|
||||
|
||||
double spline::operator()(double x) const {
|
||||
// polynomial evaluation using Horner's scheme
|
||||
// TODO: consider more numerically accurate algorithms, e.g.:
|
||||
// - Clenshaw
|
||||
// - Even-Odd method by A.C.R. Newbery
|
||||
// - Compensated Horner Scheme
|
||||
size_t n = m_x.size();
|
||||
size_t idx = find_closest(x);
|
||||
|
||||
double h = x - m_x[idx];
|
||||
double interpol;
|
||||
if (x < m_x[0]) {
|
||||
// extrapolation to the left
|
||||
interpol = (m_c0 * h + m_b[0]) * h + m_y[0];
|
||||
} else if (x > m_x[n - 1]) {
|
||||
// extrapolation to the right
|
||||
interpol = (m_c[n - 1] * h + m_b[n - 1]) * h + m_y[n - 1];
|
||||
} else {
|
||||
// interpolation
|
||||
interpol = ((m_d[idx] * h + m_c[idx]) * h + m_b[idx]) * h + m_y[idx];
|
||||
}
|
||||
return interpol;
|
||||
}
|
||||
|
||||
double spline::deriv(int order, double x) const {
|
||||
assert(order > 0);
|
||||
size_t n = m_x.size();
|
||||
size_t idx = find_closest(x);
|
||||
|
||||
double h = x - m_x[idx];
|
||||
double interpol;
|
||||
if (x < m_x[0]) {
|
||||
// extrapolation to the left
|
||||
switch (order) {
|
||||
case 1:
|
||||
interpol = 2.0 * m_c0 * h + m_b[0];
|
||||
break;
|
||||
case 2:
|
||||
interpol = 2.0 * m_c0;
|
||||
break;
|
||||
default:
|
||||
interpol = 0.0;
|
||||
break;
|
||||
}
|
||||
} else if (x > m_x[n - 1]) {
|
||||
// extrapolation to the right
|
||||
switch (order) {
|
||||
case 1:
|
||||
interpol = 2.0 * m_c[n - 1] * h + m_b[n - 1];
|
||||
break;
|
||||
case 2:
|
||||
interpol = 2.0 * m_c[n - 1];
|
||||
break;
|
||||
default:
|
||||
interpol = 0.0;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// interpolation
|
||||
switch (order) {
|
||||
case 1:
|
||||
interpol = (3.0 * m_d[idx] * h + 2.0 * m_c[idx]) * h + m_b[idx];
|
||||
break;
|
||||
case 2:
|
||||
interpol = 6.0 * m_d[idx] * h + 2.0 * m_c[idx];
|
||||
break;
|
||||
case 3:
|
||||
interpol = 6.0 * m_d[idx];
|
||||
break;
|
||||
default:
|
||||
interpol = 0.0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return interpol;
|
||||
}
|
||||
|
||||
std::vector<double> spline::solve(double y, bool ignore_extrapolation) const {
|
||||
std::vector<double> x; // roots for the entire spline
|
||||
std::vector<double> root; // roots for each piecewise cubic
|
||||
const size_t n = m_x.size();
|
||||
|
||||
// left extrapolation
|
||||
if (ignore_extrapolation == false) {
|
||||
root = solve_cubic(m_y[0] - y, m_b[0], m_c0, 0.0, 1);
|
||||
for (size_t j = 0; j < root.size(); j++) {
|
||||
if (root[j] < 0.0) {
|
||||
x.push_back(m_x[0] + root[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// brute force check if piecewise cubic has roots in their resp. segment
|
||||
// TODO: make more efficient
|
||||
for (size_t i = 0; i < n - 1; i++) {
|
||||
root = solve_cubic(m_y[i] - y, m_b[i], m_c[i], m_d[i], 1);
|
||||
for (size_t j = 0; j < root.size(); j++) {
|
||||
double h = (i > 0) ? (m_x[i] - m_x[i - 1]) : 0.0;
|
||||
double eps = get_eps() * 512.0 * std::min(h, 1.0);
|
||||
if ((-eps <= root[j]) && (root[j] < m_x[i + 1] - m_x[i])) {
|
||||
double new_root = m_x[i] + root[j];
|
||||
if (x.size() > 0 && x.back() + eps > new_root) {
|
||||
x.back() = new_root; // avoid spurious duplicate roots
|
||||
} else {
|
||||
x.push_back(new_root);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// right extrapolation
|
||||
if (ignore_extrapolation == false) {
|
||||
root = solve_cubic(m_y[n - 1] - y, m_b[n - 1], m_c[n - 1], 0.0, 1);
|
||||
for (size_t j = 0; j < root.size(); j++) {
|
||||
if (0.0 <= root[j]) {
|
||||
x.push_back(m_x[n - 1] + root[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return x;
|
||||
};
|
||||
|
||||
std::string spline::info() const {
|
||||
std::stringstream ss;
|
||||
ss << "type " << m_type << ", left boundary deriv " << m_left << " = ";
|
||||
ss << m_left_value << ", right boundary deriv " << m_right << " = ";
|
||||
ss << m_right_value << std::endl;
|
||||
if (m_made_monotonic) {
|
||||
ss << "(spline has been adjusted for piece-wise monotonicity)";
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
} // namespace tk
|
||||
|
||||
namespace {
|
||||
|
||||
// band_matrix implementation
|
||||
// -------------------------
|
||||
|
||||
band_matrix::band_matrix(int dim, int n_u, int n_l) { resize(dim, n_u, n_l); }
|
||||
void band_matrix::resize(int dim, int n_u, int n_l) {
|
||||
assert(dim > 0);
|
||||
assert(n_u >= 0);
|
||||
assert(n_l >= 0);
|
||||
m_upper.resize(n_u + 1);
|
||||
m_lower.resize(n_l + 1);
|
||||
for (size_t i = 0; i < m_upper.size(); i++) {
|
||||
m_upper[i].resize(dim);
|
||||
}
|
||||
for (size_t i = 0; i < m_lower.size(); i++) {
|
||||
m_lower[i].resize(dim);
|
||||
}
|
||||
}
|
||||
int band_matrix::dim() const {
|
||||
if (m_upper.size() > 0) {
|
||||
return m_upper[0].size();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// defines the new operator (), so that we can access the elements
|
||||
// by A(i,j), index going from i=0,...,dim()-1
|
||||
double &band_matrix::operator()(int i, int j) {
|
||||
int k = j - i; // what band is the entry
|
||||
assert((i >= 0) && (i < dim()) && (j >= 0) && (j < dim()));
|
||||
assert((-num_lower() <= k) && (k <= num_upper()));
|
||||
// k=0 -> diagonal, k<0 lower left part, k>0 upper right part
|
||||
if (k >= 0)
|
||||
return m_upper[k][i];
|
||||
else
|
||||
return m_lower[-k][i];
|
||||
}
|
||||
double band_matrix::operator()(int i, int j) const {
|
||||
int k = j - i; // what band is the entry
|
||||
assert((i >= 0) && (i < dim()) && (j >= 0) && (j < dim()));
|
||||
assert((-num_lower() <= k) && (k <= num_upper()));
|
||||
// k=0 -> diagonal, k<0 lower left part, k>0 upper right part
|
||||
if (k >= 0)
|
||||
return m_upper[k][i];
|
||||
else
|
||||
return m_lower[-k][i];
|
||||
}
|
||||
// second diag (used in LU decomposition), saved in m_lower
|
||||
double band_matrix::saved_diag(int i) const {
|
||||
assert((i >= 0) && (i < dim()));
|
||||
return m_lower[0][i];
|
||||
}
|
||||
double &band_matrix::saved_diag(int i) {
|
||||
assert((i >= 0) && (i < dim()));
|
||||
return m_lower[0][i];
|
||||
}
|
||||
|
||||
// LR-Decomposition of a band matrix
|
||||
void band_matrix::lu_decompose() {
|
||||
int i_max, j_max;
|
||||
int j_min;
|
||||
double x;
|
||||
|
||||
// preconditioning
|
||||
// normalize column i so that a_ii=1
|
||||
for (int i = 0; i < this->dim(); i++) {
|
||||
assert(this->operator()(i, i) != 0.0);
|
||||
this->saved_diag(i) = 1.0 / this->operator()(i, i);
|
||||
j_min = std::max(0, i - this->num_lower());
|
||||
j_max = std::min(this->dim() - 1, i + this->num_upper());
|
||||
for (int j = j_min; j <= j_max; j++) {
|
||||
this->operator()(i, j) *= this->saved_diag(i);
|
||||
}
|
||||
this->operator()(i, i) = 1.0; // prevents rounding errors
|
||||
}
|
||||
|
||||
// Gauss LR-Decomposition
|
||||
for (int k = 0; k < this->dim(); k++) {
|
||||
i_max = std::min(this->dim() - 1,
|
||||
k + this->num_lower()); // num_lower not a mistake!
|
||||
for (int i = k + 1; i <= i_max; i++) {
|
||||
assert(this->operator()(k, k) != 0.0);
|
||||
x = -this->operator()(i, k) / this->operator()(k, k);
|
||||
this->operator()(i, k) = -x; // assembly part of L
|
||||
j_max = std::min(this->dim() - 1, k + this->num_upper());
|
||||
for (int j = k + 1; j <= j_max; j++) {
|
||||
// assembly part of R
|
||||
this->operator()(i, j) =
|
||||
this->operator()(i, j) + x * this->operator()(k, j);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// solves Ly=b
|
||||
std::vector<double> band_matrix::l_solve(const std::vector<double> &b) const {
|
||||
assert(this->dim() == (int)b.size());
|
||||
std::vector<double> x(this->dim());
|
||||
int j_start;
|
||||
double sum;
|
||||
for (int i = 0; i < this->dim(); i++) {
|
||||
sum = 0;
|
||||
j_start = std::max(0, i - this->num_lower());
|
||||
for (int j = j_start; j < i; j++)
|
||||
sum += this->operator()(i, j) * x[j];
|
||||
x[i] = (b[i] * this->saved_diag(i)) - sum;
|
||||
}
|
||||
return x;
|
||||
}
|
||||
// solves Rx=y
|
||||
std::vector<double> band_matrix::r_solve(const std::vector<double> &b) const {
|
||||
assert(this->dim() == (int)b.size());
|
||||
std::vector<double> x(this->dim());
|
||||
int j_stop;
|
||||
double sum;
|
||||
for (int i = this->dim() - 1; i >= 0; i--) {
|
||||
sum = 0;
|
||||
j_stop = std::min(this->dim() - 1, i + this->num_upper());
|
||||
for (int j = i + 1; j <= j_stop; j++)
|
||||
sum += this->operator()(i, j) * x[j];
|
||||
x[i] = (b[i] - sum) / this->operator()(i, i);
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
std::vector<double> band_matrix::lu_solve(const std::vector<double> &b,
|
||||
bool is_lu_decomposed) {
|
||||
assert(this->dim() == (int)b.size());
|
||||
std::vector<double> x, y;
|
||||
if (is_lu_decomposed == false) {
|
||||
this->lu_decompose();
|
||||
}
|
||||
y = this->l_solve(b);
|
||||
x = this->r_solve(y);
|
||||
return x;
|
||||
}
|
||||
|
||||
// machine precision of a double, i.e. the successor of 1 is 1+eps
|
||||
double get_eps() {
|
||||
// return std::numeric_limits<double>::epsilon(); // __DBL_EPSILON__
|
||||
return 2.2204460492503131e-16; // 2^-52
|
||||
}
|
||||
|
||||
// solutions for a + b*x = 0
|
||||
std::vector<double> solve_linear(double a, double b) {
|
||||
std::vector<double> x; // roots
|
||||
if (b == 0.0) {
|
||||
if (a == 0.0) {
|
||||
// 0*x = 0
|
||||
x.resize(1);
|
||||
x[0] = 0.0; // any x solves it but we need to pick one
|
||||
return x;
|
||||
} else {
|
||||
// 0*x + ... = 0, no solution
|
||||
return x;
|
||||
}
|
||||
} else {
|
||||
x.resize(1);
|
||||
x[0] = -a / b;
|
||||
return x;
|
||||
}
|
||||
}
|
||||
|
||||
// solutions for a + b*x + c*x^2 = 0
|
||||
std::vector<double> solve_quadratic(double a, double b, double c,
|
||||
int newton_iter = 0) {
|
||||
if (c == 0.0) {
|
||||
return solve_linear(a, b);
|
||||
}
|
||||
// rescale so that we solve x^2 + 2p x + q = (x+p)^2 + q - p^2 = 0
|
||||
double p = 0.5 * b / c;
|
||||
double q = a / c;
|
||||
double discr = p * p - q;
|
||||
const double eps = 0.5 * get_eps();
|
||||
double discr_err = (6.0 * (p * p) + 3.0 * fabs(q) + fabs(discr)) * eps;
|
||||
|
||||
std::vector<double> x; // roots
|
||||
if (fabs(discr) <= discr_err) {
|
||||
// discriminant is zero --> one root
|
||||
x.resize(1);
|
||||
x[0] = -p;
|
||||
} else if (discr < 0) {
|
||||
// no root
|
||||
} else {
|
||||
// two roots
|
||||
x.resize(2);
|
||||
x[0] = -p - sqrt(discr);
|
||||
x[1] = -p + sqrt(discr);
|
||||
}
|
||||
|
||||
// improve solution via newton steps
|
||||
for (size_t i = 0; i < x.size(); i++) {
|
||||
for (int k = 0; k < newton_iter; k++) {
|
||||
double f = (c * x[i] + b) * x[i] + a;
|
||||
double f1 = 2.0 * c * x[i] + b;
|
||||
// only adjust if slope is large enough
|
||||
if (fabs(f1) > 1e-8) {
|
||||
x[i] -= f / f1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return x;
|
||||
}
|
||||
|
||||
// solutions for the cubic equation: a + b*x +c*x^2 + d*x^3 = 0
|
||||
// this is a naive implementation of the analytic solution without
|
||||
// optimisation for speed or numerical accuracy
|
||||
// newton_iter: number of newton iterations to improve analytical solution
|
||||
// see also
|
||||
// gsl: gsl_poly_solve_cubic() in solve_cubic.c
|
||||
// octave: roots.m - via eigenvalues of the Frobenius companion matrix
|
||||
std::vector<double> solve_cubic(double a, double b, double c, double d,
|
||||
int newton_iter) {
|
||||
if (d == 0.0) {
|
||||
return solve_quadratic(a, b, c, newton_iter);
|
||||
}
|
||||
|
||||
// convert to normalised form: a + bx + cx^2 + x^3 = 0
|
||||
if (d != 1.0) {
|
||||
a /= d;
|
||||
b /= d;
|
||||
c /= d;
|
||||
}
|
||||
|
||||
// convert to depressed cubic: z^3 - 3pz - 2q = 0
|
||||
// via substitution: z = x + c/3
|
||||
std::vector<double> z; // roots of the depressed cubic
|
||||
double p = -(1.0 / 3.0) * b + (1.0 / 9.0) * (c * c);
|
||||
double r = 2.0 * (c * c) - 9.0 * b;
|
||||
double q = -0.5 * a - (1.0 / 54.0) * (c * r);
|
||||
double discr = p * p * p - q * q; // discriminant
|
||||
// calculating numerical round-off errors with assumptions:
|
||||
// - each operation is precise but each intermediate result x
|
||||
// when stored has max error of x*eps
|
||||
// - only multiplication with a power of 2 introduces no new error
|
||||
// - a,b,c,d and some fractions (e.g. 1/3) have rounding errors eps
|
||||
// - p_err << |p|, q_err << |q|, ... (this is violated in rare cases)
|
||||
// would be more elegant to use boost::numeric::interval<double>
|
||||
const double eps = get_eps();
|
||||
double p_err =
|
||||
eps * ((3.0 / 3.0) * fabs(b) + (4.0 / 9.0) * (c * c) + fabs(p));
|
||||
double r_err = eps * (6.0 * (c * c) + 18.0 * fabs(b) + fabs(r));
|
||||
double q_err = 0.5 * fabs(a) * eps +
|
||||
(1.0 / 54.0) * fabs(c) * (r_err + fabs(r) * 3.0 * eps) +
|
||||
fabs(q) * eps;
|
||||
double discr_err = (p * p) * (3.0 * p_err + fabs(p) * 2.0 * eps) +
|
||||
fabs(q) * (2.0 * q_err + fabs(q) * eps) +
|
||||
fabs(discr) * eps;
|
||||
|
||||
// depending on the discriminant we get different solutions
|
||||
if (fabs(discr) <= discr_err) {
|
||||
// discriminant zero: one or two real roots
|
||||
if (fabs(p) <= p_err) {
|
||||
// p and q are zero: single root
|
||||
z.resize(1);
|
||||
z[0] = 0.0; // triple root
|
||||
} else {
|
||||
z.resize(2);
|
||||
z[0] = 2.0 * q / p; // single root
|
||||
z[1] = -0.5 * z[0]; // double root
|
||||
}
|
||||
} else if (discr > 0) {
|
||||
// three real roots: via trigonometric solution
|
||||
z.resize(3);
|
||||
double ac = (1.0 / 3.0) * acos(q / (p * sqrt(p)));
|
||||
double sq = 2.0 * sqrt(p);
|
||||
z[0] = sq * cos(ac);
|
||||
z[1] = sq * cos(ac - 2.0 * M_PI / 3.0);
|
||||
z[2] = sq * cos(ac - 4.0 * M_PI / 3.0);
|
||||
} else if (discr < 0.0) {
|
||||
// single real root: via Cardano's fromula
|
||||
z.resize(1);
|
||||
double sgnq = (q >= 0 ? 1 : -1);
|
||||
double basis = fabs(q) + sqrt(-discr);
|
||||
double C = sgnq * pow(basis, 1.0 / 3.0); // c++11 has std::cbrt()
|
||||
z[0] = C + p / C;
|
||||
}
|
||||
for (size_t i = 0; i < z.size(); i++) {
|
||||
// convert depressed cubic roots to original cubic: x = z - c/3
|
||||
z[i] -= (1.0 / 3.0) * c;
|
||||
// improve solution via newton steps
|
||||
for (int k = 0; k < newton_iter; k++) {
|
||||
double f = ((z[i] + c) * z[i] + b) * z[i] + a;
|
||||
double f1 = (3.0 * z[i] + 2.0 * c) * z[i] + b;
|
||||
// only adjust if slope is large enough
|
||||
if (fabs(f1) > 1e-8) {
|
||||
z[i] -= f / f1;
|
||||
}
|
||||
}
|
||||
}
|
||||
// ensure if a=0 we get exactly x=0 as root
|
||||
// TODO: remove this fudge
|
||||
if (a == 0.0) {
|
||||
assert(z.size() > 0); // cubic should always have at least one root
|
||||
double xmin = fabs(z[0]);
|
||||
size_t imin = 0;
|
||||
for (size_t i = 1; i < z.size(); i++) {
|
||||
if (xmin > fabs(z[i])) {
|
||||
xmin = fabs(z[i]);
|
||||
imin = i;
|
||||
}
|
||||
}
|
||||
z[imin] = 0.0; // replace the smallest absolute value with 0
|
||||
}
|
||||
std::sort(z.begin(), z.end());
|
||||
return z;
|
||||
}
|
||||
|
||||
} // namespace
|
123
plugin/android/src/main/cpp/lib/spline/spline.h
Normal file
123
plugin/android/src/main/cpp/lib/spline/spline.h
Normal file
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* spline.h
|
||||
*
|
||||
* simple cubic spline interpolation library without external
|
||||
* dependencies
|
||||
*
|
||||
* ---------------------------------------------------------------------
|
||||
* Copyright (C) 2011, 2014, 2016, 2021 Tino Kluge (ttk448 at gmail.com)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
* ---------------------------------------------------------------------
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace tk {
|
||||
|
||||
// spline interpolation
|
||||
class spline {
|
||||
public:
|
||||
// spline types
|
||||
enum spline_type {
|
||||
linear = 10, // linear interpolation
|
||||
cspline = 30, // cubic splines (classical C^2)
|
||||
cspline_hermite = 31 // cubic hermite splines (local, only C^1)
|
||||
};
|
||||
|
||||
// boundary condition type for the spline end-points
|
||||
enum bd_type { first_deriv = 1, second_deriv = 2, not_a_knot = 3 };
|
||||
|
||||
protected:
|
||||
std::vector<double> m_x, m_y; // x,y coordinates of points
|
||||
// interpolation parameters
|
||||
// f(x) = a_i + b_i*(x-x_i) + c_i*(x-x_i)^2 + d_i*(x-x_i)^3
|
||||
// where a_i = y_i, or else it won't go through grid points
|
||||
std::vector<double> m_b, m_c, m_d; // spline coefficients
|
||||
double m_c0; // for left extrapolation
|
||||
spline_type m_type;
|
||||
bd_type m_left, m_right;
|
||||
double m_left_value, m_right_value;
|
||||
bool m_made_monotonic;
|
||||
void set_coeffs_from_b(); // calculate c_i, d_i from b_i
|
||||
size_t find_closest(double x) const; // closest idx so that m_x[idx]<=x
|
||||
|
||||
public:
|
||||
// default constructor: set boundary condition to be zero curvature
|
||||
// at both ends, i.e. natural splines
|
||||
spline()
|
||||
: m_type(cspline), m_left(second_deriv), m_right(second_deriv),
|
||||
m_left_value(0.0), m_right_value(0.0), m_made_monotonic(false) {
|
||||
;
|
||||
}
|
||||
spline(const std::vector<double> &X, const std::vector<double> &Y,
|
||||
spline_type type = cspline, bool make_monotonic = false,
|
||||
bd_type left = second_deriv, double left_value = 0.0,
|
||||
bd_type right = second_deriv, double right_value = 0.0)
|
||||
: m_type(type), m_left(left), m_right(right), m_left_value(left_value),
|
||||
m_right_value(right_value),
|
||||
m_made_monotonic(false) // false correct here: make_monotonic() sets it
|
||||
{
|
||||
this->set_points(X, Y, m_type);
|
||||
if (make_monotonic) {
|
||||
this->make_monotonic();
|
||||
}
|
||||
}
|
||||
|
||||
// modify boundary conditions: if called it must be before set_points()
|
||||
void set_boundary(bd_type left, double left_value, bd_type right,
|
||||
double right_value);
|
||||
|
||||
// set all data points (cubic_spline=false means linear interpolation)
|
||||
void set_points(const std::vector<double> &x, const std::vector<double> &y,
|
||||
spline_type type = cspline);
|
||||
|
||||
// adjust coefficients so that the spline becomes piecewise monotonic
|
||||
// where possible
|
||||
// this is done by adjusting slopes at grid points by a non-negative
|
||||
// factor and this will break C^2
|
||||
// this can also break boundary conditions if adjustments need to
|
||||
// be made at the boundary points
|
||||
// returns false if no adjustments have been made, true otherwise
|
||||
bool make_monotonic();
|
||||
|
||||
// evaluates the spline at point x
|
||||
double operator()(double x) const;
|
||||
double deriv(int order, double x) const;
|
||||
|
||||
// solves for all x so that: spline(x) = y
|
||||
std::vector<double> solve(double y, bool ignore_extrapolation = true) const;
|
||||
|
||||
// returns the input data points
|
||||
std::vector<double> get_x() const { return m_x; }
|
||||
std::vector<double> get_y() const { return m_y; }
|
||||
double get_x_min() const {
|
||||
assert(!m_x.empty());
|
||||
return m_x.front();
|
||||
}
|
||||
double get_x_max() const {
|
||||
assert(!m_x.empty());
|
||||
return m_x.back();
|
||||
}
|
||||
|
||||
// spline info string, i.e. spline type, boundary conditions etc.
|
||||
std::string info() const;
|
||||
};
|
||||
|
||||
} // namespace tkPLINE_H */
|
|
@ -0,0 +1,65 @@
|
|||
package com.nkming.nc_photos.plugin
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.nkming.nc_photos.plugin.image_processor.Rgba8Image
|
||||
import com.nkming.nc_photos.plugin.image_processor.TfLiteHelper
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class ImageLoaderChannelHandler(context: Context) :
|
||||
MethodChannel.MethodCallHandler {
|
||||
companion object {
|
||||
const val METHOD_CHANNEL = "${K.LIB_ID}/image_loader_method"
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
"loadUri" -> {
|
||||
try {
|
||||
loadUri(
|
||||
call.argument("fileUri")!!,
|
||||
call.argument("maxWidth")!!,
|
||||
call.argument("maxHeight")!!,
|
||||
call.argument("resizeMethod")!!,
|
||||
call.argument("isAllowSwapSide")!!,
|
||||
call.argument("shouldUpscale")!!,
|
||||
result
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
result.error("systemException", e.toString(), null)
|
||||
}
|
||||
}
|
||||
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and resize an image pointed by a uri
|
||||
*
|
||||
* @param fileUri
|
||||
* @param maxWidth
|
||||
* @param maxHeight
|
||||
* @param resizeMethod
|
||||
* @param isAllowSwapSide
|
||||
* @param shouldUpscale
|
||||
* @param result
|
||||
*/
|
||||
private fun loadUri(
|
||||
fileUri: String, maxWidth: Int, maxHeight: Int, resizeMethod: Int,
|
||||
isAllowSwapSide: Boolean, shouldUpscale: Boolean,
|
||||
result: MethodChannel.Result
|
||||
) {
|
||||
val image = BitmapUtil.loadImage(
|
||||
context, Uri.parse(fileUri), maxWidth, maxHeight,
|
||||
BitmapResizeMethod.values()[resizeMethod], isAllowSwapSide,
|
||||
shouldUpscale
|
||||
).use {
|
||||
Rgba8Image(TfLiteHelper.bitmapToRgba8Array(it), it.width, it.height)
|
||||
}
|
||||
result.success(image.toJson())
|
||||
}
|
||||
|
||||
private val context = context
|
||||
}
|
|
@ -4,9 +4,11 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.nkming.nc_photos.plugin.image_processor.*
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import java.io.Serializable
|
||||
|
||||
class ImageProcessorChannelHandler(context: Context) :
|
||||
MethodChannel.MethodCallHandler, EventChannel.StreamHandler {
|
||||
|
@ -86,6 +88,36 @@ class ImageProcessorChannelHandler(context: Context) :
|
|||
}
|
||||
}
|
||||
|
||||
"colorFilter" -> {
|
||||
try {
|
||||
colorFilter(
|
||||
call.argument("fileUrl")!!,
|
||||
call.argument("headers"),
|
||||
call.argument("filename")!!,
|
||||
call.argument("maxWidth")!!,
|
||||
call.argument("maxHeight")!!,
|
||||
call.argument("filters")!!,
|
||||
result
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
logE(TAG, "Uncaught exception", e)
|
||||
result.error("systemException", e.toString(), null)
|
||||
}
|
||||
}
|
||||
|
||||
"filterPreview" -> {
|
||||
try {
|
||||
filterPreview(
|
||||
call.argument("rgba8")!!,
|
||||
call.argument("filters")!!,
|
||||
result
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
logE(TAG, "Uncaught exception", e)
|
||||
result.error("systemException", e.toString(), null)
|
||||
}
|
||||
}
|
||||
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
@ -142,6 +174,34 @@ class ImageProcessorChannelHandler(context: Context) :
|
|||
}
|
||||
)
|
||||
|
||||
private fun colorFilter(
|
||||
fileUrl: String, headers: Map<String, String>?, filename: String,
|
||||
maxWidth: Int, maxHeight: Int, filters: List<Map<String, Any>>,
|
||||
result: MethodChannel.Result
|
||||
) {
|
||||
// convert to serializable
|
||||
val l = arrayListOf<Serializable>()
|
||||
filters.mapTo(l, { HashMap(it) })
|
||||
method(
|
||||
fileUrl, headers, filename, maxWidth, maxHeight,
|
||||
ImageProcessorService.METHOD_COLOR_FILTER, result,
|
||||
onIntent = {
|
||||
it.putExtra(ImageProcessorService.EXTRA_FILTERS, l)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun filterPreview(
|
||||
rgba8: Map<String, Any>, filters: List<Map<String, Any>>,
|
||||
result: MethodChannel.Result
|
||||
) {
|
||||
var img = Rgba8Image.fromJson(rgba8)
|
||||
for (f in filters.map(ColorFilter::fromJson)) {
|
||||
img = f.apply(img)
|
||||
}
|
||||
result.success(img.toJson())
|
||||
}
|
||||
|
||||
private fun method(
|
||||
fileUrl: String, headers: Map<String, String>?, filename: String,
|
||||
maxWidth: Int, maxHeight: Int, method: String,
|
||||
|
@ -165,3 +225,24 @@ class ImageProcessorChannelHandler(context: Context) :
|
|||
private val context = context
|
||||
private var eventSink: EventChannel.EventSink? = null
|
||||
}
|
||||
|
||||
interface ColorFilter {
|
||||
companion object {
|
||||
fun fromJson(json: Map<String, Any>): ColorFilter {
|
||||
return when (json["type"]) {
|
||||
"brightness" -> Brightness((json["weight"] as Double).toFloat())
|
||||
"contrast" -> Contrast((json["weight"] as Double).toFloat())
|
||||
"whitePoint" -> WhitePoint((json["weight"] as Double).toFloat())
|
||||
"blackPoint" -> BlackPoint((json["weight"] as Double).toFloat())
|
||||
"saturation" -> Saturation((json["weight"] as Double).toFloat())
|
||||
"warmth" -> Warmth((json["weight"] as Double).toFloat())
|
||||
"tint" -> Tint((json["weight"] as Double).toFloat())
|
||||
else -> throw IllegalArgumentException(
|
||||
"Unknown type: ${json["type"]}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun apply(rgba8: Rgba8Image): Rgba8Image
|
||||
}
|
||||
|
|
|
@ -17,11 +17,9 @@ import androidx.core.app.NotificationChannelCompat
|
|||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
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.Esrgan
|
||||
import com.nkming.nc_photos.plugin.image_processor.ZeroDce
|
||||
import com.nkming.nc_photos.plugin.image_processor.*
|
||||
import java.io.File
|
||||
import java.io.Serializable
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
||||
|
@ -32,6 +30,7 @@ class ImageProcessorService : Service() {
|
|||
const val METHOD_DEEP_LAP_PORTRAIT = "DeepLab3Portrait"
|
||||
const val METHOD_ESRGAN = "Esrgan"
|
||||
const val METHOD_ARBITRARY_STYLE_TRANSFER = "ArbitraryStyleTransfer"
|
||||
const val METHOD_COLOR_FILTER = "ColorFilter"
|
||||
const val EXTRA_FILE_URL = "fileUrl"
|
||||
const val EXTRA_HEADERS = "headers"
|
||||
const val EXTRA_FILENAME = "filename"
|
||||
|
@ -41,6 +40,17 @@ class ImageProcessorService : Service() {
|
|||
const val EXTRA_ITERATION = "iteration"
|
||||
const val EXTRA_STYLE_URI = "styleUri"
|
||||
const val EXTRA_WEIGHT = "weight"
|
||||
const val EXTRA_FILTERS = "filters"
|
||||
|
||||
val ENHANCE_METHODS = listOf(
|
||||
METHOD_ZERO_DCE,
|
||||
METHOD_DEEP_LAP_PORTRAIT,
|
||||
METHOD_ESRGAN,
|
||||
METHOD_ARBITRARY_STYLE_TRANSFER,
|
||||
)
|
||||
val EDIT_METHODS = listOf(
|
||||
METHOD_COLOR_FILTER,
|
||||
)
|
||||
|
||||
private const val ACTION_CANCEL = "cancel"
|
||||
|
||||
|
@ -116,6 +126,7 @@ class ImageProcessorService : Service() {
|
|||
METHOD_ARBITRARY_STYLE_TRANSFER -> onArbitraryStyleTransfer(
|
||||
startId, intent.extras!!
|
||||
)
|
||||
METHOD_COLOR_FILTER -> onColorFilter(startId, intent.extras!!)
|
||||
else -> {
|
||||
logE(TAG, "Unknown method: $method")
|
||||
// we can't call stopSelf here as it'll stop the service even if
|
||||
|
@ -159,6 +170,18 @@ class ImageProcessorService : Service() {
|
|||
)
|
||||
}
|
||||
|
||||
private fun onColorFilter(startId: Int, extras: Bundle) {
|
||||
val filters = extras.getSerializable(EXTRA_FILTERS)!!
|
||||
.asType<ArrayList<Serializable>>()
|
||||
.map { it.asType<HashMap<String, Any>>() }
|
||||
return onMethod(
|
||||
startId, extras, METHOD_COLOR_FILTER,
|
||||
args = mapOf(
|
||||
"filters" to filters,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle methods without arguments
|
||||
*
|
||||
|
@ -231,7 +254,7 @@ class ImageProcessorService : Service() {
|
|||
)
|
||||
return NotificationCompat.Builder(this, CHANNEL_ID).run {
|
||||
setSmallIcon(R.drawable.outline_image_white_24)
|
||||
setContentTitle("Successfully enhanced image")
|
||||
setContentTitle("Successfully processed image")
|
||||
setContentText("Tap to view the result")
|
||||
setContentIntent(pi)
|
||||
setAutoCancel(true)
|
||||
|
@ -244,7 +267,7 @@ class ImageProcessorService : Service() {
|
|||
): Notification {
|
||||
return NotificationCompat.Builder(this, CHANNEL_ID).run {
|
||||
setSmallIcon(R.drawable.outline_error_outline_white_24)
|
||||
setContentTitle("Failed enhancing image")
|
||||
setContentTitle("Failed processing image")
|
||||
setContentText(exception.message)
|
||||
build()
|
||||
}
|
||||
|
@ -260,7 +283,7 @@ class ImageProcessorService : Service() {
|
|||
)
|
||||
return NotificationCompat.Builder(this, CHANNEL_ID).run {
|
||||
setSmallIcon(R.drawable.outline_auto_fix_high_white_24)
|
||||
setContentTitle("Preparing to restart photo enhancement")
|
||||
setContentTitle("Preparing to restart photo processing")
|
||||
addAction(
|
||||
0, getString(android.R.string.cancel), cancelPendingIntent
|
||||
)
|
||||
|
@ -552,13 +575,28 @@ private open class ImageProcessorCommandTask(context: Context) :
|
|||
cmd.args["weight"] as Float
|
||||
).infer(fileUri)
|
||||
|
||||
ImageProcessorService.METHOD_COLOR_FILTER -> {
|
||||
@Suppress("Unchecked_cast")
|
||||
ColorFilterProcessor(
|
||||
context, cmd.maxWidth, cmd.maxHeight,
|
||||
cmd.args["filters"] as List<Map<String, Any>>,
|
||||
).apply(fileUri)
|
||||
}
|
||||
|
||||
else -> throw IllegalArgumentException(
|
||||
"Unknown method: ${cmd.method}"
|
||||
)
|
||||
}
|
||||
})
|
||||
handleCancel()
|
||||
saveBitmap(output, cmd.filename, file)
|
||||
saveBitmap(
|
||||
output, cmd.filename, file,
|
||||
if (cmd.method in ImageProcessorService.EDIT_METHODS) {
|
||||
"Edited Photos"
|
||||
} else {
|
||||
"Enhanced Photos"
|
||||
}
|
||||
)
|
||||
} finally {
|
||||
file.delete()
|
||||
}
|
||||
|
@ -598,7 +636,7 @@ private open class ImageProcessorCommandTask(context: Context) :
|
|||
}
|
||||
|
||||
private fun saveBitmap(
|
||||
bitmap: Bitmap, filename: String, srcFile: File
|
||||
bitmap: Bitmap, filename: String, srcFile: File, subDir: String
|
||||
): Uri {
|
||||
logI(TAG, "[saveBitmap] $filename")
|
||||
val outFile = File.createTempFile("out", null, getTempDir(context))
|
||||
|
@ -619,7 +657,7 @@ private open class ImageProcessorCommandTask(context: Context) :
|
|||
// move file to user accessible storage
|
||||
val uri = MediaStoreUtil.copyFileToDownload(
|
||||
context, Uri.fromFile(outFile), filename,
|
||||
"Photos (for Nextcloud)/Enhanced Photos"
|
||||
"Photos (for Nextcloud)/$subDir"
|
||||
)
|
||||
outFile.delete()
|
||||
return uri
|
||||
|
|
|
@ -113,6 +113,14 @@ class NcPhotosPlugin : FlutterPlugin, ActivityAware,
|
|||
PreferenceChannelHandler.METHOD_CHANNEL
|
||||
)
|
||||
preferenceMethodChannel.setMethodCallHandler(preferenceChannelHandler)
|
||||
|
||||
val imageLoaderChannelHandler =
|
||||
ImageLoaderChannelHandler(flutterPluginBinding.applicationContext)
|
||||
imageLoaderMethodChannel = MethodChannel(
|
||||
flutterPluginBinding.binaryMessenger,
|
||||
ImageLoaderChannelHandler.METHOD_CHANNEL
|
||||
)
|
||||
imageLoaderMethodChannel.setMethodCallHandler(imageLoaderChannelHandler)
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(
|
||||
|
@ -131,6 +139,7 @@ class NcPhotosPlugin : FlutterPlugin, ActivityAware,
|
|||
permissionMethodChannel.setMethodCallHandler(null)
|
||||
logcatMethodChannel.setMethodCallHandler(null)
|
||||
preferenceMethodChannel.setMethodCallHandler(null)
|
||||
imageLoaderMethodChannel.setMethodCallHandler(null)
|
||||
}
|
||||
|
||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||
|
@ -219,6 +228,7 @@ class NcPhotosPlugin : FlutterPlugin, ActivityAware,
|
|||
private lateinit var permissionMethodChannel: MethodChannel
|
||||
private lateinit var logcatMethodChannel: MethodChannel
|
||||
private lateinit var preferenceMethodChannel: MethodChannel
|
||||
private lateinit var imageLoaderMethodChannel: MethodChannel
|
||||
|
||||
private lateinit var lockChannelHandler: LockChannelHandler
|
||||
private lateinit var mediaStoreChannelHandler: MediaStoreChannelHandler
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.nkming.nc_photos.plugin
|
|||
import android.app.PendingIntent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import java.io.Serializable
|
||||
import java.net.HttpURLConnection
|
||||
|
||||
fun getPendingIntentFlagImmutable(): Int {
|
||||
|
@ -40,3 +41,6 @@ inline fun <T> measureTime(tag: String, message: String, block: () -> T): T {
|
|||
}
|
||||
|
||||
fun Bundle.getIntOrNull(key: String) = get(key) as? Int
|
||||
|
||||
@Suppress("Unchecked_cast")
|
||||
fun <T> Serializable.asType() = this as T
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
package com.nkming.nc_photos.plugin.image_processor
|
||||
|
||||
import com.nkming.nc_photos.plugin.ColorFilter
|
||||
|
||||
class BlackPoint(val weight: Float) : ColorFilter {
|
||||
override fun apply(rgba8: Rgba8Image) = Rgba8Image(
|
||||
applyNative(rgba8.pixel, rgba8.width, rgba8.height, weight),
|
||||
rgba8.width, rgba8.height
|
||||
)
|
||||
|
||||
private external fun applyNative(
|
||||
rgba8: ByteArray, width: Int, height: Int, weight: Float
|
||||
): ByteArray
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package com.nkming.nc_photos.plugin.image_processor
|
||||
|
||||
import com.nkming.nc_photos.plugin.ColorFilter
|
||||
|
||||
class Brightness(val weight: Float) : ColorFilter {
|
||||
override fun apply(rgba8: Rgba8Image) = Rgba8Image(
|
||||
applyNative(rgba8.pixel, rgba8.width, rgba8.height, weight),
|
||||
rgba8.width, rgba8.height
|
||||
)
|
||||
|
||||
private external fun applyNative(
|
||||
rgba8: ByteArray, width: Int, height: Int, weight: Float
|
||||
): ByteArray
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package com.nkming.nc_photos.plugin.image_processor
|
||||
|
||||
import android.content.Context
|
||||
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.ColorFilter
|
||||
import com.nkming.nc_photos.plugin.use
|
||||
|
||||
class ColorFilterProcessor(
|
||||
context: Context, maxWidth: Int, maxHeight: Int,
|
||||
filters: List<Map<String, Any>>
|
||||
) {
|
||||
companion object {
|
||||
const val TAG = "ColorFilterProcessor"
|
||||
}
|
||||
|
||||
fun apply(imageUri: Uri): Bitmap {
|
||||
var img = BitmapUtil.loadImage(
|
||||
context, imageUri, maxWidth, maxHeight, BitmapResizeMethod.FIT,
|
||||
isAllowSwapSide = true, shouldUpscale = false
|
||||
).use {
|
||||
Rgba8Image(TfLiteHelper.bitmapToRgba8Array(it), it.width, it.height)
|
||||
}
|
||||
|
||||
for (f in filters.map(ColorFilter::fromJson)) {
|
||||
img = f.apply(img)
|
||||
}
|
||||
return img.toBitmap()
|
||||
}
|
||||
|
||||
private val context = context
|
||||
private val maxWidth = maxWidth
|
||||
private val maxHeight = maxHeight
|
||||
private val filters = filters
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package com.nkming.nc_photos.plugin.image_processor
|
||||
|
||||
import com.nkming.nc_photos.plugin.ColorFilter
|
||||
|
||||
class Contrast(val weight: Float) : ColorFilter {
|
||||
override fun apply(rgba8: Rgba8Image) = Rgba8Image(
|
||||
applyNative(rgba8.pixel, rgba8.width, rgba8.height, weight),
|
||||
rgba8.width, rgba8.height
|
||||
)
|
||||
|
||||
private external fun applyNative(
|
||||
rgba8: ByteArray, width: Int, height: Int, weight: Float
|
||||
): ByteArray
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package com.nkming.nc_photos.plugin.image_processor
|
||||
|
||||
import com.nkming.nc_photos.plugin.ColorFilter
|
||||
|
||||
class Cool(val weight: Float) : ColorFilter {
|
||||
override fun apply(rgba8: Rgba8Image) = Rgba8Image(
|
||||
applyNative(rgba8.pixel, rgba8.width, rgba8.height, weight),
|
||||
rgba8.width, rgba8.height
|
||||
)
|
||||
|
||||
private external fun applyNative(
|
||||
rgba8: ByteArray, width: Int, height: Int, weight: Float
|
||||
): ByteArray
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package com.nkming.nc_photos.plugin.image_processor
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
/**
|
||||
* Container of pixel data stored in RGBA format
|
||||
*/
|
||||
class Rgba8Image(val pixel: ByteArray, val width: Int, val height: Int) {
|
||||
companion object {
|
||||
fun fromJson(json: Map<String, Any>) = Rgba8Image(
|
||||
json["pixel"] as ByteArray, json["width"] as Int,
|
||||
json["height"] as Int
|
||||
)
|
||||
}
|
||||
|
||||
fun toJson() = mapOf<String, Any>(
|
||||
"pixel" to pixel,
|
||||
"width" to width,
|
||||
"height" to height,
|
||||
)
|
||||
|
||||
fun toBitmap(): Bitmap {
|
||||
return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
.apply {
|
||||
copyPixelsFromBuffer(ByteBuffer.wrap(pixel))
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
assert(pixel.size == width * height * 4)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package com.nkming.nc_photos.plugin.image_processor
|
||||
|
||||
import com.nkming.nc_photos.plugin.ColorFilter
|
||||
|
||||
class Saturation(val weight: Float) : ColorFilter {
|
||||
override fun apply(rgba8: Rgba8Image) = Rgba8Image(
|
||||
applyNative(rgba8.pixel, rgba8.width, rgba8.height, weight),
|
||||
rgba8.width, rgba8.height
|
||||
)
|
||||
|
||||
private external fun applyNative(
|
||||
rgba8: ByteArray, width: Int, height: Int, weight: Float
|
||||
): ByteArray
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package com.nkming.nc_photos.plugin.image_processor
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.IntBuffer
|
||||
|
||||
interface TfLiteHelper {
|
||||
|
@ -25,6 +26,18 @@ interface TfLiteHelper {
|
|||
return rgb8
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an ARGB_8888 Android bitmap to a RGBA byte array
|
||||
*
|
||||
* @param bitmap
|
||||
* @return
|
||||
*/
|
||||
fun bitmapToRgba8Array(bitmap: Bitmap): ByteArray {
|
||||
val buffer = ByteBuffer.allocate(bitmap.width * bitmap.height * 4)
|
||||
bitmap.copyPixelsToBuffer(buffer)
|
||||
return buffer.array()
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a RGB8 byte array to an ARGB_8888 Android bitmap
|
||||
*
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
package com.nkming.nc_photos.plugin.image_processor
|
||||
|
||||
import com.nkming.nc_photos.plugin.ColorFilter
|
||||
|
||||
class Tint(val weight: Float) : ColorFilter {
|
||||
override fun apply(rgba8: Rgba8Image) = Rgba8Image(
|
||||
applyNative(rgba8.pixel, rgba8.width, rgba8.height, weight),
|
||||
rgba8.width, rgba8.height
|
||||
)
|
||||
|
||||
private external fun applyNative(
|
||||
rgba8: ByteArray, width: Int, height: Int, weight: Float
|
||||
): ByteArray
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package com.nkming.nc_photos.plugin.image_processor
|
||||
|
||||
import com.nkming.nc_photos.plugin.ColorFilter
|
||||
|
||||
class Warmth(val weight: Float) : ColorFilter {
|
||||
override fun apply(rgba8: Rgba8Image) = Rgba8Image(
|
||||
applyNative(rgba8.pixel, rgba8.width, rgba8.height, weight),
|
||||
rgba8.width, rgba8.height
|
||||
)
|
||||
|
||||
private external fun applyNative(
|
||||
rgba8: ByteArray, width: Int, height: Int, weight: Float
|
||||
): ByteArray
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package com.nkming.nc_photos.plugin.image_processor
|
||||
|
||||
import com.nkming.nc_photos.plugin.ColorFilter
|
||||
|
||||
class WhitePoint(val weight: Float) : ColorFilter {
|
||||
override fun apply(rgba8: Rgba8Image) = Rgba8Image(
|
||||
applyNative(rgba8.pixel, rgba8.width, rgba8.height, weight),
|
||||
rgba8.width, rgba8.height
|
||||
)
|
||||
|
||||
private external fun applyNative(
|
||||
rgba8: ByteArray, width: Int, height: Int, weight: Float
|
||||
): ByteArray
|
||||
}
|
|
@ -2,6 +2,8 @@ library nc_photos_plugin;
|
|||
|
||||
export 'src/content_uri.dart';
|
||||
export 'src/exception.dart';
|
||||
export 'src/image.dart';
|
||||
export 'src/image_loader.dart';
|
||||
export 'src/image_processor.dart';
|
||||
export 'src/lock.dart';
|
||||
export 'src/logcat.dart';
|
||||
|
|
22
plugin/lib/src/image.dart
Normal file
22
plugin/lib/src/image.dart
Normal file
|
@ -0,0 +1,22 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
/// Container of pixel data stored in RGBA format
|
||||
class Rgba8Image {
|
||||
const Rgba8Image(this.pixel, this.width, this.height);
|
||||
|
||||
factory Rgba8Image.fromJson(Map<String, dynamic> json) => Rgba8Image(
|
||||
json["pixel"],
|
||||
json["width"],
|
||||
json["height"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"pixel": pixel,
|
||||
"width": width,
|
||||
"height": height,
|
||||
};
|
||||
|
||||
final Uint8List pixel;
|
||||
final int width;
|
||||
final int height;
|
||||
}
|
34
plugin/lib/src/image_loader.dart
Normal file
34
plugin/lib/src/image_loader.dart
Normal file
|
@ -0,0 +1,34 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:nc_photos_plugin/src/image.dart';
|
||||
import 'package:nc_photos_plugin/src/k.dart' as k;
|
||||
|
||||
enum ImageLoaderResizeMethod {
|
||||
fit,
|
||||
fill,
|
||||
}
|
||||
|
||||
class ImageLoader {
|
||||
static Future<Rgba8Image> loadUri(
|
||||
String fileUri,
|
||||
int maxWidth,
|
||||
int maxHeight,
|
||||
ImageLoaderResizeMethod resizeMethod, {
|
||||
bool isAllowSwapSide = false,
|
||||
bool shouldUpscale = false,
|
||||
}) async {
|
||||
final result =
|
||||
await _methodChannel.invokeMethod<Map>("loadUri", <String, dynamic>{
|
||||
"fileUri": fileUri,
|
||||
"maxWidth": maxWidth,
|
||||
"maxHeight": maxHeight,
|
||||
"resizeMethod": resizeMethod.index,
|
||||
"isAllowSwapSide": isAllowSwapSide,
|
||||
"shouldUpscale": shouldUpscale,
|
||||
});
|
||||
return Rgba8Image.fromJson(result!.cast<String, dynamic>());
|
||||
}
|
||||
|
||||
static const _methodChannel = MethodChannel("${k.libId}/image_loader_method");
|
||||
}
|
|
@ -1,8 +1,49 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:nc_photos_plugin/src/image.dart';
|
||||
import 'package:nc_photos_plugin/src/k.dart' as k;
|
||||
|
||||
abstract class ImageFilter {
|
||||
Map<String, dynamic> toJson();
|
||||
}
|
||||
|
||||
class ColorBrightnessFilter extends _SingleWeightFilter {
|
||||
const ColorBrightnessFilter(double weight) : super("brightness", weight);
|
||||
}
|
||||
|
||||
class ColorContrastFilter extends _SingleWeightFilter {
|
||||
const ColorContrastFilter(double weight) : super("contrast", weight);
|
||||
}
|
||||
|
||||
class ColorWhitePointFilter extends _SingleWeightFilter {
|
||||
const ColorWhitePointFilter(double weight) : super("whitePoint", weight);
|
||||
}
|
||||
|
||||
class ColorHighlightFilter extends _SingleWeightFilter {
|
||||
const ColorHighlightFilter(double weight) : super("highlight", weight);
|
||||
}
|
||||
|
||||
class ColorShadowFilter extends _SingleWeightFilter {
|
||||
const ColorShadowFilter(double weight) : super("shadow", weight);
|
||||
}
|
||||
|
||||
class ColorBlackPointFilter extends _SingleWeightFilter {
|
||||
const ColorBlackPointFilter(double weight) : super("blackPoint", weight);
|
||||
}
|
||||
|
||||
class ColorSaturationFilter extends _SingleWeightFilter {
|
||||
const ColorSaturationFilter(double weight) : super("saturation", weight);
|
||||
}
|
||||
|
||||
class ColorWarmthFilter extends _SingleWeightFilter {
|
||||
const ColorWarmthFilter(double weight) : super("warmth", weight);
|
||||
}
|
||||
|
||||
class ColorTintFilter extends _SingleWeightFilter {
|
||||
const ColorTintFilter(double weight) : super("tint", weight);
|
||||
}
|
||||
|
||||
class ImageProcessor {
|
||||
static Future<void> zeroDce(
|
||||
String fileUrl,
|
||||
|
@ -72,6 +113,48 @@ class ImageProcessor {
|
|||
"weight": weight,
|
||||
});
|
||||
|
||||
static Future<void> colorFilter(
|
||||
String fileUrl,
|
||||
String filename,
|
||||
int maxWidth,
|
||||
int maxHeight,
|
||||
List<ImageFilter> filters, {
|
||||
Map<String, String>? headers,
|
||||
}) =>
|
||||
_methodChannel.invokeMethod("colorFilter", <String, dynamic>{
|
||||
"fileUrl": fileUrl,
|
||||
"headers": headers,
|
||||
"filename": filename,
|
||||
"maxWidth": maxWidth,
|
||||
"maxHeight": maxHeight,
|
||||
"filters": filters.map((f) => f.toJson()).toList(),
|
||||
});
|
||||
|
||||
static Future<Rgba8Image> filterPreview(
|
||||
Rgba8Image img,
|
||||
List<ImageFilter> filters,
|
||||
) async {
|
||||
final result = await _methodChannel
|
||||
.invokeMethod<Map>("filterPreview", <String, dynamic>{
|
||||
"rgba8": img.toJson(),
|
||||
"filters": filters.map((f) => f.toJson()).toList(),
|
||||
});
|
||||
return Rgba8Image.fromJson(result!.cast<String, dynamic>());
|
||||
}
|
||||
|
||||
static const _methodChannel =
|
||||
MethodChannel("${k.libId}/image_processor_method");
|
||||
}
|
||||
|
||||
class _SingleWeightFilter implements ImageFilter {
|
||||
const _SingleWeightFilter(this.type, this.weight);
|
||||
|
||||
@override
|
||||
toJson() => {
|
||||
"type": type,
|
||||
"weight": weight,
|
||||
};
|
||||
|
||||
final String type;
|
||||
final double weight;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue