From 5d9676159e1cc4cbc4a5ea1b06526d30532aa6fc Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sun, 13 Aug 2023 20:04:08 +0200 Subject: [PATCH] #706 histogram fixes --- lib/widgets/viewer/overlay/histogram.dart | 109 ++++++---------------- lib/widgets/viewer/overlay/top.dart | 11 ++- lib/widgets/viewer/view/controller.dart | 6 +- lib/widgets/viewer/view/histogram.dart | 97 +++++++++++++++++++ 4 files changed, 134 insertions(+), 89 deletions(-) create mode 100644 lib/widgets/viewer/view/histogram.dart diff --git a/lib/widgets/viewer/overlay/histogram.dart b/lib/widgets/viewer/overlay/histogram.dart index 91aacddd5..7c648e5fa 100644 --- a/lib/widgets/viewer/overlay/histogram.dart +++ b/lib/widgets/viewer/overlay/histogram.dart @@ -1,20 +1,19 @@ -import 'dart:typed_data'; import 'dart:ui'; import 'package:aves/model/entry/entry.dart'; -import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/viewer/overlay/top.dart'; -import 'package:aves_model/aves_model.dart'; +import 'package:aves/widgets/viewer/view/controller.dart'; +import 'package:aves/widgets/viewer/view/histogram.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; class ImageHistogram extends StatefulWidget { - final AvesEntry entry; + final ViewStateController viewStateController; final ImageProvider image; const ImageHistogram({ super.key, - required this.entry, + required this.viewStateController, required this.image, }); @@ -23,10 +22,14 @@ class ImageHistogram extends StatefulWidget { } class _ImageHistogramState extends State { - Map> _levels = {}; + HistogramLevels _levels = {}; ImageStream? _imageStream; late ImageStreamListener _imageListener; + ViewStateController get viewStateController => widget.viewStateController; + + AvesEntry get entry => viewStateController.entry; + ImageProvider get imageProvider => widget.image; @override @@ -73,87 +76,17 @@ class _ImageHistogramState extends State { ); } - static const int bins = 256; - static const int normMax = bins - 1; - Future _updateLevels(ImageInfo info) async { - final image = info.image; - final data = (await image.toByteData(format: ImageByteFormat.rawExtendedRgba128))!; - final floats = Float32List.view(data.buffer); - - // TODO TLAD [histo] compute in isolate? - // TODO TLAD [histo] save/reuse levels in view controller - final newLevels = switch (settings.overlayHistogramStyle) { - OverlayHistogramStyle.rgb => _computeRgbLevels(floats), - OverlayHistogramStyle.luminance => _computeLuminanceLevels(floats), - _ => >{}, - }; - + final targetEntry = entry; + final newLevels = await viewStateController.getHistogramLevels(info); if (mounted) { - setState(() => _levels = newLevels); + setState(() => _levels = targetEntry == entry ? newLevels : {}); } } - - Map> _computeRgbLevels(Float32List floats) { - final redLevels = List.filled(bins, 0); - final greenLevels = List.filled(bins, 0); - final blueLevels = List.filled(bins, 0); - - final pixelCount = floats.length / 4; - for (var i = 0; i < pixelCount; i += 4) { - final a = floats[i + 3]; - if (a > 0) { - final r = floats[i + 0]; - final g = floats[i + 1]; - final b = floats[i + 2]; - redLevels[(r * normMax).round()]++; - greenLevels[(g * normMax).round()]++; - blueLevels[(b * normMax).round()]++; - } - } - - final max = [ - redLevels.max, - greenLevels.max, - blueLevels.max, - ].max; - if (max == 0) return {}; - - final f = 1.0 / max; - return { - Colors.red: redLevels.map((v) => v * f).toList(), - Colors.green: greenLevels.map((v) => v * f).toList(), - Colors.blue: blueLevels.map((v) => v * f).toList(), - }; - } - - Map> _computeLuminanceLevels(Float32List floats) { - final lumLevels = List.filled(bins, 0); - - final pixelCount = floats.length / 4; - for (var i = 0; i < pixelCount; i += 4) { - final a = floats[i + 3]; - if (a > 0) { - final r = floats[i + 0]; - final g = floats[i + 1]; - final b = floats[i + 2]; - final c = Color.fromARGB((a * 255).round(), (r * 255).round(), (g * 255).round(), (b * 255).round()); - lumLevels[(c.computeLuminance() * normMax).round()]++; - } - } - - final max = lumLevels.max; - if (max == 0) return {}; - - final f = 1.0 / max; - return { - Colors.white: lumLevels.map((v) => v * f).toList(), - }; - } } class _HistogramPainter extends CustomPainter { - final Map> levels; + final HistogramLevels levels; final Color borderColor; late final Paint fill, borderStroke; @@ -172,9 +105,14 @@ class _HistogramPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { + if (levels.isEmpty) return; + final backgroundRect = Rect.fromPoints(Offset.zero, Offset(size.width, size.height)); canvas.drawRect(backgroundRect, fill); - levels.forEach((color, values) => _drawLevels(canvas, size, color, values)); + levels.forEach((channel, values) { + final color = _getChannelColor(channel); + _drawLevels(canvas, size, color, values); + }); canvas.drawRect(backgroundRect, borderStroke); } @@ -201,6 +139,15 @@ class _HistogramPainter extends CustomPainter { ..color = color.withOpacity(.5)); } + Color _getChannelColor(HistogramChannel channel) { + return switch (channel) { + HistogramChannel.red => Colors.red, + HistogramChannel.green => Colors.green, + HistogramChannel.blue => Colors.blue, + HistogramChannel.luminance => Colors.white, + }; + } + @override bool shouldRepaint(covariant CustomPainter oldDelegate) => true; } diff --git a/lib/widgets/viewer/overlay/top.dart b/lib/widgets/viewer/overlay/top.dart index 7b3e8ee94..99d45c356 100644 --- a/lib/widgets/viewer/overlay/top.dart +++ b/lib/widgets/viewer/overlay/top.dart @@ -9,6 +9,7 @@ import 'package:aves/widgets/viewer/overlay/histogram.dart'; import 'package:aves/widgets/viewer/overlay/minimap.dart'; import 'package:aves/widgets/viewer/page_entry_builder.dart'; import 'package:aves/widgets/viewer/view/conductor.dart'; +import 'package:aves/widgets/viewer/view/controller.dart'; import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -116,15 +117,15 @@ class ViewerTopOverlay extends StatelessWidget { padding: const EdgeInsets.all(8), child: FadeTransition( opacity: scale, - child: Selector>( - selector: (context, vsc) => vsc.getOrCreateController(pageEntry!).fullImageNotifier, - builder: (context, fullImageNotifier, child) { + child: Selector( + selector: (context, vsc) => vsc.getOrCreateController(pageEntry!), + builder: (context, viewStateController, child) { return ValueListenableBuilder( - valueListenable: fullImageNotifier, + valueListenable: viewStateController.fullImageNotifier, builder: (context, fullImage, child) { if (fullImage == null || pageEntry == null) return const SizedBox(); return ImageHistogram( - entry: pageEntry, + viewStateController: viewStateController, image: fullImage, ); }, diff --git a/lib/widgets/viewer/view/controller.dart b/lib/widgets/viewer/view/controller.dart index e0ec23538..481163a6c 100644 --- a/lib/widgets/viewer/view/controller.dart +++ b/lib/widgets/viewer/view/controller.dart @@ -1,9 +1,9 @@ import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/view_state.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/painting.dart'; +import 'package:aves/widgets/viewer/view/histogram.dart'; +import 'package:flutter/material.dart'; -class ViewStateController { +class ViewStateController with HistogramMixin { final AvesEntry entry; final ValueNotifier viewStateNotifier; final ValueNotifier fullImageNotifier = ValueNotifier(null); diff --git a/lib/widgets/viewer/view/histogram.dart b/lib/widgets/viewer/view/histogram.dart new file mode 100644 index 000000000..6bb5fbcae --- /dev/null +++ b/lib/widgets/viewer/view/histogram.dart @@ -0,0 +1,97 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:aves/model/settings/settings.dart'; +import 'package:aves_model/aves_model.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +enum HistogramChannel { red, green, blue, luminance } + +typedef HistogramLevels = Map>; + +mixin HistogramMixin { + HistogramLevels _levels = {}; + Completer? _completer; + + static const int bins = 256; + static const int normMax = bins - 1; + + Future getHistogramLevels(ImageInfo info) async { + if (_levels.isEmpty) { + if (_completer == null) { + _completer = Completer(); + final data = (await info.image.toByteData(format: ImageByteFormat.rawExtendedRgba128))!; + _levels = switch (settings.overlayHistogramStyle) { + OverlayHistogramStyle.rgb => await compute(_computeRgbLevels, data), + OverlayHistogramStyle.luminance => await compute(_computeLuminanceLevels, data), + _ => >{}, + }; + _completer?.complete(); + } else { + await _completer?.future; + } + } + return _levels; + } + + static HistogramLevels _computeRgbLevels(ByteData data) { + final redLevels = List.filled(bins, 0); + final greenLevels = List.filled(bins, 0); + final blueLevels = List.filled(bins, 0); + + final floats = Float32List.view(data.buffer); + final pixelCount = floats.length / 4; + for (var i = 0; i < pixelCount; i += 4) { + final a = floats[i + 3]; + if (a > 0) { + final r = floats[i + 0]; + final g = floats[i + 1]; + final b = floats[i + 2]; + redLevels[(r * normMax).round()]++; + greenLevels[(g * normMax).round()]++; + blueLevels[(b * normMax).round()]++; + } + } + + final max = [ + redLevels.max, + greenLevels.max, + blueLevels.max, + ].max; + if (max == 0) return {}; + + final f = 1.0 / max; + return { + HistogramChannel.red: redLevels.map((v) => v * f).toList(), + HistogramChannel.green: greenLevels.map((v) => v * f).toList(), + HistogramChannel.blue: blueLevels.map((v) => v * f).toList(), + }; + } + + static HistogramLevels _computeLuminanceLevels(ByteData data) { + final lumLevels = List.filled(bins, 0); + + final floats = Float32List.view(data.buffer); + final pixelCount = floats.length / 4; + for (var i = 0; i < pixelCount; i += 4) { + final a = floats[i + 3]; + if (a > 0) { + final r = floats[i + 0]; + final g = floats[i + 1]; + final b = floats[i + 2]; + final c = Color.fromARGB((a * 255).round(), (r * 255).round(), (g * 255).round(), (b * 255).round()); + lumLevels[(c.computeLuminance() * normMax).round()]++; + } + } + + final max = lumLevels.max; + if (max == 0) return {}; + + final f = 1.0 / max; + return { + HistogramChannel.luminance: lumLevels.map((v) => v * f).toList(), + }; + } +}