diff --git a/apps/android_camera/.gitignore b/apps/android_camera/.gitignore new file mode 100644 index 000000000000..6c2f76d2093c --- /dev/null +++ b/apps/android_camera/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.externalNativeBuild +/app/src/main/jni/jni_helper_func.h +/app/src/main/jni/org_apache_tvm_native_c_api.cc +/app/src/main/jni/org_apache_tvm_native_c_api.h +/app/src/main/obj/ +gradle/ +app/src/main/assets/models diff --git a/apps/android_camera/README.md b/apps/android_camera/README.md new file mode 100644 index 000000000000..c292ce40c582 --- /dev/null +++ b/apps/android_camera/README.md @@ -0,0 +1,28 @@ +[//]: # Licensed to the Apache Software Foundation (ASF) under one +[//]: # or more contributor license agreements. See the NOTICE file +[//]: # distributed with this work for additional information +[//]: # regarding copyright ownership. The ASF licenses this file +[//]: # to you under the Apache License, Version 2.0 (the +[//]: # "License"); you may not use this file except in compliance +[//]: # with the License. You may obtain a copy of the License at +[//]: # +[//]: # http://www.apache.org/licenses/LICENSE-2.0 +[//]: # +[//]: # Unless required by applicable law or agreed to in writing, +[//]: # software distributed under the License is distributed on an +[//]: # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +[//]: # KIND, either express or implied. See the License for the +[//]: # specific language governing permissions and limitations +[//]: # under the License. + +Android Camera Demo Sample App +============================== + +The Android Camera Demo Sample App provides a basic implementation of an Android +app that uses the tvm runtime to perform image classification in real time. + +Converting Models +----------------- + +The `models/prepare_models.py` script provides a example flow for dumping model +parameter files for use by the app. diff --git a/apps/android_camera/app/build.gradle b/apps/android_camera/app/build.gradle new file mode 100644 index 000000000000..8a772a3d29f3 --- /dev/null +++ b/apps/android_camera/app/build.gradle @@ -0,0 +1,79 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +apply plugin: 'com.android.application' + +task buildJni(type: Exec, description: 'Build JNI libs') { + commandLine 'sh', 'src/main/jni/build.sh' +} + +tasks.withType(JavaCompile) { + //compileTask -> compileTask.dependsOn buildJni +} + +android { + compileSdkVersion 29 + defaultConfig { + applicationId "ml.apache.tvm.android.androidcamerademo" + minSdkVersion 24 + targetSdkVersion 29 + renderscriptTargetApi 18 + renderscriptSupportModeEnabled true + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + sourceSets { + main { + jni.srcDirs = [] + jniLibs.srcDirs = ['src/main/libs'] + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + buildToolsVersion = '29.0.3' +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + androidTestImplementation('androidx.test.espresso:espresso-core:3.2.0', { + exclude group: 'com.android.support', module: 'support-annotations' + }) + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'com.google.android.material:material:1.1.0' + implementation 'org.apache.tvm:tvm4j-core:0.0.1-SNAPSHOT' + testImplementation 'junit:junit:4.13' + + implementation("androidx.concurrent:concurrent-futures:1.0.0") + implementation "androidx.camera:camera-core:1.0.0-beta01" + implementation "androidx.camera:camera-camera2:1.0.0-beta01" + // If you want to use the CameraX View class + implementation "androidx.camera:camera-view:1.0.0-alpha08" + // If you want to use the CameraX Extensions library + implementation "androidx.camera:camera-extensions:1.0.0-alpha08" + // If you want to use the CameraX Lifecycle library + implementation "androidx.camera:camera-lifecycle:1.0.0-beta01" +} diff --git a/apps/android_camera/app/src/main/AndroidManifest.xml b/apps/android_camera/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..0821286d5543 --- /dev/null +++ b/apps/android_camera/app/src/main/AndroidManifest.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + diff --git a/apps/android_camera/app/src/main/java/org/apache/tvm/android/androidcamerademo/Camera2BasicFragment.java b/apps/android_camera/app/src/main/java/org/apache/tvm/android/androidcamerademo/Camera2BasicFragment.java new file mode 100644 index 000000000000..f598f8e698f8 --- /dev/null +++ b/apps/android_camera/app/src/main/java/org/apache/tvm/android/androidcamerademo/Camera2BasicFragment.java @@ -0,0 +1,615 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.tvm.android.androidcamerademo; + +import android.annotation.SuppressLint; +import android.content.res.AssetManager; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.media.Image; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.SystemClock; +import android.util.Log; +import android.util.Size; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ListView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatTextView; +import androidx.camera.core.Camera; +import androidx.camera.core.CameraSelector; +import androidx.camera.core.ImageAnalysis; +import androidx.camera.core.ImageProxy; +import androidx.camera.core.Preview; +import androidx.camera.lifecycle.ProcessCameraProvider; +import androidx.camera.view.PreviewView; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; +import androidx.renderscript.Allocation; +import androidx.renderscript.Element; +import androidx.renderscript.RenderScript; +import androidx.renderscript.Script; +import androidx.renderscript.Type; + +import com.google.common.util.concurrent.ListenableFuture; + +import org.apache.tvm.Function; +import org.apache.tvm.Module; +import org.apache.tvm.NDArray; +import org.apache.tvm.TVMContext; +import org.apache.tvm.TVMType; +import org.apache.tvm.TVMValue; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.PriorityQueue; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.Semaphore; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class Camera2BasicFragment extends Fragment { + private static final String TAG = Camera2BasicFragment.class.getSimpleName(); + + // TVM constants + private static final int OUTPUT_INDEX = 0; + private static final int IMG_CHANNEL = 3; + private static final boolean EXE_GPU = false; + private static final int MODEL_INPUT_SIZE = 224; + private static final String MODEL_CL_LIB_FILE = "deploy_lib_opencl.so"; + private static final String MODEL_CPU_LIB_FILE = "deploy_lib_cpu.so"; + private static final String MODEL_GRAPH_FILE = "deploy_graph.json"; + private static final String MODEL_PARAM_FILE = "deploy_param.params"; + private static final String MODEL_LABEL_FILE = "image_net_labels.json"; + private static final String MODELS = "models"; + private static String INPUT_NAME = "input_1"; + private static String[] models; + private static String mCurModel = ""; + private final float[] mCHW = new float[MODEL_INPUT_SIZE * MODEL_INPUT_SIZE * IMG_CHANNEL]; + private final float[] mCHW2 = new float[MODEL_INPUT_SIZE * MODEL_INPUT_SIZE * IMG_CHANNEL]; + private final Semaphore isProcessingDone = new Semaphore(1); + private final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( + 3, + 3, + 1, + TimeUnit.SECONDS, + new LinkedBlockingQueue<>() + ); + // rs creation just for demo. Create rs just once in onCreate and use it again. + private RenderScript rs; + private ScriptC_yuv420888 mYuv420; + private boolean mRunClassifier = false; + + private AppCompatTextView mResultView; + private AppCompatTextView mInfoView; + private ListView mModelView; + private AssetManager assetManager; + private Module graphRuntimeModule; + private JSONObject labels; + private ListenableFuture cameraProviderFuture; + private PreviewView previewView; + private ImageAnalysis imageAnalysis; + + static Camera2BasicFragment newInstance() { + return new Camera2BasicFragment(); + } + + private static Matrix getTransformationMatrix( + final int srcWidth, + final int srcHeight, + final int dstWidth, + final int dstHeight, + final int applyRotation, + final boolean maintainAspectRatio) { + final Matrix matrix = new Matrix(); + + if (applyRotation != 0) { + if (applyRotation % 90 != 0) { + Log.w(TAG, "Rotation of %d % 90 != 0 " + applyRotation); + } + + // Translate so center of image is at origin. + matrix.postTranslate(-srcWidth / 2.0f, -srcHeight / 2.0f); + + // Rotate around origin. + matrix.postRotate(applyRotation); + } + + // Account for the already applied rotation, if any, and then determine how + // much scaling is needed for each axis. + final boolean transpose = (Math.abs(applyRotation) + 90) % 180 == 0; + + final int inWidth = transpose ? srcHeight : srcWidth; + final int inHeight = transpose ? srcWidth : srcHeight; + + // Apply scaling if necessary. + if (inWidth != dstWidth || inHeight != dstHeight) { + final float scaleFactorX = dstWidth / (float) inWidth; + final float scaleFactorY = dstHeight / (float) inHeight; + + if (maintainAspectRatio) { + // Scale by minimum factor so that dst is filled completely while + // maintaining the aspect ratio. Some image may fall off the edge. + final float scaleFactor = Math.max(scaleFactorX, scaleFactorY); + matrix.postScale(scaleFactor, scaleFactor); + } else { + // Scale exactly to fill dst from src. + matrix.postScale(scaleFactorX, scaleFactorY); + } + } + + if (applyRotation != 0) { + // Translate back from origin centered reference to destination frame. + matrix.postTranslate(dstWidth / 2.0f, dstHeight / 2.0f); + } + + return matrix; + } + + private String[] getModels() { + String[] models; + try { + models = getActivity().getAssets().list(MODELS); + } catch (IOException e) { + return null; + } + return models; + } + + @SuppressLint("DefaultLocale") + private String[] inference(float[] chw) { + NDArray inputNdArray = NDArray.empty(new long[]{1, IMG_CHANNEL, MODEL_INPUT_SIZE, MODEL_INPUT_SIZE}, new TVMType("float32")); + inputNdArray.copyFrom(chw); + Function setInputFunc = graphRuntimeModule.getFunction("set_input"); + setInputFunc.pushArg(INPUT_NAME).pushArg(inputNdArray).invoke(); + // release tvm local variables + inputNdArray.release(); + setInputFunc.release(); + + // get the function from the module(run it) + Function runFunc = graphRuntimeModule.getFunction("run"); + runFunc.invoke(); + // release tvm local variables + runFunc.release(); + + // get the function from the module(get output data) + NDArray outputNdArray = NDArray.empty(new long[]{1, 1000}, new TVMType("float32")); + Function getOutputFunc = graphRuntimeModule.getFunction("get_output"); + getOutputFunc.pushArg(OUTPUT_INDEX).pushArg(outputNdArray).invoke(); + float[] output = outputNdArray.asFloatArray(); + // release tvm local variables + outputNdArray.release(); + getOutputFunc.release(); + + if (null != output) { + String[] results = new String[5]; + // top-5 + PriorityQueue pq = new PriorityQueue<>(1000, (Integer idx1, Integer idx2) -> output[idx1] > output[idx2] ? -1 : 1); + + // display the result from extracted output data + for (int j = 0; j < output.length; ++j) { + pq.add(j); + } + for (int l = 0; l < 5; l++) { + //noinspection ConstantConditions + int idx = pq.poll(); + if (idx < labels.length()) { + try { + results[l] = String.format("%.2f", output[idx]) + " : " + labels.getString(Integer.toString(idx)); + } catch (JSONException e) { + Log.e(TAG, "index out of range", e); + } + } else { + results[l] = "???: unknown"; + } + } + return results; + } + return new String[5]; + } + + private void updateActiveModel() { + Log.i(TAG, "updating active model..."); + new LoadModelAsyncTask().execute(); + } + + @Override + public void onViewCreated(final View view, Bundle savedInstanceState) { + + mResultView = view.findViewById(R.id.resultTextView); + mInfoView = view.findViewById(R.id.infoTextView); + mModelView = view.findViewById(R.id.modelListView); + if (assetManager == null) { + assetManager = getActivity().getAssets(); + } + + mModelView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); + models = getModels(); + + ArrayAdapter modelAdapter = + new ArrayAdapter<>( + getContext(), R.layout.listview_row, R.id.listview_row_text, models); + mModelView.setAdapter(modelAdapter); + mModelView.setItemChecked(0, true); + mModelView.setOnItemClickListener( + (parent, view1, position, id) -> updateActiveModel()); + + new LoadModelAsyncTask().execute(); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + } + + @Override + public void onDestroy() { + // release tvm local variables + if (null != graphRuntimeModule) + graphRuntimeModule.release(); + super.onDestroy(); + } + + /** + * Read file from assets and return byte array. + * + * @param assets The asset manager to be used to load assets. + * @param fileName The filepath of read file. + * @return byte[] file content + * @throws IOException + */ + private byte[] getBytesFromFile(AssetManager assets, String fileName) throws IOException { + InputStream is = assets.open(fileName); + int length = is.available(); + byte[] bytes = new byte[length]; + // Read in the bytes + int offset = 0; + int numRead; + try { + while (offset < bytes.length + && (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) { + offset += numRead; + } + } finally { + is.close(); + } + // Ensure all the bytes have been read in + if (offset < bytes.length) { + throw new IOException("Could not completely read file " + fileName); + } + return bytes; + } + + /** + * Get application cache path where to place compiled functions. + * + * @param fileName library file name. + * @return String application cache folder path + * @throws IOException + */ + private String getTempLibFilePath(String fileName) throws IOException { + File tempDir = File.createTempFile("tvm4j_demo_", ""); + if (!tempDir.delete() || !tempDir.mkdir()) { + throw new IOException("Couldn't create directory " + tempDir.getAbsolutePath()); + } + return (tempDir + File.separator + fileName); + } + + private Bitmap YUV_420_888_toRGB(Image image, int width, int height) { + // Get the three image planes + Image.Plane[] planes = image.getPlanes(); + ByteBuffer buffer = planes[0].getBuffer(); + byte[] y = new byte[buffer.remaining()]; + buffer.get(y); + + buffer = planes[1].getBuffer(); + byte[] u = new byte[buffer.remaining()]; + buffer.get(u); + + buffer = planes[2].getBuffer(); + byte[] v = new byte[buffer.remaining()]; + buffer.get(v); + + int yRowStride = planes[0].getRowStride(); + int uvRowStride = planes[1].getRowStride(); + int uvPixelStride = planes[1].getPixelStride(); + + + // Y,U,V are defined as global allocations, the out-Allocation is the Bitmap. + // Note also that uAlloc and vAlloc are 1-dimensional while yAlloc is 2-dimensional. + Type.Builder typeUcharY = new Type.Builder(rs, Element.U8(rs)); + typeUcharY.setX(yRowStride).setY(height); + Allocation yAlloc = Allocation.createTyped(rs, typeUcharY.create()); + yAlloc.copyFrom(y); + mYuv420.set_ypsIn(yAlloc); + + Type.Builder typeUcharUV = new Type.Builder(rs, Element.U8(rs)); + // note that the size of the u's and v's are as follows: + // ( (width/2)*PixelStride + padding ) * (height/2) + // = (RowStride ) * (height/2) + typeUcharUV.setX(u.length); + Allocation uAlloc = Allocation.createTyped(rs, typeUcharUV.create()); + uAlloc.copyFrom(u); + mYuv420.set_uIn(uAlloc); + + Allocation vAlloc = Allocation.createTyped(rs, typeUcharUV.create()); + vAlloc.copyFrom(v); + mYuv420.set_vIn(vAlloc); + + // handover parameters + mYuv420.set_picWidth(width); + mYuv420.set_uvRowStride(uvRowStride); + mYuv420.set_uvPixelStride(uvPixelStride); + + Bitmap outBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Allocation outAlloc = Allocation.createFromBitmap(rs, outBitmap, Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT); + + Script.LaunchOptions lo = new Script.LaunchOptions(); + lo.setX(0, width); // by this we ignore the y’s padding zone, i.e. the right side of x between width and yRowStride + lo.setY(0, height); + + mYuv420.forEach_doConvert(outAlloc, lo); + outAlloc.copyTo(outBitmap); + + return outBitmap; + } + + private float[] getFrame(ImageProxy imageProxy) { + @SuppressLint("UnsafeExperimentalUsageError") + Image image = imageProxy.getImage(); + // extract the jpeg content + if (image == null) { + return null; + } + Bitmap imageBitmap = YUV_420_888_toRGB(image, image.getWidth(), image.getHeight()); + + imageProxy.close(); + // crop input image at centre to model input size + Bitmap cropImageBitmap = Bitmap.createBitmap(MODEL_INPUT_SIZE, MODEL_INPUT_SIZE, Bitmap.Config.ARGB_8888); + Matrix frameToCropTransform = getTransformationMatrix(imageBitmap.getWidth(), imageBitmap.getHeight(), + MODEL_INPUT_SIZE, MODEL_INPUT_SIZE, 0, true); + Canvas canvas = new Canvas(cropImageBitmap); + canvas.drawBitmap(imageBitmap, frameToCropTransform, null); + + // image pixel int values + int[] pixelValues = new int[MODEL_INPUT_SIZE * MODEL_INPUT_SIZE]; + // image RGB float values + + // pre-process the image data from 0-255 int to normalized float based on the + // provided parameters. + cropImageBitmap.getPixels(pixelValues, 0, MODEL_INPUT_SIZE, 0, 0, MODEL_INPUT_SIZE, MODEL_INPUT_SIZE); + for (int j = 0; j < pixelValues.length; ++j) { + mCHW2[j * 3 + 0] = ((pixelValues[j] >> 16) & 0xFF) / 255.0f; + mCHW2[j * 3 + 1] = ((pixelValues[j] >> 8) & 0xFF) / 255.0f; + mCHW2[j * 3 + 2] = (pixelValues[j] & 0xFF) / 255.0f; + } + + // pre-process the image rgb data transpose based on the provided parameters. + for (int k = 0; k < IMG_CHANNEL; ++k) { + for (int l = 0; l < MODEL_INPUT_SIZE; ++l) { + for (int m = 0; m < MODEL_INPUT_SIZE; ++m) { + int dst_index = m + MODEL_INPUT_SIZE * l + MODEL_INPUT_SIZE * MODEL_INPUT_SIZE * k; + int src_index = k + IMG_CHANNEL * m + IMG_CHANNEL * MODEL_INPUT_SIZE * l; + mCHW[dst_index] = mCHW2[src_index]; + } + } + } + + return mCHW; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + cameraProviderFuture = ProcessCameraProvider.getInstance(getActivity()); + } + + @SuppressLint({"RestrictedApi", "UnsafeExperimentalUsageError"}) + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.fragment_camera2_basic, container, false); + previewView = v.findViewById(R.id.textureView); + rs = RenderScript.create(getActivity()); + mYuv420 = new ScriptC_yuv420888(rs); + cameraProviderFuture.addListener(() -> { + try { + ProcessCameraProvider cameraProvider = cameraProviderFuture.get(); + bindPreview(cameraProvider); + } catch (ExecutionException | InterruptedException e) { + // No errors need to be handled for this Future. This should never be reached + } + }, ContextCompat.getMainExecutor(getActivity())); + + imageAnalysis = new ImageAnalysis.Builder() + .setTargetResolution(new Size(224, 224)) + .setMaxResolution(new Size(300, 300)) + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .build(); + + imageAnalysis.setAnalyzer(threadPoolExecutor, image -> { + Log.e(TAG, "w: " + image.getWidth() + " h: " + image.getHeight()); + if (mRunClassifier && isProcessingDone.tryAcquire()) { + long t1 = SystemClock.uptimeMillis(); + //float[] chw = getFrame(image); + //float[] chw = YUV_420_888_toRGBPixels(image); + float[] chw = getFrame(image); + if (chw != null) { + long t2 = SystemClock.uptimeMillis(); + String[] results = inference(chw); + long t3 = SystemClock.uptimeMillis(); + StringBuilder msgBuilder = new StringBuilder(); + for (int l = 1; l < 5; l++) { + msgBuilder.append(results[l]).append("\n"); + } + String msg = msgBuilder.toString(); + msg += "getFrame(): " + (t2 - t1) + "ms" + "\n"; + msg += "inference(): " + (t3 - t2) + "ms" + "\n"; + String finalMsg = msg; + this.getActivity().runOnUiThread(() -> { + mResultView.setText(String.format("model: %s \n %s", mCurModel, results[0])); + mInfoView.setText(finalMsg); + }); + } + isProcessingDone.release(); + } + image.close(); + }); + return v; + } + + private void bindPreview(@NonNull ProcessCameraProvider cameraProvider) { + @SuppressLint("RestrictedApi") Preview preview = new Preview.Builder() + .setMaxResolution(new Size(800, 800)) + .setTargetName("Preview") + .build(); + + preview.setSurfaceProvider(previewView.getPreviewSurfaceProvider()); + CameraSelector cameraSelector = + new CameraSelector.Builder() + .requireLensFacing(CameraSelector.LENS_FACING_BACK) + .build(); + Camera camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageAnalysis); + } + + @Override + public void onDestroyView() { + threadPoolExecutor.shutdownNow(); + super.onDestroyView(); + } + + private void setInputName(String modelName) { + if (modelName.equals("mobilenet_v2")) { + INPUT_NAME = "input_1"; + } else if (modelName.equals("resnet18_v1")) { + INPUT_NAME = "data"; + } else { + throw new RuntimeException("Model input may not be right. Please set INPUT_NAME here explicitly."); + } + } + + /* + Load precompiled model on TVM graph runtime and init the system. + */ + private class LoadModelAsyncTask extends AsyncTask { + + @Override + protected Integer doInBackground(Void... args) { + mRunClassifier = false; + // load synset name + int modelIndex = mModelView.getCheckedItemPosition(); + setInputName(models[modelIndex]); + String model = MODELS + "/" + models[modelIndex]; + String labelFilename = MODEL_LABEL_FILE; + Log.i(TAG, "Reading labels from: " + model + "/" + labelFilename); + try { + labels = new JSONObject(new String(getBytesFromFile(assetManager, model + "/" + labelFilename))); + } catch (IOException | JSONException e) { + Log.e(TAG, "Problem reading labels name file!", e); + return -1;//failure + } + + // load json graph + String modelGraph; + String graphFilename = MODEL_GRAPH_FILE; + Log.i(TAG, "Reading json graph from: " + model + "/" + graphFilename); + try { + modelGraph = new String(getBytesFromFile(assetManager, model + "/" + graphFilename)); + } catch (IOException e) { + Log.e(TAG, "Problem reading json graph file!", e); + return -1;//failure + } + + // upload tvm compiled function on application cache folder + String libCacheFilePath; + String libFilename = EXE_GPU ? MODEL_CL_LIB_FILE : MODEL_CPU_LIB_FILE; + Log.i(TAG, "Uploading compiled function to cache folder"); + try { + libCacheFilePath = getTempLibFilePath(libFilename); + byte[] modelLibByte = getBytesFromFile(assetManager, model + "/" + libFilename); + FileOutputStream fos = new FileOutputStream(libCacheFilePath); + fos.write(modelLibByte); + fos.close(); + } catch (IOException e) { + Log.e(TAG, "Problem uploading compiled function!", e); + return -1;//failure + } + + // load parameters + byte[] modelParams; + try { + modelParams = getBytesFromFile(assetManager, model + "/" + MODEL_PARAM_FILE); + } catch (IOException e) { + Log.e(TAG, "Problem reading params file!", e); + return -1;//failure + } + + Log.i(TAG, "creating java tvm context..."); + // create java tvm context + TVMContext tvmCtx = EXE_GPU ? TVMContext.opencl() : TVMContext.cpu(); + + Log.i(TAG, "loading compiled functions..."); + Log.i(TAG, libCacheFilePath); + // tvm module for compiled functions + Module modelLib = Module.load(libCacheFilePath); + + + // get global function module for graph runtime + Log.i(TAG, "getting graph runtime create handle..."); + + Function runtimeCreFun = Function.getFunction("tvm.graph_runtime.create"); + Log.i(TAG, "creating graph runtime..."); + + Log.i(TAG, "ctx type: " + tvmCtx.deviceType); + Log.i(TAG, "ctx id: " + tvmCtx.deviceId); + + TVMValue runtimeCreFunRes = runtimeCreFun.pushArg(modelGraph) + .pushArg(modelLib) + .pushArg(tvmCtx.deviceType) + .pushArg(tvmCtx.deviceId) + .invoke(); + + Log.i(TAG, "as module..."); + graphRuntimeModule = runtimeCreFunRes.asModule(); + Log.i(TAG, "getting graph runtime load params handle..."); + // get the function from the module(load parameters) + Function loadParamFunc = graphRuntimeModule.getFunction("load_params"); + Log.i(TAG, "loading params..."); + loadParamFunc.pushArg(modelParams).invoke(); + // release tvm local variables + modelLib.release(); + loadParamFunc.release(); + runtimeCreFun.release(); + mCurModel = model; + mRunClassifier = true; + return 0;//success + } + } +} diff --git a/apps/android_camera/app/src/main/java/org/apache/tvm/android/androidcamerademo/MainActivity.java b/apps/android_camera/app/src/main/java/org/apache/tvm/android/androidcamerademo/MainActivity.java new file mode 100644 index 000000000000..f9c573a5d1fe --- /dev/null +++ b/apps/android_camera/app/src/main/java/org/apache/tvm/android/androidcamerademo/MainActivity.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.tvm.android.androidcamerademo; + +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +public class MainActivity extends AppCompatActivity implements + ActivityCompat.OnRequestPermissionsResultCallback { + + private static final int PERMISSIONS_REQUEST_CODE = 1; + + private String[] getRequiredPermissions() { + try { + PackageInfo info = getPackageManager() + .getPackageInfo(getPackageName(), PackageManager.GET_PERMISSIONS); + String[] ps = info.requestedPermissions; + if (ps != null && ps.length > 0) { + return ps; + } else { + return new String[0]; + } + } catch (Exception e) { + return new String[0]; + } + } + + private boolean allPermissionsGranted() { + for (String permission : getRequiredPermissions()) { + if (ContextCompat.checkSelfPermission(this, permission) + != PackageManager.PERMISSION_GRANTED) { + return false; + } + } + return true; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + if (!allPermissionsGranted()) { + requestPermissions(getRequiredPermissions(), PERMISSIONS_REQUEST_CODE); + return; + } + startFragment(); + } + + private void startFragment() { + getSupportFragmentManager() + .beginTransaction() + .replace(R.id.container, Camera2BasicFragment.newInstance()) + .commit(); + } + + @Override + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (allPermissionsGranted()) { + startFragment(); + } else { + Toast.makeText(this, "Required permissions are not granted. App may not run", Toast.LENGTH_SHORT).show(); + finish(); + } + } +} diff --git a/apps/android_camera/app/src/main/jni/Android.mk b/apps/android_camera/app/src/main/jni/Android.mk new file mode 100644 index 000000000000..f135c97602ee --- /dev/null +++ b/apps/android_camera/app/src/main/jni/Android.mk @@ -0,0 +1,59 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +LOCAL_PATH := $(call my-dir) +MY_PATH := $(LOCAL_PATH) + +include $(CLEAR_VARS) + +LOCAL_PATH := $(MY_PATH) +ROOT_PATH := $(MY_PATH)/../../../../../.. + +ifndef config + ifneq ("$(wildcard ./config.mk)","") + config ?= config.mk + else + config ?= make/config.mk + endif +endif + +include $(config) + +LOCAL_SRC_FILES := org_apache_tvm_native_c_api.cc +LOCAL_LDFLAGS := -L$(SYSROOT)/usr/lib/ -llog + +LOCAL_C_INCLUDES := $(ROOT_PATH)/include \ + $(ROOT_PATH)/3rdparty/dlpack/include \ + $(ROOT_PATH)/3rdparty/dmlc-core/include \ + $(ROOT_PATH)/3rdparty/HalideIR/src \ + $(ROOT_PATH)/topi/include + +LOCAL_MODULE = tvm4j_runtime_packed + +LOCAL_CPP_FEATURES += exceptions +LOCAL_LDLIBS += -latomic +LOCAL_ARM_MODE := arm + +ifdef ADD_C_INCLUDES + LOCAL_C_INCLUDES += $(ADD_C_INCLUDES) +endif + +ifdef ADD_LDLIBS + LOCAL_LDLIBS += $(ADD_LDLIBS) +endif + +include $(BUILD_SHARED_LIBRARY) \ No newline at end of file diff --git a/apps/android_camera/app/src/main/jni/Application.mk b/apps/android_camera/app/src/main/jni/Application.mk new file mode 100644 index 000000000000..95a5a9697bcc --- /dev/null +++ b/apps/android_camera/app/src/main/jni/Application.mk @@ -0,0 +1,46 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +ifndef config + ifneq ("$(wildcard ./config.mk)","") + config ?= config.mk + else + config ?= make/config.mk + endif +endif + +include $(config) + +# We target every architecture except armeabi here, for two reasons: +# 1) armeabi is deprecated in NDK r16 and removed in r17 +# 2) vulkan is not supported in armeabi +APP_ABI ?= all +APP_STL := c++_shared + +APP_CPPFLAGS += -DDMLC_LOG_STACK_TRACE=0 -DTVM4J_ANDROID=1 -std=c++11 -Oz -frtti +ifeq ($(USE_OPENCL), 1) + APP_CPPFLAGS += -DTVM_OPENCL_RUNTIME=1 +endif + +ifeq ($(USE_VULKAN), 1) + APP_CPPFLAGS += -DTVM_VULKAN_RUNTIME=1 + APP_LDFLAGS += -lvulkan +endif + +ifeq ($(USE_SORT), 1) + APP_CPPFLAGS += -DUSE_SORT=1 +endif \ No newline at end of file diff --git a/apps/android_camera/app/src/main/jni/build.sh b/apps/android_camera/app/src/main/jni/build.sh new file mode 100755 index 000000000000..eb6aa2b2a748 --- /dev/null +++ b/apps/android_camera/app/src/main/jni/build.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +set -x +PATH="$PATH:/usr/local/bin" +CURR_DIR=$(cd `dirname $0`; pwd) +ROOT_DIR="$CURR_DIR/../../../../../.." +javah -o $CURR_DIR/org_apache_tvm_native_c_api.h -cp "$ROOT_DIR/jvm/core/target/*" org.apache.tvm.LibInfo || exit -1 +cp -f $ROOT_DIR/jvm/native/src/main/native/org_apache_tvm_native_c_api.cc $CURR_DIR/ || exit -1 +cp -f $ROOT_DIR/jvm/native/src/main/native/jni_helper_func.h $CURR_DIR/ || exit -1 +rm -rf $CURR_DIR/../libs +ndk-build --directory=$CURR_DIR diff --git a/apps/android_camera/app/src/main/jni/make/config.mk b/apps/android_camera/app/src/main/jni/make/config.mk new file mode 100644 index 000000000000..49e332665ad9 --- /dev/null +++ b/apps/android_camera/app/src/main/jni/make/config.mk @@ -0,0 +1,54 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +#------------------------------------------------------------------------------- +# Template configuration for compiling +# +# If you want to change the configuration, please use the following +# steps. Assume you are on the root directory. First copy the this +# file so that any local changes will be ignored by git +# +# cp make/config.mk . +# +# Next modify the according entries, and then compile by +# +# ./build.sh +# +#------------------------------------------------------------------------------- +APP_ABI = all + +APP_PLATFORM = android-24 + +# whether enable OpenCL during compile +USE_OPENCL = 0 + +# whether to enable Vulkan during compile +USE_VULKAN = 0 + +# whether to enable contrib sort functions during compile +USE_SORT = 1 + +ifeq ($(USE_VULKAN), 1) + # Statically linking vulkan requires API Level 24 or higher + APP_PLATFORM = android-24 +endif + +# the additional include headers you want to add, e.g., SDK_PATH/adrenosdk/Development/Inc +ADD_C_INCLUDES = + +# the additional link libs you want to add, e.g., ANDROID_LIB_PATH/libOpenCL.so +ADD_LDLIBS = diff --git a/apps/android_camera/app/src/main/jni/tvm_runtime.h b/apps/android_camera/app/src/main/jni/tvm_runtime.h new file mode 100644 index 000000000000..a58252e780fe --- /dev/null +++ b/apps/android_camera/app/src/main/jni/tvm_runtime.h @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/*! + * \file tvm_runtime.h + * \brief Pack all tvm runtime source files + */ +#include +#include + +/* Enable custom logging - this will cause TVM to pass every log message + * through CustomLogMessage instead of LogMessage. By enabling this, we must + * implement dmlc::CustomLogMessage::Log. We use this to pass TVM log + * messages to Android logcat. + */ +#define DMLC_LOG_CUSTOMIZE 1 + +/* Ensure that fatal errors are passed to the logger before throwing + * in LogMessageFatal + */ +#define DMLC_LOG_BEFORE_THROW 1 + +#include "../src/runtime/c_runtime_api.cc" +#include "../src/runtime/cpu_device_api.cc" +#include "../src/runtime/workspace_pool.cc" +#include "../src/runtime/library_module.cc" +#include "../src/runtime/system_library.cc" +#include "../src/runtime/module.cc" +#include "../src/runtime/registry.cc" +#include "../src/runtime/file_util.cc" +#include "../src/runtime/dso_library.cc" +#include "../src/runtime/rpc/rpc_session.cc" +#include "../src/runtime/rpc/rpc_event_impl.cc" +#include "../src/runtime/rpc/rpc_server_env.cc" +#include "../src/runtime/rpc/rpc_module.cc" +#include "../src/runtime/rpc/rpc_socket_impl.cc" +#include "../src/runtime/thread_pool.cc" +#include "../src/runtime/threading_backend.cc" +#include "../src/runtime/graph/graph_runtime.cc" +#include "../src/runtime/ndarray.cc" +#include "../src/runtime/object.cc" + +#ifdef TVM_OPENCL_RUNTIME +#include "../src/runtime/opencl/opencl_device_api.cc" +#include "../src/runtime/opencl/opencl_module.cc" +#endif + +#ifdef TVM_VULKAN_RUNTIME +#include "../src/runtime/vulkan/vulkan.cc" +#endif + +#ifdef USE_SORT +#include "../src/runtime/contrib/sort/sort.cc" +#endif + + +#include + +void dmlc::CustomLogMessage::Log(const std::string& msg) { + // This is called for every message logged by TVM. + // We pass the message to logcat. + __android_log_write(ANDROID_LOG_DEBUG, "TVM_RUNTIME", msg.c_str()); +} \ No newline at end of file diff --git a/apps/android_camera/app/src/main/res/drawable/item_selector.xml b/apps/android_camera/app/src/main/res/drawable/item_selector.xml new file mode 100644 index 000000000000..bd87d8bc06b2 --- /dev/null +++ b/apps/android_camera/app/src/main/res/drawable/item_selector.xml @@ -0,0 +1,23 @@ + + + + + + + + + + diff --git a/apps/android_camera/app/src/main/res/layout/activity_main.xml b/apps/android_camera/app/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000000..80f9ac6902d6 --- /dev/null +++ b/apps/android_camera/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,23 @@ + + + \ No newline at end of file diff --git a/apps/android_camera/app/src/main/res/layout/fragment_camera2_basic.xml b/apps/android_camera/app/src/main/res/layout/fragment_camera2_basic.xml new file mode 100644 index 000000000000..12dc53b46db3 --- /dev/null +++ b/apps/android_camera/app/src/main/res/layout/fragment_camera2_basic.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + diff --git a/apps/android_camera/app/src/main/res/layout/listview_row.xml b/apps/android_camera/app/src/main/res/layout/listview_row.xml new file mode 100644 index 000000000000..4c233dc40379 --- /dev/null +++ b/apps/android_camera/app/src/main/res/layout/listview_row.xml @@ -0,0 +1,28 @@ + + + + + diff --git a/apps/android_camera/app/src/main/res/values/colors.xml b/apps/android_camera/app/src/main/res/values/colors.xml new file mode 100644 index 000000000000..8cb461a91903 --- /dev/null +++ b/apps/android_camera/app/src/main/res/values/colors.xml @@ -0,0 +1,20 @@ + + + + #DA6A84 + #F6F6F6 + #72BF3A + diff --git a/apps/android_camera/app/src/main/res/values/strings.xml b/apps/android_camera/app/src/main/res/values/strings.xml new file mode 100644 index 000000000000..cc69189a2ab6 --- /dev/null +++ b/apps/android_camera/app/src/main/res/values/strings.xml @@ -0,0 +1,18 @@ + + + + TVM Android Demo + diff --git a/apps/android_camera/app/src/main/res/values/styles.xml b/apps/android_camera/app/src/main/res/values/styles.xml new file mode 100644 index 000000000000..545a6eeed59e --- /dev/null +++ b/apps/android_camera/app/src/main/res/values/styles.xml @@ -0,0 +1,31 @@ + + + + + + + + + + diff --git a/apps/android_camera/app/src/main/res/xml/provider_paths.xml b/apps/android_camera/app/src/main/res/xml/provider_paths.xml new file mode 100644 index 000000000000..cef8fc075a53 --- /dev/null +++ b/apps/android_camera/app/src/main/res/xml/provider_paths.xml @@ -0,0 +1,20 @@ + + + + + diff --git a/apps/android_camera/app/src/main/rs/yuv420888.rs b/apps/android_camera/app/src/main/rs/yuv420888.rs new file mode 100644 index 000000000000..05229a261024 --- /dev/null +++ b/apps/android_camera/app/src/main/rs/yuv420888.rs @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Source: https://stackoverflow.com/questions/36212904/yuv-420-888-interpretation-on-samsung-galaxy-s7-camera2 +#pragma version(1) +#pragma rs java_package_name(org.apache.tvm.android.androidcamerademo); +#pragma rs_fp_relaxed + +int32_t width; +int32_t height; + +uint picWidth, uvPixelStride, uvRowStride ; +rs_allocation ypsIn,uIn,vIn; + +// The LaunchOptions ensure that the Kernel does not enter the padding zone of Y, so yRowStride can be ignored WITHIN the Kernel. +uchar4 __attribute__((kernel)) doConvert(uint32_t x, uint32_t y) { + + // index for accessing the uIn's and vIn's + uint uvIndex= uvPixelStride * (x/2) + uvRowStride*(y/2); + + // get the y,u,v values + uchar yps= rsGetElementAt_uchar(ypsIn, x, y); + uchar u= rsGetElementAt_uchar(uIn, uvIndex); + uchar v= rsGetElementAt_uchar(vIn, uvIndex); + + // calc argb + int4 argb; + argb.r = yps + v * 1436 / 1024 - 179; + argb.g = yps -u * 46549 / 131072 + 44 -v * 93604 / 131072 + 91; + argb.b = yps +u * 1814 / 1024 - 227; + argb.a = 255; + + uchar4 out = convert_uchar4(clamp(argb, 0, 255)); + return out; +} diff --git a/apps/android_camera/build.gradle b/apps/android_camera/build.gradle new file mode 100644 index 000000000000..a58bc631dac5 --- /dev/null +++ b/apps/android_camera/build.gradle @@ -0,0 +1,49 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + jcenter() + maven { + url 'https://maven.google.com' + } + google() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.6.1' + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + jcenter() + maven { + url 'https://maven.google.com' + } + mavenLocal() + mavenCentral() + google() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/apps/android_camera/dev_tools/gen_keystore.sh b/apps/android_camera/dev_tools/gen_keystore.sh new file mode 100755 index 000000000000..ff20597b7aab --- /dev/null +++ b/apps/android_camera/dev_tools/gen_keystore.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +CURR_DIR=$(cd `dirname $0`; pwd) +keytool -genkey -keystore $CURR_DIR/tvmdemo.keystore -alias tvmdemo -keyalg RSA -validity 10000 diff --git a/apps/android_camera/dev_tools/sign_apk.sh b/apps/android_camera/dev_tools/sign_apk.sh new file mode 100755 index 000000000000..29e7777562cf --- /dev/null +++ b/apps/android_camera/dev_tools/sign_apk.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +CURR_DIR=$(cd `dirname $0`; pwd) +APK_DIR=$CURR_DIR/../app/build/outputs/apk/release +UNSIGNED_APK=$APK_DIR/app-release-unsigned.apk +SIGNED_APK=$APK_DIR/tv8mdemo-release.apk +jarsigner -verbose -keystore $CURR_DIR/tv8mdemo.keystore -signedjar $SIGNED_APK $UNSIGNED_APK 'tv8mdemo' +echo $SIGNED_APK diff --git a/apps/android_camera/gradle.properties b/apps/android_camera/gradle.properties new file mode 100644 index 000000000000..f1328bf5fd43 --- /dev/null +++ b/apps/android_camera/gradle.properties @@ -0,0 +1,21 @@ + +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +org.gradle.jvmargs=-Xmx4608M +android.useAndroidX=true +android.enableJetifier=true diff --git a/apps/android_camera/models/prepare_model.py b/apps/android_camera/models/prepare_model.py new file mode 100644 index 000000000000..36674d273bd1 --- /dev/null +++ b/apps/android_camera/models/prepare_model.py @@ -0,0 +1,123 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import logging +import pathlib +from pathlib import Path +from typing import Union +import os +from os import environ +import json + +import tvm +import tvm.relay as relay +from tvm.contrib import util, ndk, graph_runtime as runtime +from tvm.contrib.download import download_testdata, download + +target = 'llvm -target=arm64-linux-android' +target_host = None + +def del_dir(target: Union[Path, str], only_if_empty: bool = False): + target = Path(target).expanduser() + assert target.is_dir() + for p in sorted(target.glob('**/*'), reverse=True): + if not p.exists(): + continue + p.chmod(0o666) + if p.is_dir(): + p.rmdir() + else: + if only_if_empty: + raise RuntimeError(f'{p.parent} is not empty!') + p.unlink() + target.rmdir() + +def get_model(model_name, batch_size=1): + if model_name == 'resnet18_v1': + import mxnet as mx + from mxnet import gluon + from mxnet.gluon.model_zoo import vision + gluon_model = vision.get_model(model_name, pretrained=True) + img_size = 224 + data_shape = (batch_size, 3, img_size, img_size) + net, params = relay.frontend.from_mxnet(gluon_model, {"data": data_shape}) + return (net, params) + elif model_name == 'mobilenet_v2': + import keras + from keras.applications.mobilenet_v2 import MobileNetV2 + keras.backend.clear_session() # Destroys the current TF graph and creates a new one. + weights_url = ''.join(['https://github.com/JonathanCMitchell/', + 'mobilenet_v2_keras/releases/download/v1.1/', + 'mobilenet_v2_weights_tf_dim_ordering_tf_kernels_0.5_224.h5']) + weights_file = 'mobilenet_v2_weights.h5' + weights_path = download_testdata(weights_url, weights_file, module='keras') + keras_mobilenet_v2 = MobileNetV2(alpha=0.5, include_top=True, weights=None, + input_shape=(224, 224, 3), classes=1000) + keras_mobilenet_v2.load_weights(weights_path) + + img_size = 224 + data_shape = (batch_size, 3, img_size, img_size) + mod, params = relay.frontend.from_keras(keras_mobilenet_v2, {'input_1': data_shape}) + return (mod, params) + +def main(model_str, output_path): + if output_path.exists(): + del_dir(output_path) + output_path.mkdir() + output_path_str = os.fspath(output_path) + print(model_str) + print("getting model...") + net, params = get_model(model_str) + try: + os.mkdir(model_str) + except FileExistsError: + pass + print("building...") + with relay.build_config(opt_level=3): + graph, lib, params = relay.build(net, target, target_host=target_host, params=params) + print("dumping lib...") + lib.export_library(output_path_str + '/' + 'deploy_lib_cpu.so', ndk.create_shared) + print("dumping graph...") + with open(output_path_str + '/' + 'deploy_graph.json', 'w') as f: + f.write(graph) + print("dumping params...") + with open(output_path_str + '/' + 'deploy_param.params', 'wb') as f: + f.write(relay.save_param_dict(params)) + print("dumping labels...") + synset_url = ''.join(['https://gist.githubusercontent.com/zhreshold/', + '4d0b62f3d01426887599d4f7ede23ee5/raw/', + '596b27d23537e5a1b5751d2b0481ef172f58b539/', + 'imagenet1000_clsid_to_human.txt']) + synset_path = output_path_str + '/image_net_labels' + download(synset_url, output_path_str + '/image_net_labels') + with open(synset_path) as fi: + synset = eval(fi.read()) + with open(output_path_str + '/image_net_labels.json', "w") as fo: + json.dump(synset, fo, indent=4) + os.remove(synset_path) + +if __name__ == '__main__': + if environ.get('TVM_NDK_CC') is None: + raise RuntimeError("Require environment variable TVM_NDK_CC") + models_path = Path().absolute().parent.joinpath('app/src/main/assets/models/') + if not models_path.exists(): + models_path.mkdir() + models = {'mobilenet_v2': models_path.joinpath('mobilenet_v2'), + 'resnet18_v1': models_path.joinpath('resnet18_v1') + } + for model, output_path in models.items(): + main(model, output_path) diff --git a/apps/android_camera/settings.gradle b/apps/android_camera/settings.gradle new file mode 100644 index 000000000000..ee503c22bf30 --- /dev/null +++ b/apps/android_camera/settings.gradle @@ -0,0 +1,18 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +include ':app' diff --git a/tests/lint/add_asf_header.py b/tests/lint/add_asf_header.py index cdfbb9a71dd3..a44fbd3df1b5 100644 --- a/tests/lint/add_asf_header.py +++ b/tests/lint/add_asf_header.py @@ -177,7 +177,6 @@ def add_header(fname, header): print("Skip file %s ..." % fname) return - with open(fname, "w") as outfile: skipline = False ext = os.path.splitext(fname)[1][1:]