|
| 1 | +package io.flutter.plugins.firebasemlvision; |
| 2 | + |
| 3 | +import android.content.Context; |
| 4 | +import android.graphics.Bitmap; |
| 5 | +import android.graphics.BitmapFactory; |
| 6 | +import android.graphics.Matrix; |
| 7 | +import android.net.Uri; |
| 8 | +import android.util.SparseArray; |
| 9 | +import androidx.exifinterface.media.ExifInterface; |
| 10 | +import com.google.firebase.ml.vision.FirebaseVision; |
| 11 | +import com.google.firebase.ml.vision.common.FirebaseVisionImage; |
| 12 | +import com.google.firebase.ml.vision.common.FirebaseVisionImageMetadata; |
| 13 | +import io.flutter.plugin.common.MethodCall; |
| 14 | +import io.flutter.plugin.common.MethodChannel; |
| 15 | +import java.io.File; |
| 16 | +import java.io.IOException; |
| 17 | +import java.util.Map; |
| 18 | + |
| 19 | +class FirebaseMlVisionHandler implements MethodChannel.MethodCallHandler { |
| 20 | + private final SparseArray<Detector> detectors = new SparseArray<>(); |
| 21 | + private final Context applicationContext; |
| 22 | + |
| 23 | + FirebaseMlVisionHandler(Context applicationContext) { |
| 24 | + this.applicationContext = applicationContext; |
| 25 | + } |
| 26 | + |
| 27 | + @Override |
| 28 | + public void onMethodCall(MethodCall call, MethodChannel.Result result) { |
| 29 | + switch (call.method) { |
| 30 | + case "BarcodeDetector#detectInImage": |
| 31 | + case "FaceDetector#processImage": |
| 32 | + case "ImageLabeler#processImage": |
| 33 | + case "TextRecognizer#processImage": |
| 34 | + handleDetection(call, result); |
| 35 | + break; |
| 36 | + case "BarcodeDetector#close": |
| 37 | + case "FaceDetector#close": |
| 38 | + case "ImageLabeler#close": |
| 39 | + case "TextRecognizer#close": |
| 40 | + closeDetector(call, result); |
| 41 | + break; |
| 42 | + default: |
| 43 | + result.notImplemented(); |
| 44 | + } |
| 45 | + } |
| 46 | + |
| 47 | + private void handleDetection(MethodCall call, MethodChannel.Result result) { |
| 48 | + Map<String, Object> options = call.argument("options"); |
| 49 | + |
| 50 | + FirebaseVisionImage image; |
| 51 | + Map<String, Object> imageData = call.arguments(); |
| 52 | + try { |
| 53 | + image = dataToVisionImage(imageData); |
| 54 | + } catch (IOException exception) { |
| 55 | + result.error("MLVisionDetectorIOError", exception.getLocalizedMessage(), null); |
| 56 | + return; |
| 57 | + } |
| 58 | + |
| 59 | + Detector detector = getDetector(call); |
| 60 | + if (detector == null) { |
| 61 | + switch (call.method.split("#")[0]) { |
| 62 | + case "BarcodeDetector": |
| 63 | + detector = new BarcodeDetector(FirebaseVision.getInstance(), options); |
| 64 | + break; |
| 65 | + case "FaceDetector": |
| 66 | + detector = new FaceDetector(FirebaseVision.getInstance(), options); |
| 67 | + break; |
| 68 | + case "ImageLabeler": |
| 69 | + detector = new ImageLabeler(FirebaseVision.getInstance(), options); |
| 70 | + break; |
| 71 | + case "TextRecognizer": |
| 72 | + detector = new TextRecognizer(FirebaseVision.getInstance(), options); |
| 73 | + break; |
| 74 | + } |
| 75 | + |
| 76 | + final Integer handle = call.argument("handle"); |
| 77 | + addDetector(handle, detector); |
| 78 | + } |
| 79 | + |
| 80 | + detector.handleDetection(image, result); |
| 81 | + } |
| 82 | + |
| 83 | + private void closeDetector(final MethodCall call, final MethodChannel.Result result) { |
| 84 | + final Detector detector = getDetector(call); |
| 85 | + |
| 86 | + if (detector == null) { |
| 87 | + final Integer handle = call.argument("handle"); |
| 88 | + final String message = String.format("Object for handle does not exists: %s", handle); |
| 89 | + throw new IllegalArgumentException(message); |
| 90 | + } |
| 91 | + |
| 92 | + try { |
| 93 | + detector.close(); |
| 94 | + result.success(null); |
| 95 | + } catch (IOException e) { |
| 96 | + final String code = String.format("%sIOError", detector.getClass().getSimpleName()); |
| 97 | + result.error(code, e.getLocalizedMessage(), null); |
| 98 | + } finally { |
| 99 | + final Integer handle = call.argument("handle"); |
| 100 | + detectors.remove(handle); |
| 101 | + } |
| 102 | + } |
| 103 | + |
| 104 | + private FirebaseVisionImage dataToVisionImage(Map<String, Object> imageData) throws IOException { |
| 105 | + String imageType = (String) imageData.get("type"); |
| 106 | + assert imageType != null; |
| 107 | + |
| 108 | + switch (imageType) { |
| 109 | + case "file": |
| 110 | + final String imageFilePath = (String) imageData.get("path"); |
| 111 | + final int rotation = getImageExifOrientation(imageFilePath); |
| 112 | + |
| 113 | + if (rotation == 0) { |
| 114 | + File file = new File(imageFilePath); |
| 115 | + return FirebaseVisionImage.fromFilePath(applicationContext, Uri.fromFile(file)); |
| 116 | + } |
| 117 | + |
| 118 | + Matrix matrix = new Matrix(); |
| 119 | + matrix.postRotate(rotation); |
| 120 | + |
| 121 | + final Bitmap bitmap = BitmapFactory.decodeFile(imageFilePath); |
| 122 | + final Bitmap rotatedBitmap = |
| 123 | + Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); |
| 124 | + |
| 125 | + return FirebaseVisionImage.fromBitmap(rotatedBitmap); |
| 126 | + case "bytes": |
| 127 | + @SuppressWarnings("unchecked") |
| 128 | + Map<String, Object> metadataData = (Map<String, Object>) imageData.get("metadata"); |
| 129 | + |
| 130 | + FirebaseVisionImageMetadata metadata = |
| 131 | + new FirebaseVisionImageMetadata.Builder() |
| 132 | + .setWidth((int) (double) metadataData.get("width")) |
| 133 | + .setHeight((int) (double) metadataData.get("height")) |
| 134 | + .setFormat(FirebaseVisionImageMetadata.IMAGE_FORMAT_NV21) |
| 135 | + .setRotation(getRotation((int) metadataData.get("rotation"))) |
| 136 | + .build(); |
| 137 | + |
| 138 | + byte[] bytes = (byte[]) imageData.get("bytes"); |
| 139 | + assert bytes != null; |
| 140 | + |
| 141 | + return FirebaseVisionImage.fromByteArray(bytes, metadata); |
| 142 | + default: |
| 143 | + throw new IllegalArgumentException(String.format("No image type for: %s", imageType)); |
| 144 | + } |
| 145 | + } |
| 146 | + |
| 147 | + private int getImageExifOrientation(String imageFilePath) throws IOException { |
| 148 | + ExifInterface exif = new ExifInterface(imageFilePath); |
| 149 | + int orientation = |
| 150 | + exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); |
| 151 | + |
| 152 | + switch (orientation) { |
| 153 | + case ExifInterface.ORIENTATION_ROTATE_90: |
| 154 | + return 90; |
| 155 | + case ExifInterface.ORIENTATION_ROTATE_180: |
| 156 | + return 180; |
| 157 | + case ExifInterface.ORIENTATION_ROTATE_270: |
| 158 | + return 270; |
| 159 | + default: |
| 160 | + return 0; |
| 161 | + } |
| 162 | + } |
| 163 | + |
| 164 | + private int getRotation(int rotation) { |
| 165 | + switch (rotation) { |
| 166 | + case 0: |
| 167 | + return FirebaseVisionImageMetadata.ROTATION_0; |
| 168 | + case 90: |
| 169 | + return FirebaseVisionImageMetadata.ROTATION_90; |
| 170 | + case 180: |
| 171 | + return FirebaseVisionImageMetadata.ROTATION_180; |
| 172 | + case 270: |
| 173 | + return FirebaseVisionImageMetadata.ROTATION_270; |
| 174 | + default: |
| 175 | + throw new IllegalArgumentException(String.format("No rotation for: %d", rotation)); |
| 176 | + } |
| 177 | + } |
| 178 | + |
| 179 | + private void addDetector(final int handle, final Detector detector) { |
| 180 | + if (detectors.get(handle) != null) { |
| 181 | + final String message = String.format("Object for handle already exists: %s", handle); |
| 182 | + throw new IllegalArgumentException(message); |
| 183 | + } |
| 184 | + |
| 185 | + detectors.put(handle, detector); |
| 186 | + } |
| 187 | + |
| 188 | + private Detector getDetector(final MethodCall call) { |
| 189 | + final Integer handle = call.argument("handle"); |
| 190 | + return detectors.get(handle); |
| 191 | + } |
| 192 | +} |
0 commit comments