From f156ea3e828f752b1b37abe34f8c8557603373bc Mon Sep 17 00:00:00 2001 From: EddyVerbruggen Date: Thu, 17 Jan 2019 20:22:26 +0100 Subject: [PATCH] Add custom (TensorFlow Lite) models support to the ML Kit feature #702 --- demo-ng/app/tabs/mlkit/mlkit.component.ts | 18 +++--- docs/ML_KIT.md | 58 +++++++++++++++++- .../features/mlkit_custom_model_tflite.png | Bin 0 -> 14163 bytes 3 files changed, 65 insertions(+), 11 deletions(-) create mode 100644 docs/images/features/mlkit_custom_model_tflite.png diff --git a/demo-ng/app/tabs/mlkit/mlkit.component.ts b/demo-ng/app/tabs/mlkit/mlkit.component.ts index 607f4af8..f7d2e502 100644 --- a/demo-ng/app/tabs/mlkit/mlkit.component.ts +++ b/demo-ng/app/tabs/mlkit/mlkit.component.ts @@ -1,21 +1,21 @@ import { Component, NgZone } from "@angular/core"; import { RouterExtensions } from "nativescript-angular"; -import { fromFile, ImageSource } from "tns-core-modules/image-source"; -import * as fileSystemModule from "tns-core-modules/file-system"; -import { action } from "tns-core-modules/ui/dialogs"; -import { ImageAsset } from "tns-core-modules/image-asset"; -import { isIOS } from "tns-core-modules/platform"; -import * as ImagePicker from "nativescript-imagepicker"; import * as Camera from "nativescript-camera"; +import * as ImagePicker from "nativescript-imagepicker"; import { BarcodeFormat, MLKitScanBarcodesOnDeviceResult } from "nativescript-plugin-firebase/mlkit/barcodescanning"; -import { MLKitLandmarkRecognitionCloudResult } from "nativescript-plugin-firebase/mlkit/landmarkrecognition"; +import { MLKitCustomModelResult } from "nativescript-plugin-firebase/mlkit/custommodel"; import { MLKitDetectFacesOnDeviceResult } from "nativescript-plugin-firebase/mlkit/facedetection"; -import { MLKitRecognizeTextResult } from "nativescript-plugin-firebase/mlkit/textrecognition"; import { MLKitImageLabelingCloudResult, MLKitImageLabelingOnDeviceResult } from "nativescript-plugin-firebase/mlkit/imagelabeling"; -import { MLKitCustomModelResult } from "nativescript-plugin-firebase/mlkit/custommodel"; +import { MLKitLandmarkRecognitionCloudResult } from "nativescript-plugin-firebase/mlkit/landmarkrecognition"; +import { MLKitRecognizeTextResult } from "nativescript-plugin-firebase/mlkit/textrecognition"; +import * as fileSystemModule from "tns-core-modules/file-system"; +import { ImageAsset } from "tns-core-modules/image-asset"; +import { fromFile, ImageSource } from "tns-core-modules/image-source"; +import { isIOS } from "tns-core-modules/platform"; +import { action } from "tns-core-modules/ui/dialogs"; const firebase = require("nativescript-plugin-firebase"); diff --git a/docs/ML_KIT.md b/docs/ML_KIT.md index 76cae09f..74f18e51 100644 --- a/docs/ML_KIT.md +++ b/docs/ML_KIT.md @@ -84,7 +84,7 @@ To be able to use Cloud features you need to do two things: |[Barcode scanning](#barcode-scanning)|✅| |[Image labeling](#image-labeling)|✅|✅ |[Landmark recognition](#landmark-recognition)||✅ -|[Custom model inference](#custom-model-inference)|| +|[Custom model inference](#custom-model-inference)|✅|✅ ### Text recognition ML Kit - Text recognition @@ -363,6 +363,60 @@ firebase.mlkit.landmarkrecognition.recognizeLandmarksCloud({ ``` ### Custom model inference +ML Kit - Custom Model (TensorFlow Lite) + [Firebase documentation 🌎](https://firebase.google.com/docs/ml-kit/use-custom-models) -Coming soon. See issue #702. +⚠️ Please take note of the following: + +- Currently only models bundled with your app can be used (not ones hosted on Firebase). That may change in the future. +- Prefix the `localModelFile` and `labelsFile` below with `~/` so they point to your `app/` folder. This is for future compatibility, because I'd like to support loading models from the native bundle as well. +- On Android, make sure the model is not compressed by adding [your model's file extension to app.gradle](https://github.com/EddyVerbruggen/nativescript-plugin-firebase/blob/57969d0a62d761bffb98b19db85af88bfae858dd/demo-ng/app/App_Resources/Android/app.gradle#L22). +- Only "Quantized" models can be used. Not "Float" models, so `modelInput.type` below must be set to `QUANT`. +- The `modelInput.shape` parameter below must specify your model's dimensions. If you're not sure, use the script in the paragraph "Specify the model's input and output" at [the Firebase docs](https://firebase.google.com/docs/ml-kit/ios/use-custom-models). + +#### Still image (on-device) + +```typescript +import { MLKitCustomModelResult } from "nativescript-plugin-firebase/mlkit/custommodel"; +const firebase = require("nativescript-plugin-firebase"); + +firebase.mlkit.custommodel.useCustomModel({ + image: imageSource, // a NativeScript Image or ImageSource, see the demo for examples + maxResults: 10, // default 5 (limit numbers to this amount of results) + localModelFile: "~/custommodel/inception/inception_v3_quant.tflite", // see the demo, where the model lives in app/custommodel/etc.. + labelsFile: "~/custommodel/inception/inception_labels.txt", + modelInput: [{ // Array + shape: [1, 299, 299, 3], // see the tips above + type: "QUANT" // for now, must be "QUANT" (and you should use a 'quantized' model (not 'float')) + }] +}) +.then((result: MLKitCustomModelResult) => console.log(JSON.stringify(result.result))) +.catch(errorMessage => console.log("ML Kit error: " + errorMessage)); +``` + +#### Live camera feed +The basics are explained above for 'Text recognition'. + +Make sure to specify `modelInputShape` without the `[` and `]` characters. + +```typescript +import { registerElement } from "nativescript-angular/element-registry"; +registerElement("MLKitCustomModel", () => require("nativescript-plugin-firebase/mlkit/custommodel").MLKitCustomModel); +``` + +```html + + +``` + diff --git a/docs/images/features/mlkit_custom_model_tflite.png b/docs/images/features/mlkit_custom_model_tflite.png new file mode 100644 index 0000000000000000000000000000000000000000..3e34088a1f3586d64d216655aa7c9a42b1751d08 GIT binary patch literal 14163 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 literal 0 HcmV?d00001