From 886188bb9e7997bbf0595be6a1b3cbef545f278d Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Sun, 6 Aug 2023 16:28:20 +0200 Subject: [PATCH] ML-95 Live histogram (#97) * Added histogram and separated camera view builder * Added histogram to `MeteringScreenLayoutConfig` * `ResolutionPreset.medium` -> `ResolutionPreset.low` * Adjusted histogram paddings --- .github/workflows/build_apk.yml | 2 +- .github/workflows/create_release.yml | 2 +- .vscode/launch.json | 4 +- .vscode/tasks.json | 6 +- README.md | 4 +- .../models/metering_screen_layout_config.dart | 10 +- lib/data/shared_prefs_service.dart | 1 + lib/l10n/intl_en.arb | 1 + lib/l10n/intl_fr.arb | 1 + lib/l10n/intl_ru.arb | 1 + lib/l10n/intl_zh.arb | 1 + .../bloc_container_camera.dart | 2 +- .../camera_view/widget_camera_view.dart | 10 +- .../widget_placeholder_camera_view.dart | 0 .../histogram/widget_histogram.dart | 132 ++++++++++++++++++ .../camera_preview/widget_camera_preview.dart | 62 ++++++++ .../widget_container_camera.dart | 26 ++-- ...ialog_metering_screen_layout_features.dart | 2 + .../metering_screen_layout_config_test.dart | 60 +++++--- test/data/shared_prefs_service_test.dart | 7 +- 20 files changed, 280 insertions(+), 54 deletions(-) rename lib/screens/metering/components/camera_container/components/{ => camera_preview/components}/camera_view/widget_camera_view.dart (86%) rename lib/screens/metering/components/camera_container/components/{ => camera_preview/components}/camera_view_placeholder/widget_placeholder_camera_view.dart (100%) create mode 100644 lib/screens/metering/components/camera_container/components/camera_preview/components/histogram/widget_histogram.dart create mode 100644 lib/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart diff --git a/.github/workflows/build_apk.yml b/.github/workflows/build_apk.yml index c972954..9157f90 100644 --- a/.github/workflows/build_apk.yml +++ b/.github/workflows/build_apk.yml @@ -76,7 +76,7 @@ jobs: - name: Build Apk env: FLAVOR: ${{ github.event.inputs.flavor }} - run: flutter build apk --release --flavor $FLAVOR --dart-define cameraPreviewAspectRatio=2/3 -t lib/main_$FLAVOR.dart + run: flutter build apk --release --flavor $FLAVOR --dart-define c -t lib/main_$FLAVOR.dart - name: Upload artifact uses: actions/upload-artifact@v3 diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index 6fe5d77..3851170 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -16,7 +16,7 @@ on: type: string env: - BUILD_ARGS: --release --flavor prod --dart-define cameraPreviewAspectRatio=2/3 -t lib/main_prod.dart + BUILD_ARGS: --release --flavor prod --dart-define cameraPreviewAspectRatio=240/320 -t lib/main_prod.dart jobs: build: diff --git a/.vscode/launch.json b/.vscode/launch.json index fb960be..822d34d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,7 +12,7 @@ "--flavor", "dev", "--dart-define", - "cameraPreviewAspectRatio=2/3", + "cameraPreviewAspectRatio=240/320", ], "program": "${workspaceFolder}/lib/main_dev.dart", }, @@ -36,7 +36,7 @@ "--flavor", "prod", "--dart-define", - "cameraPreviewAspectRatio=2/3", + "cameraPreviewAspectRatio=240/320", ], "program": "${workspaceFolder}/lib/main_prod.dart", }, diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 8615437..adaac0f 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -12,7 +12,7 @@ "dev", "--release", "--dart-define", - "cameraPreviewAspectRatio=2/3", + "cameraPreviewAspectRatio=240/320", "-t", "lib/main_dev.dart", ], @@ -28,7 +28,7 @@ "prod", "--release", "--dart-define", - "cameraPreviewAspectRatio=2/3", + "cameraPreviewAspectRatio=240/320", "-t", "lib/main_prod.dart", ], @@ -44,7 +44,7 @@ "prod", "--release", "--dart-define", - "cameraPreviewAspectRatio=2/3", + "cameraPreviewAspectRatio=240/320", "-t", "lib/main_prod.dart", ], diff --git a/README.md b/README.md index 2fef498..f629aa2 100644 --- a/README.md +++ b/README.md @@ -45,11 +45,11 @@ flutter pub get flutter pub run intl_utils:generate ``` -### 4. Build +### 4. Build (Android) You can build an apk by running the following command from the root of the repository: ```console -flutter build apk --release --flavor $FLAVOR --dart-define cameraPreviewAspectRatio=2/3 -t lib/main_$FLAVOR.dart +flutter build apk --release --flavor $FLAVOR --dart-define cameraPreviewAspectRatio=240/320 -t lib/main_$FLAVOR.dart ``` Just replace `$FLAVOR` with `dev` or `prod`. diff --git a/lib/data/models/metering_screen_layout_config.dart b/lib/data/models/metering_screen_layout_config.dart index 3410632..0aae055 100644 --- a/lib/data/models/metering_screen_layout_config.dart +++ b/lib/data/models/metering_screen_layout_config.dart @@ -1,11 +1,13 @@ -enum MeteringScreenLayoutFeature { extremeExposurePairs, filmPicker } +enum MeteringScreenLayoutFeature { extremeExposurePairs, filmPicker, histogram } typedef MeteringScreenLayoutConfig = Map; extension MeteringScreenLayoutConfigJson on MeteringScreenLayoutConfig { - static MeteringScreenLayoutConfig fromJson(Map data) => data.map( - (key, value) => MapEntry(MeteringScreenLayoutFeature.values[int.parse(key)], value as bool), - ); + static MeteringScreenLayoutConfig fromJson(Map data) => + { + for (final f in MeteringScreenLayoutFeature.values) + f: data[f.index.toString()] as bool? ?? true + }; Map toJson() => map((key, value) => MapEntry(key.index.toString(), value)); } diff --git a/lib/data/shared_prefs_service.dart b/lib/data/shared_prefs_service.dart index 8105d5c..8ae5487 100644 --- a/lib/data/shared_prefs_service.dart +++ b/lib/data/shared_prefs_service.dart @@ -97,6 +97,7 @@ class UserPreferencesService { return { MeteringScreenLayoutFeature.extremeExposurePairs: true, MeteringScreenLayoutFeature.filmPicker: true, + MeteringScreenLayoutFeature.histogram: true, }; } } diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 39700b8..1bfe8ed 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -38,6 +38,7 @@ "meteringScreenLayoutHint": "Hide elements on the metering screen that you don't need so that they don't waste exposure pairs list space.", "meteringScreenFeatureExtremeExposurePairs": "Fastest & shortest exposure pairs", "meteringScreenFeatureFilmPicker": "Film picker", + "meteringScreenFeatureHistogram": "Histogram", "film": "Film", "equipment": "Equipment", "equipmentProfileName": "Equipment profile name", diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 5957d0d..f75d76b 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -38,6 +38,7 @@ "meteringScreenLayoutHint": "Masquer les éléments sur l'écran de mesure dont vous n'avez pas besoin pour qu'ils ne gaspillent pas de l'espace dans les paires d'exposition.", "meteringScreenFeatureExtremeExposurePairs": "Paires d'exposition les plus rapides et les plus courtes", "meteringScreenFeatureFilmPicker": "Sélecteur de film", + "meteringScreenFeatureHistogram": "Histogramme", "film": "Pellicule", "equipment": "Équipement", "equipmentProfileName": "Nom du profil de l'équipement", diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index f6b3833..3a67fc2 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -38,6 +38,7 @@ "meteringScreenLayoutHint": "Здесь вы можете скрыть некоторые ненужные или неиспользуемые элементы с главного экрана.", "meteringScreenFeatureExtremeExposurePairs": "Длинная и короткая выдержки", "meteringScreenFeatureFilmPicker": "Выбор пленки", + "meteringScreenFeatureHistogram": "Гистограмма", "film": "Пленка", "equipment": "Оборудование", "equipmentProfileName": "Название профиля", diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index b931906..5d020cf 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -38,6 +38,7 @@ "meteringScreenLayoutHint": "隐藏不需要的元素,以免浪费曝光列表空间", "meteringScreenFeatureExtremeExposurePairs": "最快 & 最慢曝光组合", "meteringScreenFeatureFilmPicker": "胶片选择", + "meteringScreenFeatureHistogram": "直方图", "film": "胶片", "equipment": "设备", "equipmentProfileName": "设备配置名称", diff --git a/lib/screens/metering/components/camera_container/bloc_container_camera.dart b/lib/screens/metering/components/camera_container/bloc_container_camera.dart index 9c8658d..991bdf6 100644 --- a/lib/screens/metering/components/camera_container/bloc_container_camera.dart +++ b/lib/screens/metering/components/camera_container/bloc_container_camera.dart @@ -123,7 +123,7 @@ class CameraContainerBloc extends EvSourceBlocBase camera.lensDirection == CameraLensDirection.back, orElse: () => cameras.last, ), - ResolutionPreset.medium, + ResolutionPreset.low, enableAudio: false, ); diff --git a/lib/screens/metering/components/camera_container/components/camera_view/widget_camera_view.dart b/lib/screens/metering/components/camera_container/components/camera_preview/components/camera_view/widget_camera_view.dart similarity index 86% rename from lib/screens/metering/components/camera_container/components/camera_view/widget_camera_view.dart rename to lib/screens/metering/components/camera_container/components/camera_preview/components/camera_view/widget_camera_view.dart index e443ad1..7c06062 100644 --- a/lib/screens/metering/components/camera_container/components/camera_view/widget_camera_view.dart +++ b/lib/screens/metering/components/camera_container/components/camera_preview/components/camera_view/widget_camera_view.dart @@ -14,10 +14,12 @@ class CameraView extends StatelessWidget { valueListenable: controller, builder: (_, __, ___) => AspectRatio( aspectRatio: _isLandscape(value) ? value.aspectRatio : (1 / value.aspectRatio), - child: RotatedBox( - quarterTurns: _getQuarterTurns(value), - child: controller.buildPreview(), - ), + child: value.isInitialized + ? RotatedBox( + quarterTurns: _getQuarterTurns(value), + child: controller.buildPreview(), + ) + : const SizedBox.shrink(), ), ); } diff --git a/lib/screens/metering/components/camera_container/components/camera_view_placeholder/widget_placeholder_camera_view.dart b/lib/screens/metering/components/camera_container/components/camera_preview/components/camera_view_placeholder/widget_placeholder_camera_view.dart similarity index 100% rename from lib/screens/metering/components/camera_container/components/camera_view_placeholder/widget_placeholder_camera_view.dart rename to lib/screens/metering/components/camera_container/components/camera_preview/components/camera_view_placeholder/widget_placeholder_camera_view.dart diff --git a/lib/screens/metering/components/camera_container/components/camera_preview/components/histogram/widget_histogram.dart b/lib/screens/metering/components/camera_container/components/camera_preview/components/histogram/widget_histogram.dart new file mode 100644 index 0000000..6964e77 --- /dev/null +++ b/lib/screens/metering/components/camera_container/components/camera_preview/components/histogram/widget_histogram.dart @@ -0,0 +1,132 @@ +import 'dart:math'; + +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; +import 'package:lightmeter/res/dimens.dart'; + +class CameraHistogram extends StatefulWidget { + final CameraController controller; + + const CameraHistogram({required this.controller, super.key}); + + @override + _CameraHistogramState createState() => _CameraHistogramState(); +} + +class _CameraHistogramState extends State { + List histogramR = List.filled(256, 0); + List histogramG = List.filled(256, 0); + List histogramB = List.filled(256, 0); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _startImageStream(); + }); + } + + @override + void dispose() { + widget.controller.stopImageStream(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + HistogramChannel( + color: Colors.red, + values: histogramR, + ), + const SizedBox(height: Dimens.grid4), + HistogramChannel( + color: Colors.green, + values: histogramG, + ), + const SizedBox(height: Dimens.grid4), + HistogramChannel( + color: Colors.blue, + values: histogramB, + ), + ], + ); + } + + void _startImageStream() { + widget.controller.startImageStream((CameraImage image) { + histogramR = List.filled(256, 0); + histogramG = List.filled(256, 0); + histogramB = List.filled(256, 0); + + final int uvRowStride = image.planes[1].bytesPerRow; + final int uvPixelStride = image.planes[1].bytesPerPixel!; + + for (int x = 0; x < image.width; x++) { + for (int y = 0; y < image.height; y++) { + final int uvIndex = uvPixelStride * (x / 2).floor() + uvRowStride * (y / 2).floor(); + final int index = y * image.width + x; + + final yp = image.planes[0].bytes[index]; + final up = image.planes[1].bytes[uvIndex]; + final vp = image.planes[2].bytes[uvIndex]; + + final r = yp + vp * 1436 / 1024 - 179; + final g = yp - up * 46549 / 131072 + 44 - vp * 93604 / 131072 + 91; + final b = yp + up * 1814 / 1024 - 227; + + histogramR[r.round().clamp(0, 255)]++; + histogramG[g.round().clamp(0, 255)]++; + histogramB[b.round().clamp(0, 255)]++; + } + } + + if (mounted) setState(() {}); + }); + } +} + +class HistogramChannel extends StatelessWidget { + final List values; + final Color color; + + final int _maxOccurences; + + HistogramChannel({ + required this.values, + required this.color, + super.key, + }) : _maxOccurences = values.reduce((value, element) => max(value, element)); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final pixelWidth = constraints.maxWidth / values.length; + return Column( + children: [ + SizedBox( + height: Dimens.grid16, + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: values + .map( + (e) => SizedBox( + height: _maxOccurences == 0 ? 0 : Dimens.grid16 * (e / _maxOccurences), + width: pixelWidth, + child: ColoredBox(color: color), + ), + ) + .toList(), + ), + ), + const Divider(), + ], + ); + }, + ); + } +} diff --git a/lib/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart b/lib/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart new file mode 100644 index 0000000..288c678 --- /dev/null +++ b/lib/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart @@ -0,0 +1,62 @@ +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; +import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; +import 'package:lightmeter/platform_config.dart'; +import 'package:lightmeter/providers/metering_screen_layout_provider.dart'; +import 'package:lightmeter/res/dimens.dart'; +import 'package:lightmeter/screens/metering/components/camera_container/components/camera_preview/components/camera_view/widget_camera_view.dart'; +import 'package:lightmeter/screens/metering/components/camera_container/components/camera_preview/components/camera_view_placeholder/widget_placeholder_camera_view.dart'; +import 'package:lightmeter/screens/metering/components/camera_container/components/camera_preview/components/histogram/widget_histogram.dart'; +import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart'; + +class CameraPreview extends StatefulWidget { + final CameraController? controller; + final CameraErrorType? error; + + const CameraPreview({this.controller, this.error, super.key}); + + @override + State createState() => _CameraPreviewState(); +} + +class _CameraPreviewState extends State { + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: PlatformConfig.cameraPreviewAspectRatio, + child: Center( + child: Stack( + children: [ + const CameraViewPlaceholder(error: null), + AnimatedSwitcher( + duration: Dimens.switchDuration, + child: widget.controller != null + ? ValueListenableBuilder( + valueListenable: widget.controller!, + builder: (_, __, ___) => widget.controller!.value.isInitialized + ? Stack( + alignment: Alignment.bottomCenter, + children: [ + CameraView(controller: widget.controller!), + if (MeteringScreenLayout.featureOf( + context, + MeteringScreenLayoutFeature.histogram, + )) + Positioned( + left: Dimens.grid8, + right: Dimens.grid8, + bottom: Dimens.grid16, + child: CameraHistogram(controller: widget.controller!), + ), + ], + ) + : const SizedBox.shrink(), + ) + : CameraViewPlaceholder(error: widget.error), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/metering/components/camera_container/widget_container_camera.dart b/lib/screens/metering/components/camera_container/widget_container_camera.dart index 7ac43eb..a8f0a11 100644 --- a/lib/screens/metering/components/camera_container/widget_container_camera.dart +++ b/lib/screens/metering/components/camera_container/widget_container_camera.dart @@ -10,8 +10,7 @@ import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/metering/components/camera_container/bloc_container_camera.dart'; import 'package:lightmeter/screens/metering/components/camera_container/components/camera_controls/widget_camera_controls.dart'; import 'package:lightmeter/screens/metering/components/camera_container/components/camera_controls_placeholder/widget_placeholder_camera_controls.dart'; -import 'package:lightmeter/screens/metering/components/camera_container/components/camera_view/widget_camera_view.dart'; -import 'package:lightmeter/screens/metering/components/camera_container/components/camera_view_placeholder/widget_placeholder_camera_view.dart'; +import 'package:lightmeter/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart'; import 'package:lightmeter/screens/metering/components/camera_container/event_container_camera.dart'; import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart'; import 'package:lightmeter/screens/metering/components/camera_container/state_container_camera.dart'; @@ -107,20 +106,11 @@ class _CameraViewBuilder extends StatelessWidget { @override Widget build(BuildContext context) { - return AspectRatio( - aspectRatio: PlatformConfig.cameraPreviewAspectRatio, - child: BlocBuilder( - buildWhen: (previous, current) => current is! CameraActiveState, - builder: (context, state) => Center( - child: AnimatedSwitcher( - duration: Dimens.durationM, - child: switch (state) { - CameraInitializedState() => CameraView(controller: state.controller), - CameraErrorState() => CameraViewPlaceholder(error: state.error), - _ => const CameraViewPlaceholder(error: null), - }, - ), - ), + return BlocBuilder( + buildWhen: (previous, current) => current is! CameraActiveState, + builder: (context, state) => CameraPreview( + controller: state is CameraInitializedState ? state.controller : null, + error: state is CameraErrorState ? state.error : null, ), ); } @@ -161,7 +151,9 @@ class _CameraControlsBuilder extends StatelessWidget { }, ); } else { - child = const Column(children: [Expanded(child: SizedBox.shrink())],); + child = const Column( + children: [Expanded(child: SizedBox.shrink())], + ); } return AnimatedSwitcher( diff --git a/lib/screens/settings/components/metering/components/metering_screen_layout/components/meterins_screen_layout_features_dialog/widget_dialog_metering_screen_layout_features.dart b/lib/screens/settings/components/metering/components/metering_screen_layout/components/meterins_screen_layout_features_dialog/widget_dialog_metering_screen_layout_features.dart index 654a6c4..d43f011 100644 --- a/lib/screens/settings/components/metering/components/metering_screen_layout/components/meterins_screen_layout_features_dialog/widget_dialog_metering_screen_layout_features.dart +++ b/lib/screens/settings/components/metering/components/metering_screen_layout/components/meterins_screen_layout_features_dialog/widget_dialog_metering_screen_layout_features.dart @@ -71,6 +71,8 @@ class _MeteringScreenLayoutFeaturesDialogState extends State sharedPreferences.setString( UserPreferencesService.meteringScreenLayoutKey, - """{"0":false,"1":true}""", + """{"0":false,"1":true,"2":true}""", ), ).thenAnswer((_) => Future.value(true)); service.meteringScreenLayout = { MeteringScreenLayoutFeature.extremeExposurePairs: false, MeteringScreenLayoutFeature.filmPicker: true, + MeteringScreenLayoutFeature.histogram: true, }; verify( () => sharedPreferences.setString( UserPreferencesService.meteringScreenLayoutKey, - """{"0":false,"1":true}""", + """{"0":false,"1":true,"2":true}""", ), ).called(1); });