#706 histogram fixes

This commit is contained in:
Thibault Deckers 2023-08-13 20:04:08 +02:00
parent 6d997c438a
commit 5d9676159e
4 changed files with 134 additions and 89 deletions

View file

@ -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<ImageHistogram> {
Map<Color, List<double>> _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<ImageHistogram> {
);
}
static const int bins = 256;
static const int normMax = bins - 1;
Future<void> _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),
_ => <Color, List<double>>{},
};
final targetEntry = entry;
final newLevels = await viewStateController.getHistogramLevels(info);
if (mounted) {
setState(() => _levels = newLevels);
setState(() => _levels = targetEntry == entry ? newLevels : {});
}
}
Map<Color, List<double>> _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<Color, List<double>> _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<Color, List<double>> 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;
}

View file

@ -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<ViewStateConductor, ValueNotifier<ImageProvider?>>(
selector: (context, vsc) => vsc.getOrCreateController(pageEntry!).fullImageNotifier,
builder: (context, fullImageNotifier, child) {
child: Selector<ViewStateConductor, ViewStateController>(
selector: (context, vsc) => vsc.getOrCreateController(pageEntry!),
builder: (context, viewStateController, child) {
return ValueListenableBuilder<ImageProvider?>(
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,
);
},

View file

@ -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<ViewState> viewStateNotifier;
final ValueNotifier<ImageProvider?> fullImageNotifier = ValueNotifier(null);

View file

@ -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<HistogramChannel, List<double>>;
mixin HistogramMixin {
HistogramLevels _levels = {};
Completer? _completer;
static const int bins = 256;
static const int normMax = bins - 1;
Future<HistogramLevels> 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<ByteData, HistogramLevels>(_computeRgbLevels, data),
OverlayHistogramStyle.luminance => await compute<ByteData, HistogramLevels>(_computeLuminanceLevels, data),
_ => <HistogramChannel, List<double>>{},
};
_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(),
};
}
}