Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Add the ability for ML Kit to create image from bytes #901

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c21de5c
Make it possible to pass bytes
bparrishMines Oct 25, 2018
671b66c
Add metadata for images
bparrishMines Oct 26, 2018
956d20f
Change tests to reflect metadata
bparrishMines Oct 26, 2018
f1ccefa
Android side of byte images
bparrishMines Oct 26, 2018
09075d0
Change error string to image type
bparrishMines Oct 29, 2018
4c9c428
Use int as rotation
bparrishMines Oct 31, 2018
7def42f
Send int of rotation
bparrishMines Oct 31, 2018
d712e4f
Assertions for metadata
bparrishMines Oct 31, 2018
eb1e94f
Merge branch 'master' of github.com:flutter/plugins into mlkit_android
bparrishMines Nov 15, 2018
35ca899
Add ios implementation to take bytes
bparrishMines Nov 16, 2018
a94fdd3
Create UIImage for mlkit
bparrishMines Nov 16, 2018
26c77ae
fomatting
bparrishMines Nov 16, 2018
9aca7df
Merge branch 'master' of github.com:flutter/plugins into mlkit_android
bparrishMines Nov 16, 2018
eb2c5f6
Make single code place for creating image
bparrishMines Nov 16, 2018
3552446
Add error for wrong type
bparrishMines Nov 16, 2018
c5a2ca8
Add ios orientation
bparrishMines Nov 16, 2018
3671e81
Add TODOs and testing
bparrishMines Nov 17, 2018
724c181
Remove orientation
bparrishMines Nov 17, 2018
31b011c
Image documentation
bparrishMines Nov 17, 2018
30e3e31
version bump
bparrishMines Nov 17, 2018
857b303
formatting
bparrishMines Nov 17, 2018
c5e4894
Documentation
bparrishMines Nov 18, 2018
10e81e2
Switch statement for ImageRotation
bparrishMines Nov 18, 2018
8081a83
formatting
bparrishMines Nov 18, 2018
b139e08
Change default rotation to zero
bparrishMines Nov 26, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/firebase_ml_vision/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.2.1

* Add capability to create image from bytes.

## 0.2.0+2

* Fix bug with empty text object.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import android.net.Uri;
import com.google.firebase.ml.vision.common.FirebaseVisionImage;
import com.google.firebase.ml.vision.common.FirebaseVisionImageMetadata;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
Expand Down Expand Up @@ -29,55 +30,74 @@ public static void registerWith(Registrar registrar) {
@Override
public void onMethodCall(MethodCall call, Result result) {
Map<String, Object> options = call.argument("options");

FirebaseVisionImage image;
Map<String, Object> imageData = call.arguments();
try {
image = dataToVisionImage(imageData);
} catch (IOException exception) {
result.error("MLVisionDetectorIOError", exception.getLocalizedMessage(), null);
return;
}

switch (call.method) {
case "BarcodeDetector#detectInImage":
try {
Copy link
Contributor Author

@bparrishMines bparrishMines Nov 17, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved this to top of onMethodCall for all detectors

image = filePathToVisionImage((String) call.argument("path"));
BarcodeDetector.instance.handleDetection(image, options, result);
} catch (IOException e) {
result.error("barcodeDetectorIOError", e.getLocalizedMessage(), null);
}
BarcodeDetector.instance.handleDetection(image, options, result);
break;
case "FaceDetector#detectInImage":
try {
image = filePathToVisionImage((String) call.argument("path"));
FaceDetector.instance.handleDetection(image, options, result);
} catch (IOException e) {
result.error("faceDetectorIOError", e.getLocalizedMessage(), null);
}
FaceDetector.instance.handleDetection(image, options, result);
break;
case "LabelDetector#detectInImage":
try {
image = filePathToVisionImage((String) call.argument("path"));
LabelDetector.instance.handleDetection(image, options, result);
} catch (IOException e) {
result.error("labelDetectorIOError", e.getLocalizedMessage(), null);
}
LabelDetector.instance.handleDetection(image, options, result);
break;
case "CloudLabelDetector#detectInImage":
try {
image = filePathToVisionImage((String) call.argument("path"));
CloudLabelDetector.instance.handleDetection(image, options, result);
} catch (IOException e) {
result.error("cloudLabelDetectorIOError", e.getLocalizedMessage(), null);
}
CloudLabelDetector.instance.handleDetection(image, options, result);
break;
case "TextRecognizer#processImage":
try {
image = filePathToVisionImage((String) call.argument("path"));
TextRecognizer.instance.handleDetection(image, options, result);
} catch (IOException e) {
result.error("textRecognizerIOError", e.getLocalizedMessage(), null);
}
TextRecognizer.instance.handleDetection(image, options, result);
break;
default:
result.notImplemented();
}
}

private FirebaseVisionImage filePathToVisionImage(String path) throws IOException {
File file = new File(path);
return FirebaseVisionImage.fromFilePath(registrar.context(), Uri.fromFile(file));
private FirebaseVisionImage dataToVisionImage(Map<String, Object> imageData) throws IOException {
String imageType = (String) imageData.get("type");

switch (imageType) {
case "file":
File file = new File((String) imageData.get("path"));
return FirebaseVisionImage.fromFilePath(registrar.context(), Uri.fromFile(file));
case "bytes":
@SuppressWarnings("unchecked")
Map<String, Object> metadataData = (Map<String, Object>) imageData.get("metadata");

FirebaseVisionImageMetadata metadata =
new FirebaseVisionImageMetadata.Builder()
.setWidth((int) (double) metadataData.get("width"))
.setHeight((int) (double) metadataData.get("height"))
.setFormat(FirebaseVisionImageMetadata.IMAGE_FORMAT_NV21)
.setRotation(getRotation((int) metadataData.get("rotation")))
.build();

return FirebaseVisionImage.fromByteArray((byte[]) imageData.get("bytes"), metadata);
default:
throw new IllegalArgumentException(String.format("No image type for: %s", imageType));
}
}

private int getRotation(int rotation) {
switch (rotation) {
case 0:
return FirebaseVisionImageMetadata.ROTATION_0;
case 90:
return FirebaseVisionImageMetadata.ROTATION_90;
case 180:
return FirebaseVisionImageMetadata.ROTATION_180;
case 270:
return FirebaseVisionImageMetadata.ROTATION_270;
default:
throw new IllegalArgumentException(String.format("No rotation for: %d", rotation));
}
}
}
22 changes: 18 additions & 4 deletions packages/firebase_ml_vision/ios/Classes/FirebaseMlVisionPlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ - (instancetype)init {
}

- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {
FIRVisionImage *image = [self filePathToVisionImage:call.arguments[@"path"]];
FIRVisionImage *image = [self dataToVisionImage:call.arguments];
NSDictionary *options = call.arguments[@"options"];
if ([@"BarcodeDetector#detectInImage" isEqualToString:call.method]) {
[BarcodeDetector handleDetection:image options:options result:result];
Expand All @@ -53,8 +53,22 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result
}
}

- (FIRVisionImage *)filePathToVisionImage:(NSString *)path {
UIImage *image = [UIImage imageWithContentsOfFile:path];
return [[FIRVisionImage alloc] initWithImage:image];
- (FIRVisionImage *)dataToVisionImage:(NSDictionary *)imageData {
NSString *imageType = imageData[@"type"];

if ([@"file" isEqualToString:imageType]) {
UIImage *image = [UIImage imageWithContentsOfFile:imageData[@"path"]];
return [[FIRVisionImage alloc] initWithImage:image];
} else if ([@"bytes" isEqualToString:imageType]) {
FlutterStandardTypedData *byteData = imageData[@"bytes"];
NSData *imageBytes = byteData.data;
UIImage *image = [[UIImage alloc] initWithData:imageBytes];
// TODO(bmparr): Rotate image from imageData[@"rotation"].
return [[FIRVisionImage alloc] initWithImage:image];
} else {
NSString *errorReason = [NSString stringWithFormat:@"No image type for: %@", imageType];
@throw
[NSException exceptionWithName:NSInvalidArgumentException reason:errorReason userInfo:nil];
}
}
@end
2 changes: 2 additions & 0 deletions packages/firebase_ml_vision/lib/firebase_ml_vision.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ library firebase_ml_vision;
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'dart:ui';

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
Expand Down
3 changes: 1 addition & 2 deletions packages/firebase_ml_vision/lib/src/barcode_detector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -189,11 +189,10 @@ class BarcodeDetector extends FirebaseVisionDetector {
final List<dynamic> reply = await FirebaseVision.channel.invokeMethod(
'BarcodeDetector#detectInImage',
<String, dynamic>{
'path': visionImage.imageFile.path,
'options': <String, dynamic>{
'barcodeFormats': options.barcodeFormats.value,
},
},
}..addAll(visionImage._serialize()),
);

final List<Barcode> barcodes = <Barcode>[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class CloudDetectorOptions {
/// The type of model to use for the detection.
final CloudModelType modelType;

Map<String, dynamic> _toMap() => <String, dynamic>{
Map<String, dynamic> _serialize() => <String, dynamic>{
'maxResults': maxResults,
'modelType': _enumToString(modelType),
};
Expand Down
3 changes: 1 addition & 2 deletions packages/firebase_ml_vision/lib/src/face_detector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,14 @@ class FaceDetector extends FirebaseVisionDetector {
final List<dynamic> reply = await FirebaseVision.channel.invokeMethod(
'FaceDetector#detectInImage',
<String, dynamic>{
'path': visionImage.imageFile.path,
'options': <String, dynamic>{
'enableClassification': options.enableClassification,
'enableLandmarks': options.enableLandmarks,
'enableTracking': options.enableTracking,
'minFaceSize': options.minFaceSize,
'mode': _enumToString(options.mode),
},
},
}..addAll(visionImage._serialize()),
);

final List<Face> faces = <Face>[];
Expand Down
92 changes: 87 additions & 5 deletions packages/firebase_ml_vision/lib/src/firebase_vision.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@

part of firebase_ml_vision;

enum _ImageType { file, bytes }

/// Indicates the image rotation.
///
/// Rotation is counter-clockwise.
enum ImageRotation { rotation_0, rotation_90, rotation_180, rotation_270 }

/// The Firebase machine learning vision API.
///
/// You can get an instance by calling [FirebaseVision.instance] and then get
Expand Down Expand Up @@ -56,22 +63,83 @@ class FirebaseVision {
///
/// Create an instance by calling one of the factory constructors.
class FirebaseVisionImage {
FirebaseVisionImage._(this.imageFile);
const FirebaseVisionImage._({
@required _ImageType type,
FirebaseVisionImageMetadata metadata,
File imageFile,
Uint8List bytes,
}) : _imageFile = imageFile,
_metadata = metadata,
_bytes = bytes,
_type = type;

// TODO(bmparr): Add [ImageOrientation] when passing file.
/// Construct a [FirebaseVisionImage] from a file.
factory FirebaseVisionImage.fromFile(File imageFile) {
assert(imageFile != null);
return FirebaseVisionImage._(imageFile);
return FirebaseVisionImage._(
type: _ImageType.file,
imageFile: imageFile,
);
}

/// Construct a [FirebaseVisionImage] from a file path.
factory FirebaseVisionImage.fromFilePath(String imagePath) {
assert(imagePath != null);
return FirebaseVisionImage._(File(imagePath));
return FirebaseVisionImage._(
type: _ImageType.file,
imageFile: File(imagePath),
);
}

/// Construct a [FirebaseVisionImage] from a list of bytes.
///
/// Expects `ImageFormat.NV21` on Android and expects bytes from `NSData`
/// provided from a `UIImage` on iOS. (e.g. using
/// `UIImageJPEGRepresentation()` on the platform side).
factory FirebaseVisionImage.fromBytes(
Uint8List bytes,
FirebaseVisionImageMetadata metadata,
) {
assert(bytes != null);
return FirebaseVisionImage._(
type: _ImageType.bytes,
bytes: bytes,
metadata: metadata,
);
}

/// The file location of the image.
final File imageFile;
final Uint8List _bytes;
final File _imageFile;
final FirebaseVisionImageMetadata _metadata;
final _ImageType _type;

Map<String, dynamic> _serialize() => <String, dynamic>{
'type': _enumToString(_type),
'bytes': _bytes,
'path': _imageFile?.path,
'metadata': _type == _ImageType.bytes ? _metadata._serialize() : null,
};
}

/// Image metadata used by [FirebaseVision] detectors.
///
/// [rotation] defaults to [ImageRotation.rotation_0]. Currently only rotates on
/// Android.
class FirebaseVisionImageMetadata {
const FirebaseVisionImageMetadata({
@required this.size,
this.rotation = ImageRotation.rotation_0,
}) : assert(size != null);

final Size size;
final ImageRotation rotation;

Map<String, dynamic> _serialize() => <String, dynamic>{
'width': size.width,
'height': size.height,
'rotation': _imageRotationToInt(rotation),
};
}

/// Abstract class for detectors in [FirebaseVision] API.
Expand All @@ -80,6 +148,20 @@ abstract class FirebaseVisionDetector {
Future<dynamic> detectInImage(FirebaseVisionImage visionImage);
}

int _imageRotationToInt(ImageRotation rotation) {
switch (rotation) {
case ImageRotation.rotation_90:
return 90;
case ImageRotation.rotation_180:
return 180;
case ImageRotation.rotation_270:
return 270;
default:
assert(rotation == ImageRotation.rotation_0);
return 0;
}
}

String _enumToString(dynamic enumValue) {
final String enumString = enumValue.toString();
return enumString.substring(enumString.indexOf('.') + 1);
Expand Down
8 changes: 3 additions & 5 deletions packages/firebase_ml_vision/lib/src/label_detector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,10 @@ class LabelDetector extends FirebaseVisionDetector {
final List<dynamic> reply = await FirebaseVision.channel.invokeMethod(
'LabelDetector#detectInImage',
<String, dynamic>{
'path': visionImage.imageFile.path,
'options': <String, dynamic>{
'confidenceThreshold': options.confidenceThreshold,
},
},
}..addAll(visionImage._serialize()),
);

final List<Label> labels = <Label>[];
Expand Down Expand Up @@ -80,9 +79,8 @@ class CloudLabelDetector extends FirebaseVisionDetector {
final List<dynamic> reply = await FirebaseVision.channel.invokeMethod(
'CloudLabelDetector#detectInImage',
<String, dynamic>{
'path': visionImage.imageFile.path,
'options': options._toMap(),
},
'options': options._serialize(),
}..addAll(visionImage._serialize()),
);

final List<Label> labels = <Label>[];
Expand Down
3 changes: 1 addition & 2 deletions packages/firebase_ml_vision/lib/src/text_recognizer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,8 @@ class TextRecognizer implements FirebaseVisionDetector {
await FirebaseVision.channel.invokeMethod(
'TextRecognizer#processImage',
<String, dynamic>{
'path': visionImage.imageFile.path,
'options': <String, dynamic>{},
},
}..addAll(visionImage._serialize()),
);

return VisionText._(reply);
Expand Down
2 changes: 1 addition & 1 deletion packages/firebase_ml_vision/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ description: Flutter plugin for Google ML Vision for Firebase, an SDK that bring
learning expertise to Android and iOS apps in a powerful yet easy-to-use package.
author: Flutter Team <flutter-dev@googlegroups.com>
homepage: https://github.com/flutter/plugins/tree/master/packages/firebase_ml_vision
version: 0.2.0+2
version: 0.2.1

dependencies:
flutter:
Expand Down
Loading