diff --git a/custom-image-labeling/.gitignore b/custom-image-labeling/.gitignore new file mode 100644 index 0000000..c5464d9 --- /dev/null +++ b/custom-image-labeling/.gitignore @@ -0,0 +1,42 @@ +# OSX +# +.DS_Store + +# node.js +# +node_modules/ +npm-debug.log +yarn-error.log + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace + +# Android/IntelliJ +# +build/ +.idea +.gradle +local.properties +*.iml + +# BUCK +buck-out/ +\.buckd/ +*.keystore diff --git a/custom-image-labeling/LICENSE b/custom-image-labeling/LICENSE new file mode 100644 index 0000000..1fc7b79 --- /dev/null +++ b/custom-image-labeling/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Ahmed Mahmoud + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/custom-image-labeling/README.md b/custom-image-labeling/README.md new file mode 100644 index 0000000..8ea009f --- /dev/null +++ b/custom-image-labeling/README.md @@ -0,0 +1,31 @@ +# @react-native-ml-kit/custom-image-labeling + +React Native On-Device Custom Image Labeling w/ Google ML Kit + +## Getting started + +`npm install @react-native-ml-kit/custom-image-labeling --save` + +### Linking + +#### React Native > 0.59 + +[CLI autolink feature](https://github.com/react-native-community/cli/blob/master/docs/autolinking.md) links the module while building the app. + +#### React Native <= 0.59 + +`react-native link @react-native-ml-kit/custom-image-labeling` + +### Installing Pods + +On iOS, use CocoaPods to add the native RNMLKitCustomImageLabeling to your project: + +`npx pod-install` + +## Usage + +```javascript +import CustomImageLabeling from '@react-native-ml-kit/custom-image-labeling'; + +const labels = await CustomImageLabeling.label(imageURL); +``` diff --git a/custom-image-labeling/RNMLKitCustomImageLabeling.podspec b/custom-image-labeling/RNMLKitCustomImageLabeling.podspec new file mode 100644 index 0000000..15e834c --- /dev/null +++ b/custom-image-labeling/RNMLKitCustomImageLabeling.podspec @@ -0,0 +1,26 @@ +# RNMLKitCustomImageLabeling.podspec + +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "package.json"))) + +Pod::Spec.new do |s| + s.name = "RNMLKitCustomImageLabeling" + s.version = package["version"] + s.summary = package["description"] + + s.homepage = "https://github.com/a7med-mahmoud/react-native-ml-kit" + # brief license entry: + s.license = "MIT" + # optional - use expanded license entry instead: + # s.license = { :type => "MIT", :file => "LICENSE" } + s.authors = { "Ahmed" => "a7med.mahmoud2004@gmail.com" } + s.platforms = { :ios => "9.0" } + s.source = { :git => "https://github.com/a7med-mahmoud/react-native-ml-kit.git", :tag => "#{s.version}" } + + s.source_files = "ios/**/*.{h,c,cc,cpp,m,mm,swift}" + s.requires_arc = true + + s.dependency "React" + s.dependency "GoogleMLKit/ImageLabeling", "2.6.0" +end diff --git a/custom-image-labeling/android/.gitignore b/custom-image-labeling/android/.gitignore new file mode 100644 index 0000000..6125af2 --- /dev/null +++ b/custom-image-labeling/android/.gitignore @@ -0,0 +1,4 @@ +*.iml +gradle/ +gradlew +gradlew.bat diff --git a/custom-image-labeling/android/.npmignore b/custom-image-labeling/android/.npmignore new file mode 100644 index 0000000..54b25fb --- /dev/null +++ b/custom-image-labeling/android/.npmignore @@ -0,0 +1,10 @@ +*.iml +.DS_Store +.gradle/ +.idea/ +.npmignore +build/ +gradle/ +gradlew +gradlew.bat +local.properties diff --git a/custom-image-labeling/android/README.md b/custom-image-labeling/android/README.md new file mode 100644 index 0000000..ac2fbda --- /dev/null +++ b/custom-image-labeling/android/README.md @@ -0,0 +1,14 @@ +README +====== + +If you want to publish the lib as a maven dependency, follow these steps before publishing a new version to npm: + +1. Be sure to have the Android [SDK](https://developer.android.com/studio/index.html) and [NDK](https://developer.android.com/ndk/guides/index.html) installed +2. Be sure to have a `local.properties` file in this folder that points to the Android SDK and NDK +``` +ndk.dir=/Users/{username}/Library/Android/sdk/ndk-bundle +sdk.dir=/Users/{username}/Library/Android/sdk +``` +3. Delete the `maven` folder +4. Run `./gradlew installArchives` +5. Verify that latest set of generated files is in the maven folder with the correct version number diff --git a/custom-image-labeling/android/build.gradle b/custom-image-labeling/android/build.gradle new file mode 100644 index 0000000..22ad7d0 --- /dev/null +++ b/custom-image-labeling/android/build.gradle @@ -0,0 +1,65 @@ +// These defaults should reflect the SDK versions used by +// the minimum React Native version supported. +def DEFAULT_COMPILE_SDK_VERSION = 28 +def DEFAULT_BUILD_TOOLS_VERSION = '28.0.3' +def DEFAULT_MIN_SDK_VERSION = 16 +def DEFAULT_TARGET_SDK_VERSION = 28 + +def safeExtGet(prop, fallback) { + rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback +} + +apply plugin: 'com.android.library' + +buildscript { + // The Android Gradle plugin is only required when opening the android folder stand-alone. + // This avoids unnecessary downloads and potential conflicts when the library is included as a + // module dependency in an application project. + // ref: https://docs.gradle.org/current/userguide/tutorial_using_tasks.html#sec:build_script_external_dependencies + if (project == rootProject) { + repositories { + google() + } + dependencies { + // This should reflect the Gradle plugin version used by + // the minimum React Native version supported. + classpath 'com.android.tools.build:gradle:3.4.1' + } + } +} + +android { + compileSdkVersion safeExtGet('compileSdkVersion', DEFAULT_COMPILE_SDK_VERSION) + buildToolsVersion safeExtGet('buildToolsVersion', DEFAULT_BUILD_TOOLS_VERSION) + defaultConfig { + minSdkVersion safeExtGet('minSdkVersion', DEFAULT_MIN_SDK_VERSION) + targetSdkVersion safeExtGet('targetSdkVersion', DEFAULT_TARGET_SDK_VERSION) + versionCode 1 + versionName "1.0" + } + lintOptions { + abortOnError false + } +} + +repositories { + mavenCentral() + // ref: https://www.baeldung.com/maven-local-repository + mavenLocal() + maven { + // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm + url "$rootDir/../node_modules/react-native/android" + } + maven { + // Android JSC is installed from npm + url "$rootDir/../node_modules/jsc-android/dist" + } + google() +} + +dependencies { + //noinspection GradleDynamicVersion + implementation 'com.facebook.react:react-native:+' // From node_modules + implementation 'com.google.mlkit:image-labeling:17.0.7' + implementation 'com.google.mlkit:image-labeling-custom:17.0.1' +} diff --git a/custom-image-labeling/android/src/main/AndroidManifest.xml b/custom-image-labeling/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..342ef91 --- /dev/null +++ b/custom-image-labeling/android/src/main/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/custom-image-labeling/android/src/main/java/com/rnmlkit/customimagelabeling/CustomImageLabelingModule.java b/custom-image-labeling/android/src/main/java/com/rnmlkit/customimagelabeling/CustomImageLabelingModule.java new file mode 100644 index 0000000..d22a1ab --- /dev/null +++ b/custom-image-labeling/android/src/main/java/com/rnmlkit/customimagelabeling/CustomImageLabelingModule.java @@ -0,0 +1,145 @@ +// CustomImageLabelingModule.java + +package com.rnmlkit.customimagelabeling; + +import android.net.Uri; +import android.content.res.AssetManager; +import android.graphics.BitmapFactory; +import android.graphics.Bitmap; + +import androidx.annotation.NonNull; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.ReadableMap; +import com.google.android.gms.tasks.OnFailureListener; +import com.google.android.gms.tasks.OnSuccessListener; +import com.google.mlkit.vision.common.InputImage; +import com.google.mlkit.common.model.LocalModel; +import com.google.mlkit.vision.label.custom.CustomImageLabelerOptions; +import com.google.mlkit.vision.label.ImageLabel; +import com.google.mlkit.vision.label.ImageLabeler; +import com.google.mlkit.vision.label.ImageLabeling; +import com.google.mlkit.vision.label.defaults.ImageLabelerOptions; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.net.URL; + +public class CustomImageLabelingModule extends ReactContextBaseJavaModule { + + private final ReactApplicationContext reactContext; + + public CustomImageLabelingModule(ReactApplicationContext reactContext) { + super(reactContext); + this.reactContext = reactContext; + } + + @Override + public String getName() { + return "CustomImageLabeling"; + } + + public static List loadLabelsFromAsset(ReactApplicationContext context, String fileName) + throws IOException { + + AssetManager manager = context.getAssets(); + InputStream is = manager.open(fileName); + + List localLabels = new ArrayList(); + + BufferedReader bf = new BufferedReader(new InputStreamReader(is, "UTF-8")); + + String line = bf.readLine(); + + while (line != null) { + localLabels.add(line); + line = bf.readLine(); + } + + is.close(); + bf.close(); + + return localLabels; + } + + public static InputImage getInputImage(ReactApplicationContext reactContext, String url) + throws IOException { + + if (url.contains("http://") || url.contains("https://")) { + URL urlInput = new URL(url); + Bitmap image = BitmapFactory.decodeStream(urlInput.openConnection().getInputStream()); + InputImage inputImage = InputImage.fromBitmap(image, 0); + return inputImage; + } + else { + Uri uri = Uri.parse(url); + InputImage inputImage = InputImage.fromFilePath(reactContext, uri); + return inputImage; + } + } + + @ReactMethod + public void label(final ReadableMap optionsMap, final Promise promise) { + + String url = optionsMap.getString("url"); + Float confidence = (float)(optionsMap.getDouble("confidence")); + + try { + InputImage image = getInputImage(this.reactContext, url); + + String localModelFilename = optionsMap.getString("localModelFilename"); + String localLabelsFilename = optionsMap.getString("localLabelsFilename"); + + List localLabels + = loadLabelsFromAsset(this.reactContext, localLabelsFilename); + + LocalModel localModel = new LocalModel.Builder() + .setAssetFilePath(localModelFilename) + .build(); + + CustomImageLabelerOptions options = + new CustomImageLabelerOptions.Builder(localModel) + .setConfidenceThreshold(confidence) + .build(); + + ImageLabeler labeler = ImageLabeling.getClient(options); + + labeler.process(image) + .addOnSuccessListener(new OnSuccessListener>() { + @Override + public void onSuccess(List labels) { + WritableArray result = Arguments.createArray(); + for (ImageLabel label : labels) { + WritableMap map = Arguments.createMap(); + map.putString("text", localLabels.get(label.getIndex())); + map.putDouble("confidence", label.getConfidence()); + map.putInt("index", label.getIndex()); + result.pushMap(map); + } + promise.resolve(result); + } + }) + .addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception e) { + e.printStackTrace(); + promise.reject("Custom Image labeling failed", e); + } + }); + } catch (IOException e) { + e.printStackTrace(); + promise.reject("Custom Image labeling failed", e); + } + } +} diff --git a/custom-image-labeling/android/src/main/java/com/rnmlkit/customimagelabeling/CustomImageLabelingPackage.java b/custom-image-labeling/android/src/main/java/com/rnmlkit/customimagelabeling/CustomImageLabelingPackage.java new file mode 100644 index 0000000..cbf36a2 --- /dev/null +++ b/custom-image-labeling/android/src/main/java/com/rnmlkit/customimagelabeling/CustomImageLabelingPackage.java @@ -0,0 +1,24 @@ +// CustomImageLabelingPackage.java + +package com.rnmlkit.customimagelabeling; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; + +public class CustomImageLabelingPackage implements ReactPackage { + @Override + public List createNativeModules(ReactApplicationContext reactContext) { + return Arrays.asList(new CustomImageLabelingModule(reactContext)); + } + + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } +} diff --git a/custom-image-labeling/example/.buckconfig b/custom-image-labeling/example/.buckconfig new file mode 100644 index 0000000..934256c --- /dev/null +++ b/custom-image-labeling/example/.buckconfig @@ -0,0 +1,6 @@ + +[android] + target = Google Inc.:Google APIs:23 + +[maven_repositories] + central = https://repo1.maven.org/maven2 diff --git a/custom-image-labeling/example/.editorconfig b/custom-image-labeling/example/.editorconfig new file mode 100644 index 0000000..7c28613 --- /dev/null +++ b/custom-image-labeling/example/.editorconfig @@ -0,0 +1,3 @@ +# Windows files +[*.bat] +end_of_line = crlf diff --git a/custom-image-labeling/example/.eslintrc.js b/custom-image-labeling/example/.eslintrc.js new file mode 100644 index 0000000..40c6dcd --- /dev/null +++ b/custom-image-labeling/example/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: '@react-native-community', +}; diff --git a/custom-image-labeling/example/.flowconfig b/custom-image-labeling/example/.flowconfig new file mode 100644 index 0000000..4320b70 --- /dev/null +++ b/custom-image-labeling/example/.flowconfig @@ -0,0 +1,65 @@ +[ignore] +; We fork some components by platform +.*/*[.]android.js + +; Ignore "BUCK" generated dirs +/\.buckd/ + +; Ignore polyfills +node_modules/react-native/Libraries/polyfills/.* + +; Flow doesn't support platforms +.*/Libraries/Utilities/LoadingView.js + +[untyped] +.*/node_modules/@react-native-community/cli/.*/.* + +[include] + +[libs] +node_modules/react-native/interface.js +node_modules/react-native/flow/ + +[options] +emoji=true + +exact_by_default=true + +format.bracket_spacing=false + +module.file_ext=.js +module.file_ext=.json +module.file_ext=.ios.js + +munge_underscores=true + +module.name_mapper='^react-native/\(.*\)$' -> '/node_modules/react-native/\1' +module.name_mapper='^@?[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> '/node_modules/react-native/Libraries/Image/RelativeImageStub' + +suppress_type=$FlowIssue +suppress_type=$FlowFixMe +suppress_type=$FlowFixMeProps +suppress_type=$FlowFixMeState + +[lints] +sketchy-null-number=warn +sketchy-null-mixed=warn +sketchy-number=warn +untyped-type-import=warn +nonstrict-import=warn +deprecated-type=warn +unsafe-getters-setters=warn +unnecessary-invariant=warn +signature-verification-failure=warn + +[strict] +deprecated-type +nonstrict-import +sketchy-null +unclear-type +unsafe-getters-setters +untyped-import +untyped-type-import + +[version] +^0.158.0 diff --git a/custom-image-labeling/example/.gitattributes b/custom-image-labeling/example/.gitattributes new file mode 100644 index 0000000..45a3dcb --- /dev/null +++ b/custom-image-labeling/example/.gitattributes @@ -0,0 +1,3 @@ +# Windows files should use crlf line endings +# https://help.github.com/articles/dealing-with-line-endings/ +*.bat text eol=crlf diff --git a/custom-image-labeling/example/.gitignore b/custom-image-labeling/example/.gitignore new file mode 100644 index 0000000..cc53454 --- /dev/null +++ b/custom-image-labeling/example/.gitignore @@ -0,0 +1,60 @@ +# OSX +# +.DS_Store + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate + +# Android/IntelliJ +# +build/ +.idea +.gradle +local.properties +*.iml +*.hprof + +# node.js +# +node_modules/ +npm-debug.log +yarn-error.log + +# BUCK +buck-out/ +\.buckd/ +*.keystore +!debug.keystore + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/ + +*/fastlane/report.xml +*/fastlane/Preview.html +*/fastlane/screenshots + +# Bundle artifact +*.jsbundle + +# CocoaPods +/ios/Pods/ diff --git a/custom-image-labeling/example/.prettierrc.js b/custom-image-labeling/example/.prettierrc.js new file mode 100644 index 0000000..abd4c02 --- /dev/null +++ b/custom-image-labeling/example/.prettierrc.js @@ -0,0 +1,6 @@ +module.exports = { + jsxBracketSameLine: true, + singleQuote: true, + trailingComma: 'all', + arrowParens: 'avoid', +}; diff --git a/custom-image-labeling/example/.watchmanconfig b/custom-image-labeling/example/.watchmanconfig new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/custom-image-labeling/example/.watchmanconfig @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/custom-image-labeling/example/App.js b/custom-image-labeling/example/App.js new file mode 100644 index 0000000..2ffb652 --- /dev/null +++ b/custom-image-labeling/example/App.js @@ -0,0 +1,61 @@ +import React, { useState } from 'react'; +import { Text, Button, StyleSheet, View } from 'react-native'; +import ImagePicker from 'react-native-image-crop-picker'; +import CustomImageLabeling from '@react-native-ml-kit/custom-image-labeling'; + +const App = () => { + const [labels, setLabels] = useState([]); + + const handlePress = async () => { + setLabels([]); + const image = await ImagePicker.openPicker({ mediaType: 'photo' }); + console.log(image.path); + const result = await CustomImageLabeling.label({ + url: image.path, + confidence: 0.8, + localModelFilename: 'mobilenet_v1_1.0_224_quant.tflite', + localLabelsFilename: 'labels_mobilenet_quant_v1_224.txt', + withFilePrefix: 'yes' + }); + console.log(result); + setLabels(result); + }; + + return ( + +