extract focal length from exif

This commit is contained in:
Vadim 2025-02-17 21:03:09 +01:00
parent 2dee63e78e
commit a7831b42e0
8 changed files with 62 additions and 40 deletions

View file

@ -1,6 +1,7 @@
enum CameraFeature {
spotMetering,
histogram,
showFocalLength,
}
typedef CameraFeaturesConfig = Map<CameraFeature, bool>;

View file

@ -22,6 +22,7 @@ class UserPreferencesService {
static const lightSensorEvCalibrationKey = "lightSensorEvCalibration";
static const meteringScreenLayoutKey = "meteringScreenLayout";
static const cameraFeaturesKey = "cameraFeatures";
static const cameraFocalLengthKey = "cameraFocalLength";
static const caffeineKey = "caffeine";
static const hapticsKey = "haptics";
@ -118,6 +119,7 @@ class UserPreferencesService {
return {
CameraFeature.spotMetering: false,
CameraFeature.histogram: false,
CameraFeature.showFocalLength: false,
};
}
}
@ -125,6 +127,9 @@ class UserPreferencesService {
set cameraFeatures(CameraFeaturesConfig value) =>
_sharedPreferences.setString(cameraFeaturesKey, json.encode(value.toJson()));
int? get cameraFocalLength => _sharedPreferences.getInt(cameraFocalLengthKey);
set cameraFocalLength(int? value) => _sharedPreferences.setInt(cameraFocalLengthKey, value!);
bool get caffeine => _sharedPreferences.getBool(caffeineKey) ?? false;
set caffeine(bool value) => _sharedPreferences.setBool(caffeineKey, value);

View file

@ -30,8 +30,7 @@ class MeteringInteractor {
if (_userPreferencesService.caffeine) {
_caffeineService.keepScreenOn(true);
}
_volumeEventsService
.setVolumeHandling(_userPreferencesService.volumeAction != VolumeAction.none);
_volumeEventsService.setVolumeHandling(_userPreferencesService.volumeAction != VolumeAction.none);
}
double get cameraEvCalibration => _userPreferencesService.cameraEvCalibration;
@ -62,15 +61,11 @@ class MeteringInteractor {
}
Future<bool> checkCameraPermission() async {
return _permissionsService
.checkCameraPermission()
.then((value) => value == PermissionStatus.granted);
return _permissionsService.checkCameraPermission().then((value) => value == PermissionStatus.granted);
}
Future<bool> requestCameraPermission() async {
return _permissionsService
.requestCameraPermission()
.then((value) => value == PermissionStatus.granted);
return _permissionsService.requestCameraPermission().then((value) => value == PermissionStatus.granted);
}
void openAppSettings() {
@ -80,4 +75,6 @@ class MeteringInteractor {
Future<bool> hasAmbientLightSensor() async => _lightSensorService.hasSensor();
Stream<int> luxStream() => _lightSensorService.luxStream();
void setCameraFocalLength(int focalLength) => _userPreferencesService.cameraFocalLength = focalLength;
}

View file

@ -4,6 +4,7 @@ import 'dart:io';
import 'dart:math' as math;
import 'package:camera/camera.dart';
import 'package:exif/exif.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -17,7 +18,7 @@ import 'package:lightmeter/screens/metering/components/camera_container/event_co
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';
import 'package:lightmeter/screens/metering/components/shared/ev_source_base/bloc_base_ev_source.dart';
import 'package:lightmeter/utils/ev_from_bytes.dart';
import 'package:lightmeter/utils/exif_utils.dart';
part 'mock_bloc_container_camera.part.dart';
@ -157,7 +158,8 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
emit(CameraInitializedState(_cameraController!));
_emitActiveState(emit);
} catch (e) {
} catch (e, stackTrace) {
_analytics.logCrash(e, stackTrace);
emit(const CameraErrorState(CameraErrorType.other));
}
}
@ -220,7 +222,9 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
final file = await _cameraController!.takePicture();
final bytes = await file.readAsBytes();
Directory(file.path).deleteSync(recursive: true);
return await evFromImage(bytes);
final tags = await readExifFromBytes(bytes);
_meteringInteractor.setCameraFocalLength(focalLengthFromTags(tags));
return evFromTags(tags);
} catch (e, stackTrace) {
_analytics.logCrash(e, stackTrace);
return null;

View file

@ -72,7 +72,9 @@ class MockCameraContainerBloc extends CameraContainerBloc {
Future<double?> _takePhoto() async {
try {
final bytes = (await rootBundle.load(PlatformConfig.cameraStubImage)).buffer.asUint8List();
return await evFromImage(bytes);
final tags = await readExifFromBytes(bytes);
_meteringInteractor.setCameraFocalLength(focalLengthFromTags(tags));
return evFromTags(tags);
} catch (e, stackTrace) {
log(e.toString(), stackTrace: stackTrace);
return null;

View file

@ -1,22 +1,21 @@
import 'dart:math' as math;
import 'package:exif/exif.dart';
import 'package:flutter/foundation.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
const String _isoExifKey = 'EXIF ISOSpeedRatings';
const String _apertureExifKey = 'EXIF FNumber';
const String _shutterSpeedExifKey = 'EXIF ExposureTime';
const String _focalLengthIn35mmExifKey = "EXIF FocalLengthIn35mmFilm";
Future<double> evFromImage(Uint8List bytes) async {
final tags = await readExifFromBytes(bytes);
double evFromTags(Map<String, IfdTag> tags) {
final iso = double.tryParse("${tags[_isoExifKey]}");
final apertureValueRatio = (tags[_apertureExifKey]?.values as IfdRatios?)?.ratios.first;
final speedValueRatio = (tags[_shutterSpeedExifKey]?.values as IfdRatios?)?.ratios.first;
if (iso == null || apertureValueRatio == null || speedValueRatio == null) {
throw ArgumentError(
'Error parsing EXIF',
'Error calculating EV',
[
if (iso == null) '$_isoExifKey: ${tags[_isoExifKey]?.printable} ${tags[_isoExifKey]?.printable.runtimeType}',
if (apertureValueRatio == null) '$_apertureExifKey: $apertureValueRatio',
@ -30,3 +29,14 @@ Future<double> evFromImage(Uint8List bytes) async {
return log2(math.pow(aperture, 2)) - log2(speed) - log2(iso / 100);
}
int focalLengthFromTags(Map<String, IfdTag> tags) {
final focalLengthIn35mm = int.tryParse("${tags[_focalLengthIn35mmExifKey]}");
if (focalLengthIn35mm == null) {
throw ArgumentError(
'Error parsing focal length',
['$_focalLengthIn35mmExifKey: ${tags[_focalLengthIn35mmExifKey]}'].join(', '),
);
}
return focalLengthIn35mm;
}

View file

@ -1,24 +0,0 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:lightmeter/utils/ev_from_bytes.dart';
void main() {
group('evFromImage', () {
test(
'camera_stub_image.jpg',
() {
final bytes = File('assets/camera_stub_image.jpg').readAsBytesSync();
expectLater(evFromImage(bytes), completion(8.25230310752341));
},
);
test(
'no EXIF',
() {
final bytes = File('assets/launcher_icon_dev_512.png').readAsBytesSync();
expectLater(evFromImage(bytes), throwsArgumentError);
},
);
});
}

View file

@ -0,0 +1,27 @@
import 'dart:io';
import 'package:exif/exif.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:lightmeter/utils/exif_utils.dart';
void main() {
group('evFromTags', () {
test(
'camera_stub_image.jpg',
() async {
final bytes = File('assets/camera_stub_image.jpg').readAsBytesSync();
final tags = await readExifFromBytes(bytes);
expectLater(evFromTags(tags), completion(8.25230310752341));
},
);
test(
'no EXIF',
() async {
final bytes = File('assets/launcher_icon_dev_512.png').readAsBytesSync();
final tags = await readExifFromBytes(bytes);
expectLater(evFromTags(tags), throwsArgumentError);
},
);
});
}