diff --git a/TeamCode/build.gradle b/TeamCode/build.gradle index 436ce671e98f..2c139c932a46 100644 --- a/TeamCode/build.gradle +++ b/TeamCode/build.gradle @@ -26,4 +26,7 @@ android { dependencies { implementation project(':FtcRobotController') annotationProcessor files('lib/OpModeAnnotationProcessor.jar') + implementation('org.tensorflow:tensorflow-lite:2.0.0') + implementation('org.tensorflow:tensorflow-lite-gpu:2.0.0') + implementation('org.tensorflow:tensorflow-lite-support:0.0.0-nightly') { changing = true } } diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFLiteimgmethod/Detector.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFLiteimgmethod/Detector.java new file mode 100644 index 000000000000..2af82d6711e7 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFLiteimgmethod/Detector.java @@ -0,0 +1,531 @@ +package org.firstinspires.ftc.teamcode.tfrec; + +import android.app.Activity; +import android.app.Fragment; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Typeface; +import android.hardware.Camera; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.params.StreamConfigurationMap; +import android.media.Image; +import android.media.ImageReader; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Trace; +import android.util.Log; +import android.util.Size; +import android.util.TypedValue; +import android.view.Surface; +import android.view.View; +import android.widget.FrameLayout; + +import org.firstinspires.ftc.robotcore.external.Telemetry; +import org.firstinspires.ftc.teamcode.R; +import org.firstinspires.ftc.teamcode.tfrec.classification.Classifier; +import org.firstinspires.ftc.teamcode.tfrec.classification.Classifier.Device; +import org.firstinspires.ftc.teamcode.tfrec.classification.Classifier.Model; +import org.firstinspires.ftc.teamcode.tfrec.utils.BorderedText; +import org.firstinspires.ftc.teamcode.tfrec.utils.ImageUtils; +import org.firstinspires.ftc.teamcode.tfrec.views.CameraConnectionFragment; +import org.firstinspires.ftc.teamcode.tfrec.views.LegacyCameraConnectionFragment; + +import java.nio.ByteBuffer; +import java.util.List; + +public class Detector implements ImageReader.OnImageAvailableListener, Camera.PreviewCallback { + private static final String TAG = "Detector"; + private Bitmap rgbFrameBitmap = null; + String cameraId; + private Integer sensorOrientation; + private BorderedText borderedText; + private int[] rgbBytes = null; + private byte[][] yuvBytes = new byte[3][]; + private Runnable imageConverter; + private Runnable postInferenceCallback; + private Handler handler; + private HandlerThread handlerThread; + protected int previewWidth = 0; + protected int previewHeight = 0; + private int yRowStride; + Telemetry telemetry; + boolean useCamera2API = false; + private boolean isProcessingFrame = false; + private Context appContext; + private Classifier classifier; + private Model model = Model.QUANTIZED_EFFICIENTNET; + private Device device = Device.CPU; + private String modelPath; + private String labelPath; + private int numThreads = 1; + private static final float TEXT_SIZE_DIP = 10; + private static final Size DESIRED_PREVIEW_SIZE = new Size(640, 480); + private List lastResults = null; + private int tfodMonitorViewId = -1; + private Fragment fragment; + + /** Input image size of the model along x axis. */ + private int imageSizeX; + /** Input image size of the model along y axis. */ + private int imageSizeY; + + + public Detector(Model modelType, String modelPath, Context ctx, Telemetry t) throws Exception{ + String modelFileName = modelPath.substring(0, modelPath.lastIndexOf('.')); + String labelFileName = String.format("%s_labels.txt", modelFileName); + modelFileName = String.format("%s.tflite", modelFileName); + init(modelType, modelFileName, labelFileName, ctx, t); + } + + public Detector(Model modelType, String modelPath, String labelPath, Context ctx, Telemetry t) throws Exception{ + init(modelType, modelPath, labelPath, ctx, t); + } + + protected void init(Model modelType, String modelPath, String labelPath, Context ctx, Telemetry t) throws Exception{ + Log.d(TAG, "Classifier. Starting Init method"); + appContext = ctx; + telemetry = t; + tfodMonitorViewId = appContext.getResources().getIdentifier( + "tfodMonitorViewId", "id", appContext.getPackageName()); + try { + ((Activity)appContext).runOnUiThread(new Runnable() { + @Override + public void run() { + //make visible + FrameLayout fm = (FrameLayout)((Activity)appContext).findViewById(tfodMonitorViewId); + if (fm != null){ + fm.setVisibility(View.VISIBLE); + } + } + }); + + setModelType(modelType); + setModelPath(modelPath); + setLabelPath(labelPath); + Log.d(TAG, "Classifier. Init method complete"); + } catch (Exception e) { + Log.e(TAG, "Init method error", e); + throw new Exception("Make frame visible", e); + } + } + + protected synchronized void startProcessing() { + handlerThread = new HandlerThread("inference"); + handlerThread.start(); + handler = new Handler(handlerThread.getLooper()); + } + + public synchronized void stopProcessing() { + if(handlerThread == null) { + return; + } + handlerThread.quitSafely(); + try { + handlerThread.join(); + handlerThread = null; + handler = null; + if (fragment != null) { + ((Activity) appContext).getFragmentManager().beginTransaction().remove(fragment).commit(); + } + + } catch (final InterruptedException e) { + Log.e(TAG, "Unable to stop processing", e); + } + } + + + public void activate() throws Exception{ + startProcessing(); + final CameraManager manager = (CameraManager) appContext.getSystemService(Context.CAMERA_SERVICE); + try { + for (final String cameraId : manager.getCameraIdList()) { + final CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId); + Log.d(TAG, String.format("Activation. Cam ID: %s", cameraId)); + + final Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING); + Log.d(TAG, String.format("Activation. Facing: %d", facing)); + + Integer orientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); + Log.d(TAG, String.format("Activation. Orientation: %d", orientation)); + + if (facing != null && facing == CameraCharacteristics.LENS_FACING_BACK) { + + final StreamConfigurationMap map = + characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); + + if (map == null) { + continue; + } + + // Fallback to camera1 API for internal cameras that don't have full support. + // This should help with legacy situations where using the camera2 API causes + // distorted or otherwise broken previews. + useCamera2API = (facing == CameraCharacteristics.LENS_FACING_EXTERNAL) + || isHardwareLevelSupported( + characteristics, CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL); + Log.d(TAG, String.format("Activation. Camera API lv2?: %b", useCamera2API)); + this.cameraId = cameraId; + break; + } + } + Log.d(TAG, String.format("Activation. Selected Camera: %s", cameraId)); + setFragment(); + Log.d(TAG, "Activation. Complete"); + } catch (CameraAccessException e) { + Log.e(TAG, "Not allowed to access camera", e); + throw new Exception("Not allowed to access camera", e); + } + catch (Exception e) { + Log.e(TAG, "Problems with activation", e); + throw new Exception("Problems with activation", e); + } + } + + protected void setFragment() { + if (useCamera2API) { + CameraConnectionFragment camera2Fragment = + CameraConnectionFragment.newInstance( + new CameraConnectionFragment.ConnectionCallback() { + @Override + public void onPreviewSizeChosen(final Size size, final int rotation) { + previewHeight = size.getHeight(); + previewWidth = size.getWidth(); + Detector.this.onPreviewSizeChosen(size, rotation); + } + }, + this, + getLayoutId(), + getDesiredPreviewFrameSize(), telemetry); + + camera2Fragment.setCamera(this.cameraId); + fragment = camera2Fragment; + } else { + fragment = + new LegacyCameraConnectionFragment(this, getLayoutId(), getDesiredPreviewFrameSize(), telemetry); + } + + + ((Activity)appContext).getFragmentManager().beginTransaction().replace(tfodMonitorViewId, fragment).commit(); + Log.d(TAG, "SetFragment. Complete"); + } + + protected int getLayoutId() { + return R.layout.tfe_ic_camera_connection_fragment; + } + + protected Size getDesiredPreviewFrameSize() { + return DESIRED_PREVIEW_SIZE; + } + + private boolean isHardwareLevelSupported( + CameraCharacteristics characteristics, int requiredLevel) { + int deviceLevel = characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL); + if (deviceLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) { + return requiredLevel == deviceLevel; + } + // deviceLevel is not LEGACY, can use numerical sort + return requiredLevel <= deviceLevel; + } + + + protected int[] getRgbBytes() { + imageConverter.run(); + return rgbBytes; + } + + protected void fillBytes(final Image.Plane[] planes, final byte[][] yuvBytes) { + // Because of the variable row stride it's not possible to know in + // advance the actual necessary dimensions of the yuv planes. + for (int i = 0; i < planes.length; ++i) { + final ByteBuffer buffer = planes[i].getBuffer(); + if (yuvBytes[i] == null) { + yuvBytes[i] = new byte[buffer.capacity()]; + } + buffer.get(yuvBytes[i]); + } + } + + + protected void readyForNextImage() { + if (postInferenceCallback != null) { + postInferenceCallback.run(); + } + } + + protected int getScreenOrientation() { + switch (((Activity)appContext).getWindowManager().getDefaultDisplay().getRotation()) { + case Surface.ROTATION_270: + return 270; + case Surface.ROTATION_180: + return 180; + case Surface.ROTATION_90: + return 90; + default: + return 0; + } + } + + protected synchronized void runInBackground(final Runnable r) { + if (handler != null) { + handler.post(r); + } + } + + @Override + public void onPreviewFrame(final byte[] bytes, final Camera camera) { + if (isProcessingFrame) { + Log.d(TAG, "onPreviewFrame. Dropping frame"); + return; + } + + try { + // Initialize the storage bitmaps once when the resolution is known. + if (rgbBytes == null) { + Camera.Size previewSize = camera.getParameters().getPreviewSize(); + previewHeight = previewSize.height; + previewWidth = previewSize.width; + rgbBytes = new int[previewWidth * previewHeight]; + onPreviewSizeChosen(new Size(previewSize.width, previewSize.height), 90); + } + } catch (final Exception e) { + Log.e(TAG, "onPreviewFrame. Error", e); + return; + } + + if (classifier == null) { + Log.e(TAG, "No classifier on preview!"); + return; + } + + isProcessingFrame = true; + yuvBytes[0] = bytes; + yRowStride = previewWidth; + + imageConverter = + new Runnable() { + @Override + public void run() { + ImageUtils.convertYUV420SPToARGB8888(bytes, previewWidth, previewHeight, rgbBytes); + } + }; + + postInferenceCallback = + new Runnable() { + @Override + public void run() { + camera.addCallbackBuffer(bytes); + isProcessingFrame = false; + } + }; + processImage(); + } + + public void onPreviewSizeChosen(final Size size, final int rotation) { + final float textSizePx = + TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, TEXT_SIZE_DIP, appContext.getResources().getDisplayMetrics()); + borderedText = new BorderedText(textSizePx); + borderedText.setTypeface(Typeface.MONOSPACE); + + recreateClassifier(getModelType(), getDevice(), getNumThreads()); + if (classifier == null) { + Log.e(TAG, "onPreviewSizeChosen. No classifier on preview!"); + return; + } + + previewWidth = size.getWidth(); + previewHeight = size.getHeight(); + + sensorOrientation = rotation - getScreenOrientation(); + + Log.d(TAG, String.format("onPreviewSizeChosen. Camera orientation relative to screen canvas: %d", sensorOrientation)); + Log.d(TAG, String.format("onPreviewSizeChosen. Initializing at size %dx%ds:", previewWidth, previewHeight)); + rgbFrameBitmap = Bitmap.createBitmap(previewWidth, previewHeight, Bitmap.Config.ARGB_8888); + } + + protected void processImage() { + rgbFrameBitmap.setPixels(getRgbBytes(), 0, previewWidth, 0, 0, previewWidth, previewHeight); +// final int cropSize = Math.min(previewWidth, previewHeight); + + runInBackground( + new Runnable() { + @Override + public void run() { + if (classifier != null) { + lastResults = classifier.recognizeImage(rgbFrameBitmap, sensorOrientation); + } + else{ + Log.e(TAG, "processImage. No Classifier to process image"); + } + readyForNextImage(); + } + }); + } + + @Override + public void onImageAvailable(ImageReader imageReader) { + if (previewWidth == 0 || previewHeight == 0) { + return; + } + if (rgbBytes == null) { + rgbBytes = new int[previewWidth * previewHeight]; + } + try { + final Image image = imageReader.acquireLatestImage(); + + if (image == null) { + return; + } + + if (isProcessingFrame) { + image.close(); + return; + } + isProcessingFrame = true; + Trace.beginSection("imageAvailable"); + final Image.Plane[] planes = image.getPlanes(); + fillBytes(planes, yuvBytes); + yRowStride = planes[0].getRowStride(); + final int uvRowStride = planes[1].getRowStride(); + final int uvPixelStride = planes[1].getPixelStride(); + + imageConverter = + new Runnable() { + @Override + public void run() { + ImageUtils.convertYUV420ToARGB8888( + yuvBytes[0], + yuvBytes[1], + yuvBytes[2], + previewWidth, + previewHeight, + yRowStride, + uvRowStride, + uvPixelStride, + rgbBytes); + } + }; + + postInferenceCallback = + new Runnable() { + @Override + public void run() { + image.close(); + isProcessingFrame = false; + } + }; + + processImage(); + } catch (final Exception e) { + Log.e(TAG, "onImageAvailable error", e); + Trace.endSection(); + return; + } + Trace.endSection(); + } + + protected void onInferenceConfigurationChanged() { + if (rgbFrameBitmap == null) { + // Defer creation until we're getting camera frames. + return; + } + final Device device = getDevice(); + final Model model = getModelType(); + final int numThreads = getNumThreads(); + runInBackground(new Runnable() { + @Override + public void run() { + Detector.this.recreateClassifier(model, device, numThreads); + } + }); + } + + private void recreateClassifier(Model model, Device device, int numThreads) { + if (classifier != null) { + Log.d(TAG, "recreateClassifier. Closing classifier"); + classifier.close(); + classifier = null; + } + if (device == Device.GPU + && (model == Model.QUANTIZED_MOBILENET || model == Model.QUANTIZED_EFFICIENTNET)) { + Log.e(TAG, "recreateClassifier. Not creating classifier: GPU doesn't support quantized models."); + return; + } + try { + classifier = Classifier.create((Activity)this.appContext, model, device, numThreads, this.getModelPath(), this.getLabelPath(), telemetry); + Log.d(TAG, String.format("Created classifier (model=%s, device=%s, numThreads=%d)", model, device, numThreads)); + } + catch (Exception e) { + Log.e(TAG, "Failed to create classifier", e); + } + + if (classifier != null) { + // Updates the input image size. + imageSizeX = classifier.getImageSizeX(); + imageSizeY = classifier.getImageSizeY(); + Log.d(TAG, "Classifier done"); + } + else{ + Log.e(TAG, "Failed to create classifier"); + } + } + + protected Model getModelType() { + return model; + } + + public void setModelType(Model model) { + if (this.model != model) { + this.model = model; + Log.d(TAG, String.format("Updated model %s", model)); +// onInferenceConfigurationChanged(); + } + } + + protected Device getDevice() { + return device; + } + + + private void setDevice(Device device) { + if (this.device != device) { + this.device = device; + Log.d(TAG, String.format("Updated device %s", device)); + final boolean threadsEnabled = device == Device.CPU; + onInferenceConfigurationChanged(); + } + } + + protected int getNumThreads() { + return numThreads; + } + + private void setNumThreads(int numThreads) { + if (this.numThreads != numThreads) { + this.numThreads = numThreads; + onInferenceConfigurationChanged(); + } + } + + public List getLastResults() { + return lastResults; + } + + public String getModelPath() { + return modelPath; + } + + public void setModelPath(String modelPath) { + this.modelPath = modelPath; + } + + public String getLabelPath() { + return labelPath; + } + + public void setLabelPath(String labelPath) { + this.labelPath = labelPath; + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFLiteimgmethod/classification/Classifier.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFLiteimgmethod/classification/Classifier.java new file mode 100644 index 000000000000..43ad052c76fb --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFLiteimgmethod/classification/Classifier.java @@ -0,0 +1,379 @@ +package org.firstinspires.ftc.teamcode.tfrec.classification; + +import android.app.Activity; +import android.graphics.Bitmap; +import android.graphics.RectF; +import android.os.SystemClock; +import android.os.Trace; +import android.util.Log; + +import org.firstinspires.ftc.robotcore.external.Telemetry; +import org.tensorflow.lite.DataType; +import org.tensorflow.lite.Interpreter; +import org.tensorflow.lite.gpu.GpuDelegate; +import org.tensorflow.lite.nnapi.NnApiDelegate; +import org.tensorflow.lite.support.common.FileUtil; +import org.tensorflow.lite.support.common.TensorOperator; +import org.tensorflow.lite.support.common.TensorProcessor; +import org.tensorflow.lite.support.image.ImageProcessor; +import org.tensorflow.lite.support.image.TensorImage; +import org.tensorflow.lite.support.image.ops.ResizeOp; +import org.tensorflow.lite.support.image.ops.ResizeWithCropOrPadOp; +import org.tensorflow.lite.support.image.ops.Rot90Op; +import org.tensorflow.lite.support.label.TensorLabel; +import org.tensorflow.lite.support.tensorbuffer.TensorBuffer; + +import java.io.IOException; +import java.nio.MappedByteBuffer; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.PriorityQueue; + +import static java.lang.Math.min; + +public abstract class Classifier { + private static final String TAG = "Classifier"; + private Telemetry telemetry; + private String modelPath; + private String labelPath; + + public void setModelPath(String modelPath) { + this.modelPath = modelPath; + } + + public void setLabelPath(String labelPath) { + this.labelPath = labelPath; + } + + /** The model type used for classification. */ + public enum Model { + FLOAT_MOBILENET, + QUANTIZED_MOBILENET, + FLOAT_EFFICIENTNET, + QUANTIZED_EFFICIENTNET + } + + /** The runtime device type used for executing classification. */ + public enum Device { + CPU, + NNAPI, + GPU + } + + /** Number of results to show in the UI. */ + private static final int MAX_RESULTS = 3; + + /** The loaded TensorFlow Lite model. */ + + /** Image size along the x axis. */ + private final int imageSizeX; + + /** Image size along the y axis. */ + private final int imageSizeY; + + /** Optional GPU delegate for accleration. */ + private GpuDelegate gpuDelegate = null; + + /** Optional NNAPI delegate for accleration. */ + private NnApiDelegate nnApiDelegate = null; + + /** An instance of the driver class to run model inference with Tensorflow Lite. */ + protected Interpreter tflite; + + /** Options for configuring the Interpreter. */ + private final Interpreter.Options tfliteOptions = new Interpreter.Options(); + + /** Labels corresponding to the output of the vision model. */ + private final List labels; + + /** Input image TensorBuffer. */ + private TensorImage inputImageBuffer; + + /** Output probability TensorBuffer. */ + private final TensorBuffer outputProbabilityBuffer; + + /** Processer to apply post processing of the output probability. */ + private final TensorProcessor probabilityProcessor; + + /** + * Creates a classifier with the provided configuration. + * + * @param activity The current Activity. + * @param model The model to use for classification. + * @param device The device to use for classification. + * @param numThreads The number of threads to use for classification. + * @return A classifier with the desired configuration. + */ + public static Classifier create(Activity activity, Model model, Device device, int numThreads, String modelFileName, String labelFileName, Telemetry t) + throws Exception { + if (model == Model.QUANTIZED_MOBILENET) { + return new ClassifierQuantizedMobileNet(activity, device, numThreads, modelFileName, labelFileName, t); + } else if (model == Model.FLOAT_MOBILENET) { + return new ClassifierFloatMobileNet(activity, device, numThreads, modelFileName, labelFileName, t); + } else if (model == Model.FLOAT_EFFICIENTNET) { + return new ClassifierFloatEfficientNet(activity, device, numThreads, modelFileName, labelFileName, t); + } else if (model == Model.QUANTIZED_EFFICIENTNET) { + return new ClassifierQuantizedEfficientNet(activity, device, numThreads, modelFileName, labelFileName, t); + } else { + throw new UnsupportedOperationException(); + } + } + + /** An immutable result returned by a Classifier describing what was recognized. */ + public static class Recognition { + /** + * A unique identifier for what has been recognized. Specific to the class, not the instance of + * the object. + */ + private final String id; + + /** Display name for the recognition. */ + private final String title; + + /** + * A sortable score for how good the recognition is relative to others. Higher should be better. + */ + private final Float confidence; + + /** Optional location within the source image for the location of the recognized object. */ + private RectF location; + + public Recognition( + final String id, final String title, final Float confidence, final RectF location) { + this.id = id; + this.title = title; + this.confidence = confidence; + this.location = location; + } + + public String getId() { + return id; + } + + public String getTitle() { + return title; + } + + public Float getConfidence() { + return confidence; + } + + public RectF getLocation() { + return new RectF(location); + } + + public void setLocation(RectF location) { + this.location = location; + } + + @Override + public String toString() { + String resultString = ""; + if (id != null) { + resultString += "[" + id + "] "; + } + + if (title != null) { + resultString += title + " "; + } + + if (confidence != null) { + resultString += String.format("(%.1f%%) ", confidence * 100.0f); + } + + if (location != null) { + resultString += location + " "; + } + + return resultString.trim(); + } + } + + /** Initializes a {@code Classifier}. */ + protected Classifier(Activity activity, Device device, int numThreads, String modelFileName, String labelFileName, Telemetry t) throws Exception { + this.telemetry = t; + this.setModelPath(modelFileName); + this.setLabelPath(labelFileName); + try { + MappedByteBuffer tfliteModel = FileUtil.loadMappedFile(activity, getModelPath()); + switch (device) { + case NNAPI: + nnApiDelegate = new NnApiDelegate(); + tfliteOptions.addDelegate(nnApiDelegate); + break; + case GPU: + gpuDelegate = new GpuDelegate(); + tfliteOptions.addDelegate(gpuDelegate); + break; + case CPU: + break; + } + tfliteOptions.setNumThreads(numThreads); + tflite = new Interpreter(tfliteModel, tfliteOptions); + + // Loads labels out from the label file. + labels = FileUtil.loadLabels(activity, getLabelPath()); + + Log.d(TAG, "Classifier. Labels loaded"); + + // Reads type and shape of input and output tensors, respectively. + int imageTensorIndex = 0; + int[] imageShape = tflite.getInputTensor(imageTensorIndex).shape(); // {1, height, width, 3} + imageSizeY = imageShape[1]; + imageSizeX = imageShape[2]; + DataType imageDataType = tflite.getInputTensor(imageTensorIndex).dataType(); + int probabilityTensorIndex = 0; + int[] probabilityShape = + tflite.getOutputTensor(probabilityTensorIndex).shape(); // {1, NUM_CLASSES} + DataType probabilityDataType = tflite.getOutputTensor(probabilityTensorIndex).dataType(); + + // Creates the input tensor. + inputImageBuffer = new TensorImage(imageDataType); + + // Creates the output tensor and its processor. + outputProbabilityBuffer = TensorBuffer.createFixedSize(probabilityShape, probabilityDataType); + + // Creates the post processor for the output probability. + probabilityProcessor = new TensorProcessor.Builder().add(getPostprocessNormalizeOp()).build(); + + Log.d(TAG, "Created a Tensorflow Lite Image Classifier."); + } + catch (Exception ex){ + throw new Exception("Unable init classifier", ex); + } + } + + /** Runs inference and returns the classification results. */ + public List recognizeImage(final Bitmap bitmap, int sensorOrientation) { + List recognitonList = new ArrayList<>(); + + try { + // Logs this method so that it can be analyzed with systrace. + Trace.beginSection("recognizeImage"); + + Trace.beginSection("loadImage"); + long startTimeForLoadImage = SystemClock.uptimeMillis(); + inputImageBuffer = loadImage(bitmap, sensorOrientation); + long endTimeForLoadImage = SystemClock.uptimeMillis(); + Trace.endSection(); + Log.v(TAG, "Time to load the image: " + (endTimeForLoadImage - startTimeForLoadImage)); + + // Runs the inference call. + Trace.beginSection("runInference"); + long startTimeForReference = SystemClock.uptimeMillis(); + tflite.run(inputImageBuffer.getBuffer(), outputProbabilityBuffer.getBuffer().rewind()); + long endTimeForReference = SystemClock.uptimeMillis(); + Trace.endSection(); + Log.v(TAG, "Time to run model inference: " + (endTimeForReference - startTimeForReference)); + + // Gets the map of label and probability. + Map labeledProbability = + new TensorLabel(labels, probabilityProcessor.process(outputProbabilityBuffer)) + .getMapWithFloatValue(); + Trace.endSection(); + + // Gets top-k results. + recognitonList = getTopKProbability(labeledProbability); + } catch (Exception ex) { + Log.e(TAG, String.format("Error in recognizeImage. %s", ex.toString())); + } + + return recognitonList; + } + + /** Closes the interpreter and model to release resources. */ + public void close() { + if (tflite != null) { + tflite.close(); + tflite = null; + } + if (gpuDelegate != null) { + gpuDelegate.close(); + gpuDelegate = null; + } + if (nnApiDelegate != null) { + nnApiDelegate.close(); + nnApiDelegate = null; + } + } + + /** Get the image size along the x axis. */ + public int getImageSizeX() { + return imageSizeX; + } + + /** Get the image size along the y axis. */ + public int getImageSizeY() { + return imageSizeY; + } + + /** Loads input image, and applies preprocessing. */ + private TensorImage loadImage(final Bitmap bitmap, int sensorOrientation) { + // Loads bitmap into a TensorImage. + inputImageBuffer.load(bitmap); + + // Creates processor for the TensorImage. + int cropSize = min(bitmap.getWidth(), bitmap.getHeight()); + int numRotation = sensorOrientation / 90; + // TODO(b/143564309): Fuse ops inside ImageProcessor. + ImageProcessor imageProcessor = + new ImageProcessor.Builder() + .add(new ResizeWithCropOrPadOp(cropSize, cropSize)) + .add(new ResizeOp(imageSizeX, imageSizeY, ResizeOp.ResizeMethod.NEAREST_NEIGHBOR)) + .add(new Rot90Op(numRotation)) + .add(getPreprocessNormalizeOp()) + .build(); + return imageProcessor.process(inputImageBuffer); + } + + /** Gets the top-k results. */ + private static List getTopKProbability(Map labelProb) { + // Find the best classifications. + PriorityQueue pq = + new PriorityQueue<>( + MAX_RESULTS, + new Comparator() { + @Override + public int compare(Recognition lhs, Recognition rhs) { + // Intentionally reversed to put high confidence at the head of the queue. + return Float.compare(rhs.getConfidence(), lhs.getConfidence()); + } + }); + + for (Map.Entry entry : labelProb.entrySet()) { + pq.add(new Recognition("" + entry.getKey(), entry.getKey(), entry.getValue(), null)); + } + + final ArrayList recognitions = new ArrayList<>(); + int recognitionsSize = min(pq.size(), MAX_RESULTS); + for (int i = 0; i < recognitionsSize; ++i) { + recognitions.add(pq.poll()); + } + return recognitions; + } + + + protected String getModelPath(){ + return modelPath; + } + + + protected String getLabelPath(){ + return labelPath; + } + + /** Gets the TensorOperator to nomalize the input image in preprocessing. */ + protected abstract TensorOperator getPreprocessNormalizeOp(); + + /** + * Gets the TensorOperator to dequantize the output probability in post processing. + * + *

For quantized model, we need de-quantize the prediction with NormalizeOp (as they are all + * essentially linear transformation). For float model, de-quantize is not required. But to + * uniform the API, de-quantize is added to float model too. Mean and std are set to 0.0f and + * 1.0f, respectively. + */ + protected abstract TensorOperator getPostprocessNormalizeOp(); +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFLiteimgmethod/classification/ClassifierFloatEfficientNet.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFLiteimgmethod/classification/ClassifierFloatEfficientNet.java new file mode 100644 index 000000000000..6f7eabc44a23 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFLiteimgmethod/classification/ClassifierFloatEfficientNet.java @@ -0,0 +1,42 @@ +package org.firstinspires.ftc.teamcode.tfrec.classification; + +import android.app.Activity; + +import org.firstinspires.ftc.robotcore.external.Telemetry; +import org.tensorflow.lite.support.common.TensorOperator; +import org.tensorflow.lite.support.common.ops.NormalizeOp; + +import java.io.IOException; + +public class ClassifierFloatEfficientNet extends Classifier { + private static final float IMAGE_MEAN = 127.0f; + private static final float IMAGE_STD = 128.0f; + + /** + * Float model does not need dequantization in the post-processing. Setting mean and std as 0.0f + * and 1.0f, repectively, to bypass the normalization. + */ + private static final float PROBABILITY_MEAN = 0.0f; + + private static final float PROBABILITY_STD = 1.0f; + + /** + * Initializes a {@code ClassifierFloatMobileNet}. + * + * @param activity + */ + public ClassifierFloatEfficientNet(Activity activity, Device device, int numThreads, String modelFileName, String labelFileName, Telemetry t) + throws Exception { + super(activity, device, numThreads, modelFileName, labelFileName, t); + } + + @Override + protected TensorOperator getPreprocessNormalizeOp() { + return new NormalizeOp(IMAGE_MEAN, IMAGE_STD); + } + + @Override + protected TensorOperator getPostprocessNormalizeOp() { + return new NormalizeOp(PROBABILITY_MEAN, PROBABILITY_STD); + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFLiteimgmethod/classification/ClassifierFloatMobileNet.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFLiteimgmethod/classification/ClassifierFloatMobileNet.java new file mode 100644 index 000000000000..b886aca30f6f --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFLiteimgmethod/classification/ClassifierFloatMobileNet.java @@ -0,0 +1,44 @@ +package org.firstinspires.ftc.teamcode.tfrec.classification; + +import android.app.Activity; + +import org.firstinspires.ftc.robotcore.external.Telemetry; +import org.tensorflow.lite.support.common.TensorOperator; +import org.tensorflow.lite.support.common.ops.NormalizeOp; + +import java.io.IOException; + +public class ClassifierFloatMobileNet extends Classifier { + /** Float MobileNet requires additional normalization of the used input. */ + private static final float IMAGE_MEAN = 127.5f; + + private static final float IMAGE_STD = 127.5f; + + /** + * Float model does not need dequantization in the post-processing. Setting mean and std as 0.0f + * and 1.0f, repectively, to bypass the normalization. + */ + private static final float PROBABILITY_MEAN = 0.0f; + + private static final float PROBABILITY_STD = 1.0f; + + /** + * Initializes a {@code ClassifierFloatMobileNet}. + * + * @param activity + */ + public ClassifierFloatMobileNet(Activity activity, Device device, int numThreads, String modelFileName, String labelFileName, Telemetry t) + throws Exception { + super(activity, device, numThreads, modelFileName, labelFileName, t); + } + + @Override + protected TensorOperator getPreprocessNormalizeOp() { + return new NormalizeOp(IMAGE_MEAN, IMAGE_STD); + } + + @Override + protected TensorOperator getPostprocessNormalizeOp() { + return new NormalizeOp(PROBABILITY_MEAN, PROBABILITY_STD); + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFLiteimgmethod/classification/ClassifierQuantizedEfficientNet.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFLiteimgmethod/classification/ClassifierQuantizedEfficientNet.java new file mode 100644 index 000000000000..1c01b92cc92b --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFLiteimgmethod/classification/ClassifierQuantizedEfficientNet.java @@ -0,0 +1,45 @@ +package org.firstinspires.ftc.teamcode.tfrec.classification; + +import android.app.Activity; + +import org.firstinspires.ftc.robotcore.external.Telemetry; +import org.tensorflow.lite.support.common.TensorOperator; +import org.tensorflow.lite.support.common.ops.NormalizeOp; + +import java.io.IOException; + +public class ClassifierQuantizedEfficientNet extends Classifier { + /** + * The quantized model does not require normalization, thus set mean as 0.0f, and std as 1.0f to + * bypass the normalization. + */ + private static final float IMAGE_MEAN = 0.0f; + + private static final float IMAGE_STD = 1.0f; + + /** Quantized MobileNet requires additional dequantization to the output probability. */ + private static final float PROBABILITY_MEAN = 0.0f; + + private static final float PROBABILITY_STD = 255.0f; + + /** + * Initializes a {@code ClassifierQuantizedMobileNet}. + * + * @param activity + */ + public ClassifierQuantizedEfficientNet(Activity activity, Device device, int numThreads, String modelFileName, String labelFileName, Telemetry t) + throws Exception { + super(activity, device, numThreads, modelFileName, labelFileName, t); + } + + + @Override + protected TensorOperator getPreprocessNormalizeOp() { + return new NormalizeOp(IMAGE_MEAN, IMAGE_STD); + } + + @Override + protected TensorOperator getPostprocessNormalizeOp() { + return new NormalizeOp(PROBABILITY_MEAN, PROBABILITY_STD); + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFLiteimgmethod/classification/ClassifierQuantizedMobileNet.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFLiteimgmethod/classification/ClassifierQuantizedMobileNet.java new file mode 100644 index 000000000000..f643b8d710f3 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFLiteimgmethod/classification/ClassifierQuantizedMobileNet.java @@ -0,0 +1,45 @@ +package org.firstinspires.ftc.teamcode.tfrec.classification; + +import android.app.Activity; + +import org.firstinspires.ftc.robotcore.external.Telemetry; +import org.tensorflow.lite.support.common.TensorOperator; +import org.tensorflow.lite.support.common.ops.NormalizeOp; + +import java.io.IOException; + +public class ClassifierQuantizedMobileNet extends Classifier { + /** + * The quantized model does not require normalization, thus set mean as 0.0f, and std as 1.0f to + * bypass the normalization. + */ + private static final float IMAGE_MEAN = 0.0f; + + private static final float IMAGE_STD = 1.0f; + + /** Quantized MobileNet requires additional dequantization to the output probability. */ + private static final float PROBABILITY_MEAN = 0.0f; + + private static final float PROBABILITY_STD = 255.0f; + + /** + * Initializes a {@code ClassifierQuantizedMobileNet}. + * + * @param activity + */ + public ClassifierQuantizedMobileNet(Activity activity, Device device, int numThreads, String modelFileName, String labelFileName, Telemetry t) + throws Exception { + super(activity, device, numThreads, modelFileName, labelFileName, t); + } + + + @Override + protected TensorOperator getPreprocessNormalizeOp() { + return new NormalizeOp(IMAGE_MEAN, IMAGE_STD); + } + + @Override + protected TensorOperator getPostprocessNormalizeOp() { + return new NormalizeOp(PROBABILITY_MEAN, PROBABILITY_STD); + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFLiteimgmethod/utils/BorderedText.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFLiteimgmethod/utils/BorderedText.java new file mode 100644 index 000000000000..c63e2c582ab4 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFLiteimgmethod/utils/BorderedText.java @@ -0,0 +1,98 @@ +package org.firstinspires.ftc.teamcode.tfrec.utils; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Typeface; + +import java.util.Vector; + +public class BorderedText { + private final Paint interiorPaint; + private final Paint exteriorPaint; + + private final float textSize; + + /** + * Creates a left-aligned bordered text object with a white interior, and a black exterior with + * the specified text size. + * + * @param textSize text size in pixels + */ + public BorderedText(final float textSize) { + this(Color.WHITE, Color.BLACK, textSize); + } + + /** + * Create a bordered text object with the specified interior and exterior colors, text size and + * alignment. + * + * @param interiorColor the interior text color + * @param exteriorColor the exterior text color + * @param textSize text size in pixels + */ + public BorderedText(final int interiorColor, final int exteriorColor, final float textSize) { + interiorPaint = new Paint(); + interiorPaint.setTextSize(textSize); + interiorPaint.setColor(interiorColor); + interiorPaint.setStyle(Paint.Style.FILL); + interiorPaint.setAntiAlias(false); + interiorPaint.setAlpha(255); + + exteriorPaint = new Paint(); + exteriorPaint.setTextSize(textSize); + exteriorPaint.setColor(exteriorColor); + exteriorPaint.setStyle(Paint.Style.FILL_AND_STROKE); + exteriorPaint.setStrokeWidth(textSize / 8); + exteriorPaint.setAntiAlias(false); + exteriorPaint.setAlpha(255); + + this.textSize = textSize; + } + + public void setTypeface(Typeface typeface) { + interiorPaint.setTypeface(typeface); + exteriorPaint.setTypeface(typeface); + } + + public void drawText(final Canvas canvas, final float posX, final float posY, final String text) { + canvas.drawText(text, posX, posY, exteriorPaint); + canvas.drawText(text, posX, posY, interiorPaint); + } + + public void drawLines(Canvas canvas, final float posX, final float posY, Vector lines) { + int lineNum = 0; + for (final String line : lines) { + drawText(canvas, posX, posY - getTextSize() * (lines.size() - lineNum - 1), line); + ++lineNum; + } + } + + public void setInteriorColor(final int color) { + interiorPaint.setColor(color); + } + + public void setExteriorColor(final int color) { + exteriorPaint.setColor(color); + } + + public float getTextSize() { + return textSize; + } + + public void setAlpha(final int alpha) { + interiorPaint.setAlpha(alpha); + exteriorPaint.setAlpha(alpha); + } + + public void getTextBounds( + final String line, final int index, final int count, final Rect lineBounds) { + interiorPaint.getTextBounds(line, index, count, lineBounds); + } + + public void setTextAlign(final Paint.Align align) { + interiorPaint.setTextAlign(align); + exteriorPaint.setTextAlign(align); + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFLiteimgmethod/utils/ImageUtils.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFLiteimgmethod/utils/ImageUtils.java new file mode 100644 index 000000000000..6eba6aafe7ab --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFLiteimgmethod/utils/ImageUtils.java @@ -0,0 +1,135 @@ +package org.firstinspires.ftc.teamcode.tfrec.utils; + +import android.graphics.Bitmap; +import android.os.Environment; + +import java.io.File; +import java.io.FileOutputStream; + +public class ImageUtils { + // This value is 2 ^ 18 - 1, and is used to clamp the RGB values before their ranges + // are normalized to eight bits. + static final int kMaxChannelValue = 262143; + + + /** + * Utility method to compute the allocated size in bytes of a YUV420SP image of the given + * dimensions. + */ + public static int getYUVByteSize(final int width, final int height) { + // The luminance plane requires 1 byte per pixel. + final int ySize = width * height; + + // The UV plane works on 2x2 blocks, so dimensions with odd size must be rounded up. + // Each 2x2 block takes 2 bytes to encode, one each for U and V. + final int uvSize = ((width + 1) / 2) * ((height + 1) / 2) * 2; + + return ySize + uvSize; + } + + /** + * Saves a Bitmap object to disk for analysis. + * + * @param bitmap The bitmap to save. + */ + public static void saveBitmap(final Bitmap bitmap) { + saveBitmap(bitmap, "preview.png"); + } + + /** + * Saves a Bitmap object to disk for analysis. + * + * @param bitmap The bitmap to save. + * @param filename The location to save the bitmap to. + */ + public static void saveBitmap(final Bitmap bitmap, final String filename) { + final String root = + Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "tensorflow"; +// LOGGER.i("Saving %dx%d bitmap to %s.", bitmap.getWidth(), bitmap.getHeight(), root); + final File myDir = new File(root); + + if (!myDir.mkdirs()) { +// LOGGER.i("Make dir failed"); + } + + final String fname = filename; + final File file = new File(myDir, fname); + if (file.exists()) { + file.delete(); + } + try { + final FileOutputStream out = new FileOutputStream(file); + bitmap.compress(Bitmap.CompressFormat.PNG, 99, out); + out.flush(); + out.close(); + } catch (final Exception e) { +// LOGGER.e(e, "Exception!"); + } + } + + public static void convertYUV420SPToARGB8888(byte[] input, int width, int height, int[] output) { + final int frameSize = width * height; + for (int j = 0, yp = 0; j < height; j++) { + int uvp = frameSize + (j >> 1) * width; + int u = 0; + int v = 0; + + for (int i = 0; i < width; i++, yp++) { + int y = 0xff & input[yp]; + if ((i & 1) == 0) { + v = 0xff & input[uvp++]; + u = 0xff & input[uvp++]; + } + + output[yp] = YUV2RGB(y, u, v); + } + } + } + + private static int YUV2RGB(int y, int u, int v) { + // Adjust and check YUV values + y = (y - 16) < 0 ? 0 : (y - 16); + u -= 128; + v -= 128; + + // This is the floating point equivalent. We do the conversion in integer + // because some Android devices do not have floating point in hardware. + // nR = (int)(1.164 * nY + 2.018 * nU); + // nG = (int)(1.164 * nY - 0.813 * nV - 0.391 * nU); + // nB = (int)(1.164 * nY + 1.596 * nV); + int y1192 = 1192 * y; + int r = (y1192 + 1634 * v); + int g = (y1192 - 833 * v - 400 * u); + int b = (y1192 + 2066 * u); + + // Clipping RGB values to be inside boundaries [ 0 , kMaxChannelValue ] + r = r > kMaxChannelValue ? kMaxChannelValue : (r < 0 ? 0 : r); + g = g > kMaxChannelValue ? kMaxChannelValue : (g < 0 ? 0 : g); + b = b > kMaxChannelValue ? kMaxChannelValue : (b < 0 ? 0 : b); + + return 0xff000000 | ((r << 6) & 0xff0000) | ((g >> 2) & 0xff00) | ((b >> 10) & 0xff); + } + + public static void convertYUV420ToARGB8888( + byte[] yData, + byte[] uData, + byte[] vData, + int width, + int height, + int yRowStride, + int uvRowStride, + int uvPixelStride, + int[] out) { + int yp = 0; + for (int j = 0; j < height; j++) { + int pY = yRowStride * j; + int pUV = uvRowStride * (j >> 1); + + for (int i = 0; i < width; i++) { + int uv_offset = pUV + (i >> 1) * uvPixelStride; + + out[yp++] = YUV2RGB(0xff & yData[pY + i], 0xff & uData[uv_offset], 0xff & vData[uv_offset]); + } + } + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFLiteimgmethod/views/AutoFitTextureView.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFLiteimgmethod/views/AutoFitTextureView.java new file mode 100644 index 000000000000..718a4520e9e7 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFLiteimgmethod/views/AutoFitTextureView.java @@ -0,0 +1,56 @@ +package org.firstinspires.ftc.teamcode.tfrec.views; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.TextureView; +import android.view.View; + +public class AutoFitTextureView extends TextureView { + private int ratioWidth = 0; + private int ratioHeight = 0; + + public AutoFitTextureView(final Context context) { + this(context, null); + } + + public AutoFitTextureView(final Context context, final AttributeSet attrs) { + this(context, attrs, 0); + } + + public AutoFitTextureView(final Context context, final AttributeSet attrs, final int defStyle) { + super(context, attrs, defStyle); + } + + /** + * Sets the aspect ratio for this view. The size of the view will be measured based on the ratio + * calculated from the parameters. Note that the actual sizes of parameters don't matter, that is, + * calling setAspectRatio(2, 3) and setAspectRatio(4, 6) make the same result. + * + * @param width Relative horizontal size + * @param height Relative vertical size + */ + public void setAspectRatio(final int width, final int height) { + if (width < 0 || height < 0) { + throw new IllegalArgumentException("Size cannot be negative."); + } + ratioWidth = width; + ratioHeight = height; + requestLayout(); + } + + @Override + protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + final int width = View.MeasureSpec.getSize(widthMeasureSpec); + final int height = View.MeasureSpec.getSize(heightMeasureSpec); + if (0 == ratioWidth || 0 == ratioHeight) { + setMeasuredDimension(width, height); + } else { + if (width < height * ratioWidth / ratioHeight) { + setMeasuredDimension(width, width * ratioHeight / ratioWidth); + } else { + setMeasuredDimension(height * ratioWidth / ratioHeight, height); + } + } + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFLiteimgmethod/views/CameraConnectionFragment.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFLiteimgmethod/views/CameraConnectionFragment.java new file mode 100644 index 000000000000..1f574e9d28e7 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFLiteimgmethod/views/CameraConnectionFragment.java @@ -0,0 +1,558 @@ +package org.firstinspires.ftc.teamcode.tfrec.views; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.app.Fragment; +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.Configuration; +import android.graphics.ImageFormat; +import android.graphics.Matrix; +import android.graphics.RectF; +import android.graphics.SurfaceTexture; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.CaptureResult; +import android.hardware.camera2.TotalCaptureResult; +import android.hardware.camera2.params.StreamConfigurationMap; +import android.media.ImageReader; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.text.TextUtils; +import android.util.Size; +import android.util.SparseIntArray; +import android.view.LayoutInflater; +import android.view.Surface; +import android.view.TextureView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import org.checkerframework.checker.index.qual.LTEqLengthOf; +import org.firstinspires.ftc.robotcore.external.Telemetry; +import org.firstinspires.ftc.teamcode.R; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +@SuppressLint("ValidFragment") +public class CameraConnectionFragment extends Fragment { + private Telemetry telemetry; + + /** + * The camera preview size will be chosen to be the smallest frame by pixel size capable of + * containing a DESIRED_SIZE x DESIRED_SIZE square. + */ + private static final int MINIMUM_PREVIEW_SIZE = 320; + + /** Conversion from screen rotation to JPEG orientation. */ + private static final SparseIntArray ORIENTATIONS = new SparseIntArray(); + + private static final String FRAGMENT_DIALOG = "dialog"; + + static { + ORIENTATIONS.append(Surface.ROTATION_0, 90); + ORIENTATIONS.append(Surface.ROTATION_90, 0); + ORIENTATIONS.append(Surface.ROTATION_180, 270); + ORIENTATIONS.append(Surface.ROTATION_270, 180); + } + + /** A {@link Semaphore} to prevent the app from exiting before closing the camera. */ + private final Semaphore cameraOpenCloseLock = new Semaphore(1); + /** A {@link ImageReader.OnImageAvailableListener} to receive frames as they are available. */ + private final ImageReader.OnImageAvailableListener imageListener; + /** The input size in pixels desired by TensorFlow (width and height of a square bitmap). */ + private final Size inputSize; + /** The layout identifier to inflate for this Fragment. */ + private final int layout; + + private final ConnectionCallback cameraConnectionCallback; + private final CameraCaptureSession.CaptureCallback captureCallback = + new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureProgressed( + final CameraCaptureSession session, + final CaptureRequest request, + final CaptureResult partialResult) {} + + @Override + public void onCaptureCompleted( + final CameraCaptureSession session, + final CaptureRequest request, + final TotalCaptureResult result) {} + }; + /** ID of the current {@link CameraDevice}. */ + private String cameraId; + /** An {@link AutoFitTextureView} for camera preview. */ + private AutoFitTextureView textureView; + /** A {@link CameraCaptureSession } for camera preview. */ + private CameraCaptureSession captureSession; + /** A reference to the opened {@link CameraDevice}. */ + private CameraDevice cameraDevice; + /** The rotation in degrees of the camera sensor from the display. */ + private Integer sensorOrientation; + /** The {@link Size} of camera preview. */ + private Size previewSize; + /** An additional thread for running tasks that shouldn't block the UI. */ + private HandlerThread backgroundThread; + /** A {@link Handler} for running tasks in the background. */ + private Handler backgroundHandler; + /** + * {@link TextureView.SurfaceTextureListener} handles several lifecycle events on a {@link + * TextureView}. + */ + private final TextureView.SurfaceTextureListener surfaceTextureListener = + new TextureView.SurfaceTextureListener() { + @Override + public void onSurfaceTextureAvailable( + final SurfaceTexture texture, final int width, final int height) { + openCamera(width, height); + } + + @Override + public void onSurfaceTextureSizeChanged( + final SurfaceTexture texture, final int width, final int height) { + configureTransform(width, height); + } + + @Override + public boolean onSurfaceTextureDestroyed(final SurfaceTexture texture) { + return true; + } + + @Override + public void onSurfaceTextureUpdated(final SurfaceTexture texture) {} + }; + /** An {@link ImageReader} that handles preview frame capture. */ + private ImageReader previewReader; + /** {@link CaptureRequest.Builder} for the camera preview */ + private CaptureRequest.Builder previewRequestBuilder; + /** {@link CaptureRequest} generated by {@link #previewRequestBuilder} */ + private CaptureRequest previewRequest; + /** {@link CameraDevice.StateCallback} is called when {@link CameraDevice} changes its state. */ + private final CameraDevice.StateCallback stateCallback = + new CameraDevice.StateCallback() { + @Override + public void onOpened(final CameraDevice cd) { + // This method is called when the camera is opened. We start camera preview here. + cameraOpenCloseLock.release(); + cameraDevice = cd; + createCameraPreviewSession(); + } + + @Override + public void onDisconnected(final CameraDevice cd) { + cameraOpenCloseLock.release(); + cd.close(); + cameraDevice = null; + } + + @Override + public void onError(final CameraDevice cd, final int error) { + cameraOpenCloseLock.release(); + cd.close(); + cameraDevice = null; + final Activity activity = getActivity(); + if (null != activity) { + activity.finish(); + } + } + }; + + @SuppressLint("ValidFragment") + private CameraConnectionFragment( + final ConnectionCallback connectionCallback, + final ImageReader.OnImageAvailableListener imageListener, + final int layout, + final Size inputSize, Telemetry t) { + this.cameraConnectionCallback = connectionCallback; + this.imageListener = imageListener; + this.layout = layout; + this.inputSize = inputSize; + this.telemetry = t; + } + + /** + * Given {@code choices} of {@code Size}s supported by a camera, chooses the smallest one whose + * width and height are at least as large as the minimum of both, or an exact match if possible. + * + * @param choices The list of sizes that the camera supports for the intended output class + * @param width The minimum desired width + * @param height The minimum desired height + * @return The optimal {@code Size}, or an arbitrary one if none were big enough + */ + protected static Size chooseOptimalSize(final Size[] choices, final int width, final int height, Telemetry telemetry) { + final int minSize = Math.max(Math.min(width, height), MINIMUM_PREVIEW_SIZE); + final Size desiredSize = new Size(width, height); + + // Collect the supported resolutions that are at least as big as the preview Surface + boolean exactSizeFound = false; + final List bigEnough = new ArrayList(); + final List tooSmall = new ArrayList(); + for (final Size option : choices) { + if (option.equals(desiredSize)) { + // Set the size but don't return yet so that remaining sizes will still be logged. + exactSizeFound = true; + } + + if (option.getHeight() >= minSize && option.getWidth() >= minSize) { + bigEnough.add(option); + } else { + tooSmall.add(option); + } + } + + telemetry.addData("Info","Desired size: " + desiredSize + ", min size: " + minSize + "x" + minSize); + telemetry.addData("Info","Valid preview sizes: [" + TextUtils.join(", ", bigEnough) + "]"); + telemetry.addData("Info","Rejected preview sizes: [" + TextUtils.join(", ", tooSmall) + "]"); + + if (exactSizeFound) { + telemetry.addData("Info", "Exact size match found."); + return desiredSize; + } + + // Pick the smallest of those, assuming we found any + if (bigEnough.size() > 0) { + final Size chosenSize = Collections.min(bigEnough, new CompareSizesByArea()); + telemetry.addData("Info", "Chosen size: " + chosenSize.getWidth() + "x" + chosenSize.getHeight()); + return chosenSize; + } else { + telemetry.addData("Error", "Couldn't find any suitable preview size"); + return choices[0]; + } + } + + public static CameraConnectionFragment newInstance( + final ConnectionCallback callback, + final ImageReader.OnImageAvailableListener imageListener, + final int layout, + final Size inputSize, Telemetry t) { + return new CameraConnectionFragment(callback, imageListener, layout, inputSize, t); + } + + /** + * Shows a {@link Toast} on the UI thread. + * + * @param text The message to show + */ + private void showToast(final String text) { + final Activity activity = getActivity(); + if (activity != null) { + activity.runOnUiThread( + new Runnable() { + @Override + public void run() { + Toast.makeText(activity, text, Toast.LENGTH_SHORT).show(); + } + }); + } + } + + @Override + public View onCreateView( + final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { + return inflater.inflate(layout, container, false); + } + + @Override + public void onViewCreated(final View view, final Bundle savedInstanceState) { + textureView = (AutoFitTextureView) view.findViewById(R.id.texture); + } + + @Override + public void onActivityCreated(final Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + } + + @Override + public void onResume() { + super.onResume(); + startBackgroundThread(); + + // When the screen is turned off and turned back on, the SurfaceTexture is already + // available, and "onSurfaceTextureAvailable" will not be called. In that case, we can open + // a camera and start preview from here (otherwise, we wait until the surface is ready in + // the SurfaceTextureListener). + if (textureView.isAvailable()) { + openCamera(textureView.getWidth(), textureView.getHeight()); + } else { + textureView.setSurfaceTextureListener(surfaceTextureListener); + } + } + + @Override + public void onPause() { + closeCamera(); + stopBackgroundThread(); + super.onPause(); + } + + public void setCamera(String cameraId) { + this.cameraId = cameraId; + } + + /** Sets up member variables related to camera. */ + private void setUpCameraOutputs() { + final Activity activity = getActivity(); + final CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); + try { + final CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId); + + final StreamConfigurationMap map = + characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); + + sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); + + // Danger, W.R.! Attempting to use too large a preview size could exceed the camera + // bus' bandwidth limitation, resulting in gorgeous previews but the storage of + // garbage capture data. + previewSize = + chooseOptimalSize( + map.getOutputSizes(SurfaceTexture.class), + inputSize.getWidth(), + inputSize.getHeight(), telemetry); + + // We fit the aspect ratio of TextureView to the size of preview we picked. + final int orientation = getResources().getConfiguration().orientation; + if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + textureView.setAspectRatio(previewSize.getWidth(), previewSize.getHeight()); + } else { + textureView.setAspectRatio(previewSize.getHeight(), previewSize.getWidth()); + } + } catch (final CameraAccessException e) { + telemetry.addData("Error", "Exception!", e.getMessage()); + } catch (final NullPointerException e) { + // Currently an NPE is thrown when the Camera2API is used but not supported on the + // device this code runs. + ErrorDialog.newInstance(getString(R.string.tfe_ic_camera_error)) + .show(getChildFragmentManager(), FRAGMENT_DIALOG); + throw new IllegalStateException(getString(R.string.tfe_ic_camera_error)); + } + + cameraConnectionCallback.onPreviewSizeChosen(previewSize, sensorOrientation); + } + + /** Opens the camera specified by {@link CameraConnectionFragment#cameraId}. */ + @SuppressLint("MissingPermission") + private void openCamera(final int width, final int height) { + setUpCameraOutputs(); + configureTransform(width, height); + final Activity activity = getActivity(); + final CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); + try { + if (!cameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) { + throw new RuntimeException("Time out waiting to lock camera opening."); + } + manager.openCamera(cameraId, stateCallback, backgroundHandler); + } catch (final CameraAccessException e) { + telemetry.addData("Error", "Exception!", e.getMessage()); + } catch (final InterruptedException e) { + throw new RuntimeException("Interrupted while trying to lock camera opening.", e); + } + } + + /** Closes the current {@link CameraDevice}. */ + private void closeCamera() { + try { + cameraOpenCloseLock.acquire(); + if (null != captureSession) { + captureSession.close(); + captureSession = null; + } + if (null != cameraDevice) { + cameraDevice.close(); + cameraDevice = null; + } + if (null != previewReader) { + previewReader.close(); + previewReader = null; + } + } catch (final InterruptedException e) { + throw new RuntimeException("Interrupted while trying to lock camera closing.", e); + } finally { + cameraOpenCloseLock.release(); + } + } + + /** Starts a background thread and its {@link Handler}. */ + private void startBackgroundThread() { + backgroundThread = new HandlerThread("ImageListener"); + backgroundThread.start(); + backgroundHandler = new Handler(backgroundThread.getLooper()); + } + + /** Stops the background thread and its {@link Handler}. */ + private void stopBackgroundThread() { + backgroundThread.quitSafely(); + try { + backgroundThread.join(); + backgroundThread = null; + backgroundHandler = null; + } catch (final InterruptedException e) { + telemetry.addData("Error", e.getMessage()); + } + } + + /** Creates a new {@link CameraCaptureSession} for camera preview. */ + private void createCameraPreviewSession() { + try { + final SurfaceTexture texture = textureView.getSurfaceTexture(); + assert texture != null; + + // We configure the size of default buffer to be the size of camera preview we want. + texture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); + + // This is the output Surface we need to start preview. + final Surface surface = new Surface(texture); + + // We set up a CaptureRequest.Builder with the output Surface. + previewRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); + previewRequestBuilder.addTarget(surface); + + telemetry.addData("Info","Opening camera preview: " + previewSize.getWidth() + "x" + previewSize.getHeight()); + + // Create the reader for the preview frames. + previewReader = + ImageReader.newInstance( + previewSize.getWidth(), previewSize.getHeight(), ImageFormat.YUV_420_888, 2); + + previewReader.setOnImageAvailableListener(imageListener, backgroundHandler); + previewRequestBuilder.addTarget(previewReader.getSurface()); + + // Here, we create a CameraCaptureSession for camera preview. + cameraDevice.createCaptureSession( + Arrays.asList(surface, previewReader.getSurface()), + new CameraCaptureSession.StateCallback() { + + @Override + public void onConfigured(final CameraCaptureSession cameraCaptureSession) { + // The camera is already closed + if (null == cameraDevice) { + return; + } + + // When the session is ready, we start displaying the preview. + captureSession = cameraCaptureSession; + try { + // Auto focus should be continuous for camera preview. + previewRequestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, + CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); + // Flash is automatically enabled when necessary. + previewRequestBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); + + // Finally, we start displaying the camera preview. + previewRequest = previewRequestBuilder.build(); + captureSession.setRepeatingRequest( + previewRequest, captureCallback, backgroundHandler); + } catch (final CameraAccessException e) { + telemetry.addData("Error", e.getMessage()); + } + } + + @Override + public void onConfigureFailed(final CameraCaptureSession cameraCaptureSession) { + showToast("Failed"); + } + }, + null); + } catch (final CameraAccessException e) { + telemetry.addData("Error", e.getMessage()); + } + } + + /** + * Configures the necessary {@link Matrix} transformation to `mTextureView`. This method should be + * called after the camera preview size is determined in setUpCameraOutputs and also the size of + * `mTextureView` is fixed. + * + * @param viewWidth The width of `mTextureView` + * @param viewHeight The height of `mTextureView` + */ + private void configureTransform(final int viewWidth, final int viewHeight) { + final Activity activity = getActivity(); + if (null == textureView || null == previewSize || null == activity) { + return; + } + final int rotation = activity.getWindowManager().getDefaultDisplay().getRotation(); + final Matrix matrix = new Matrix(); + final RectF viewRect = new RectF(0, 0, viewWidth, viewHeight); + final RectF bufferRect = new RectF(0, 0, previewSize.getHeight(), previewSize.getWidth()); + final float centerX = viewRect.centerX(); + final float centerY = viewRect.centerY(); + if (Surface.ROTATION_90 == rotation || Surface.ROTATION_270 == rotation) { + bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY()); + matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL); + final float scale = + Math.max( + (float) viewHeight / previewSize.getHeight(), + (float) viewWidth / previewSize.getWidth()); + matrix.postScale(scale, scale, centerX, centerY); + matrix.postRotate(90 * (rotation - 2), centerX, centerY); + } else if (Surface.ROTATION_180 == rotation) { + matrix.postRotate(180, centerX, centerY); + } + textureView.setTransform(matrix); + } + + /** + * Callback for Activities to use to initialize their data once the selected preview size is + * known. + */ + public interface ConnectionCallback { + void onPreviewSizeChosen(Size size, int cameraRotation); + } + + /** Compares two {@code Size}s based on their areas. */ + static class CompareSizesByArea implements Comparator { + @Override + public int compare(final Size lhs, final Size rhs) { + // We cast here to ensure the multiplications won't overflow + return Long.signum( + (long) lhs.getWidth() * lhs.getHeight() - (long) rhs.getWidth() * rhs.getHeight()); + } + } + + /** Shows an error message dialog. */ + public static class ErrorDialog extends DialogFragment { + private static final String ARG_MESSAGE = "message"; + + public static ErrorDialog newInstance(final String message) { + final ErrorDialog dialog = new ErrorDialog(); + final Bundle args = new Bundle(); + args.putString(ARG_MESSAGE, message); + dialog.setArguments(args); + return dialog; + } + + @Override + public Dialog onCreateDialog(final Bundle savedInstanceState) { + final Activity activity = getActivity(); + return new AlertDialog.Builder(activity) + .setMessage(getArguments().getString(ARG_MESSAGE)) + .setPositiveButton( + android.R.string.ok, + new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialogInterface, final int i) { + activity.finish(); + } + }) + .create(); + } + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFLiteimgmethod/views/LegacyCameraConnectionFragment.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFLiteimgmethod/views/LegacyCameraConnectionFragment.java new file mode 100644 index 000000000000..217f5004c6b4 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFLiteimgmethod/views/LegacyCameraConnectionFragment.java @@ -0,0 +1,232 @@ +package org.firstinspires.ftc.teamcode.tfrec.views; + +import android.annotation.SuppressLint; +import android.app.Fragment; +import android.graphics.SurfaceTexture; +import android.hardware.Camera; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.util.Log; +import android.util.Size; +import android.util.SparseIntArray; +import android.view.LayoutInflater; +import android.view.Surface; +import android.view.TextureView; +import android.view.View; +import android.view.ViewGroup; + +import org.firstinspires.ftc.robotcore.external.Telemetry; +import org.firstinspires.ftc.teamcode.R; +import org.firstinspires.ftc.teamcode.tfrec.utils.ImageUtils; + +import java.io.IOException; +import java.util.List; + +@SuppressLint("ValidFragment") +public class LegacyCameraConnectionFragment extends Fragment { + /** + * Conversion from screen rotation to JPEG orientation. + */ + private static final SparseIntArray ORIENTATIONS = new SparseIntArray(); + private static final String TAG = "Fragment"; + + static { + ORIENTATIONS.append(Surface.ROTATION_0, 90); + ORIENTATIONS.append(Surface.ROTATION_90, 0); + ORIENTATIONS.append(Surface.ROTATION_180, 270); + ORIENTATIONS.append(Surface.ROTATION_270, 180); + } + + private Camera camera; + private Camera.PreviewCallback imageListener; + private Size desiredSize; + private Telemetry telemetry; + /** + * The layout identifier to inflate for this Fragment. + */ + private int layout; + /** + * An {@link AutoFitTextureView} for camera preview. + */ + private AutoFitTextureView textureView; + /** + * {@link TextureView.SurfaceTextureListener} handles several lifecycle events on a {@link + * TextureView}. + */ + private final TextureView.SurfaceTextureListener surfaceTextureListener = + new TextureView.SurfaceTextureListener() { + @Override + public void onSurfaceTextureAvailable( + final SurfaceTexture texture, final int width, final int height) { + Log.d("Listener", "Starting texture listener"); + //This variable is required for the fix for the Android media center issue when the camera fails to initialize + final TextureView.SurfaceTextureListener listener = this; + int index = getCameraId(); + Log.d("cameraIndex", String.valueOf(index)); + camera = Camera.open(index); + + camera.setErrorCallback(new Camera.ErrorCallback() { + @Override + public void onError(int i, Camera camera) { + Log.e("Listener", String.format("Camera error %d. Width: %d, Height: %d", i, width, height)); + //Fix for the Android media center issue when the camera fails to initialize + if (i == 100){ + Log.d("Listener", "Camera error is 100"); + stopCamera(); + Log.d("Listener", "Stopped and released the camera"); + listener.onSurfaceTextureAvailable(texture, width, height); + } + } + }); + + try { +// camera.stopPreview(); +// camera.release(); + Camera.Parameters parameters = camera.getParameters(); + List focusModes = parameters.getSupportedFocusModes(); + if (focusModes != null + && focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) { + parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE); + } + List cameraSizes = parameters.getSupportedPreviewSizes(); + Size[] sizes = new Size[cameraSizes.size()]; + int i = 0; + for (Camera.Size size : cameraSizes) { + sizes[i++] = new Size(size.width, size.height); + } + Size previewSize = + CameraConnectionFragment.chooseOptimalSize( + sizes, desiredSize.getWidth(), desiredSize.getHeight(), telemetry); + parameters.setPreviewSize(previewSize.getWidth(), previewSize.getHeight()); + camera.setDisplayOrientation(90); + camera.setParameters(parameters); + camera.setPreviewTexture(texture); + } catch (IOException exception) { + Log.e("Listener", "Error when starting texture listener", exception); + camera.release(); + } + + camera.setPreviewCallbackWithBuffer(imageListener); + Camera.Size s = camera.getParameters().getPreviewSize(); + camera.addCallbackBuffer(new byte[ImageUtils.getYUVByteSize(s.height, s.width)]); + + textureView.setAspectRatio(s.height, s.width); + + camera.startPreview(); + Log.d("Listener", "Camera preview started by texture listener"); + } + + @Override + public void onSurfaceTextureSizeChanged( + final SurfaceTexture texture, final int width, final int height) { + } + + @Override + public boolean onSurfaceTextureDestroyed(final SurfaceTexture texture) { + return true; + } + + @Override + public void onSurfaceTextureUpdated(final SurfaceTexture texture) { + } + }; + /** + * An additional thread for running tasks that shouldn't block the UI. + */ + private HandlerThread backgroundThread; + + @SuppressLint("ValidFragment") + public LegacyCameraConnectionFragment( + final Camera.PreviewCallback imageListener, final int layout, final Size desiredSize, Telemetry t) { + this.imageListener = imageListener; + this.layout = layout; + this.desiredSize = desiredSize; + telemetry = t; + } + + @Override + public View onCreateView( + final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { + return inflater.inflate(layout, container, false); + } + + @Override + public void onViewCreated(final View view, final Bundle savedInstanceState) { + textureView = (AutoFitTextureView) view.findViewById(R.id.texture); + } + + @Override + public void onActivityCreated(final Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + } + + @Override + public void onResume() { + super.onResume(); + startBackgroundThread(); + // When the screen is turned off and turned back on, the SurfaceTexture is already + // available, and "onSurfaceTextureAvailable" will not be called. In that case, we can open + // a camera and start preview from here (otherwise, we wait until the surface is ready in + // the SurfaceTextureListener). + Log.d(TAG, "Starting texture preview"); + if (textureView.isAvailable()) { + if (camera != null) { + camera.startPreview(); + Log.d(TAG, "Started preview"); + } + else{ + Log.e(TAG, "Failed to start preview. Camera unavailable"); + } + } else { + Log.d(TAG, "Texture not available yet. Starting the listener..."); + textureView.setSurfaceTextureListener(surfaceTextureListener); + } + } + + @Override + public void onPause() { + stopCamera(); + stopBackgroundThread(); + super.onPause(); + } + + /** + * Starts a background thread and its {@link Handler}. + */ + private void startBackgroundThread() { + backgroundThread = new HandlerThread("CameraBackground"); + backgroundThread.start(); + } + + /** + * Stops the background thread and its {@link Handler}. + */ + private void stopBackgroundThread() { + backgroundThread.quitSafely(); + try { + backgroundThread.join(); + backgroundThread = null; + } catch (final InterruptedException e) { + Log.e(TAG, "stopBackgroundThread", e); + } + } + + protected void stopCamera() { + if (camera != null) { + camera.stopPreview(); + camera.setPreviewCallback(null); + camera.release(); + camera = null; + } + } + + private int getCameraId() { + Camera.CameraInfo ci = new Camera.CameraInfo(); + for (int i = 0; i < Camera.getNumberOfCameras(); i++) { + Camera.getCameraInfo(i, ci); + if (ci.facing == Camera.CameraInfo.CAMERA_FACING_BACK) return i; + } + return -1; // No camera found + } +} \ No newline at end of file diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFODpseudosolutionDepreciated.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFODpseudosolutionDepreciated.java new file mode 100644 index 000000000000..a9b7dd254e02 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/TFODpseudosolutionDepreciated.java @@ -0,0 +1,175 @@ +/* Copyright (c) 2019 FIRST. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted (subject to the limitations in the disclaimer below) provided that + * the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * Neither the name of FIRST nor the names of its contributors may be used to endorse or + * promote products derived from this software without specific prior written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS + * LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.firstinspires.ftc.teamcode; + +import com.qualcomm.robotcore.eventloop.opmode.Disabled; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; +import com.qualcomm.robotcore.eventloop.opmode.TeleOp; +import java.util.List; +import org.firstinspires.ftc.robotcore.external.ClassFactory; +import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName; +import org.firstinspires.ftc.robotcore.external.navigation.VuforiaLocalizer; +import org.firstinspires.ftc.robotcore.external.tfod.TFObjectDetector; +import org.firstinspires.ftc.robotcore.external.tfod.Recognition; + +@TeleOp(name = "TensorFlow Object Detection Webcam", group = "Concept") +//@Disabled +public class ConceptTensorFlowObjectDetectionWebcam extends LinearOpMode { + + /* + * Specify the source for the Tensor Flow Model. + * If the TensorFlowLite object model is included in the Robot Controller App as an "asset", + * the OpMode must to load it using loadModelFromAsset(). However, if a team generated model + * has been downloaded to the Robot Controller's SD FLASH memory, it must to be loaded using loadModelFromFile() + * Here we assume it's an Asset. Also see method initTfod() below . + */ + private static final String TFOD_MODEL_ASSET = "PowerPlay.tflite"; + // private static final String TFOD_MODEL_FILE = "/sdcard/FIRST/tflitemodels/CustomTeamModel.tflite"; + + /* + the types of object (image singals) that should be detected + */ + private static final String[] LABELS = { + "1 Bolt", + "2 Bulb", + "3 Panel" + }; + + /* + * IMPORTANT: require 'parameters.vuforiaLicenseKey' + * A free Vuforia 'Development' license key, can be obtained at https://developer.vuforia.com/license-manager. + * + * Vuforia license keys are always 380 characters long, and look as if they contain mostly + * random data. As an example, here is a example of a fragment of a valid key: + * ... yIgIzTqZ4mWjk9wd3cZO9T1axEqzuhxoGlfOOI2dRzKS4T0hQ8kT ... + */ + private static final String VUFORIA_KEY = + " -- YOUR NEW VUFORIA KEY GOES HERE --- "; + + /** + * {@link #vuforia} is the variable we will use to store our instance of the Vuforia + * localization engine. + */ + private VuforiaLocalizer vuforia; + + /** + * {@link #tfod} is the variable we will use to store our instance of the TensorFlow Object + * Detection engine. + */ + private TFObjectDetector tfod; + + @Override + public void runOpMode() { + // TFOD process image feed from Vuforia, so init 1.vufora --> 2. TFOD + initVuforia(); + initTfod(); + + /** + * Activate TensorFlow Object Detection before we wait for the start command. + **/ + if (tfod != null) { + tfod.activate(); + + // The TFOD autoscales down the input images to a lower resolution => lower detection accuracy at long distances (> 55cm or 22"). + // If your target is at great distance, 1. increase the magnification(zoom) value or 2. Retrain model + // For best results, the "aspectRatio" argument should the value used to create the TFOD model(typically 16/9). + tfod.setZoom(1.0, 16.0/9.0); + } + + /** Wait for the game to begin */ + telemetry.addData(">", "Press Play to start op mode"); + telemetry.update(); + waitForStart(); + + //use if want camera live feed display + if (opModeIsActive()) { + while (opModeIsActive()) { + if (tfod != null) { + // getUpdatedRecognitions() will return null if no new information is available since + // the last time that call was made. + List updatedRecognitions = tfod.getUpdatedRecognitions(); + if (updatedRecognitions != null) { + telemetry.addData("# Objects Detected", updatedRecognitions.size()); + + // step through the list of recognitions and display image position/size information for each one + // Note: "Image number" refers to the randomized image orientation/number + for (Recognition recognition : updatedRecognitions) { + double col = (recognition.getLeft() + recognition.getRight()) / 2 ; + double row = (recognition.getTop() + recognition.getBottom()) / 2 ; + double width = Math.abs(recognition.getRight() - recognition.getLeft()) ; + double height = Math.abs(recognition.getTop() - recognition.getBottom()) ; + + telemetry.addData(""," "); + telemetry.addData("Image", "%s (%.0f %% Conf.)", recognition.getLabel(), recognition.getConfidence() * 100 ); + telemetry.addData("- Position (Row/Col)","%.0f / %.0f", row, col); + telemetry.addData("- Size (Width/Height)","%.0f / %.0f", width, height); + } + telemetry.update(); + } + } + } + } + } + + /** + * Initialize the Vuforia localization engine. + */ + private void initVuforia() { + /* + * Configure Vuforia by creating a Parameter object, and passing it to the Vuforia engine. + */ + VuforiaLocalizer.Parameters parameters = new VuforiaLocalizer.Parameters(); + + parameters.vuforiaLicenseKey = VUFORIA_KEY; + parameters.cameraName = hardwareMap.get(WebcamName.class, "Webcam 1"); + + // Instantiate the Vuforia engine + vuforia = ClassFactory.getInstance().createVuforia(parameters); + } + + /** + * Initialize the TensorFlow Object Detection engine. + */ + private void initTfod() { + int tfodMonitorViewId = hardwareMap.appContext.getResources().getIdentifier( + "tfodMonitorViewId", "id", hardwareMap.appContext.getPackageName()); + TFObjectDetector.Parameters tfodParameters = new TFObjectDetector.Parameters(tfodMonitorViewId); + tfodParameters.minResultConfidence = 0.75f; + tfodParameters.isModelTensorFlow2 = true; + tfodParameters.inputSize = 300; + tfod = ClassFactory.getInstance().createTFObjectDetector(tfodParameters, vuforia); + + // Use loadModelFromAsset() if the TF Model is built in as an asset by Android Studio + // Use loadModelFromFile() if you have downloaded a custom team model to the Robot Controller's FLASH. + tfod.loadModelFromAsset(TFOD_MODEL_ASSET, LABELS); + // tfod.loadModelFromFile(TFOD_MODEL_FILE, LABELS); + Telemetry.Item addData(java.lang.String "detected", java.lang.Object LABELS); + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/assets/readme.md b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/assets/readme.md new file mode 100644 index 000000000000..0515c0ce5a2d --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/assets/readme.md @@ -0,0 +1,18 @@ +## TeamCode Module + +A folder to be filled with google teachable machine data files + +Copy the files generated by the Teachable Machine into your assets folder. Each model is represented by 2 files: *.tflite file with the model *.txt file with labels. It does not matter what you call them since you will be referencing the files by name in your code. + +For an example of how to consume the detection, take a look at MHSRoboticEagles's TeamCode/skills/GenericDetector class. It is a wrapper of the tflite engine that we have in tfrec folder. This class is specific to the last season. Note that it is a runnable, to run on a separate thread. This is where we reference the file names of our model. Make sure to change them based on your model file names. You can use MHSRoboticEagles's TeamCode/OpMOdes/GenericRecognitionTest as an example opmode that consumes the GenericDetector. Both of these generic classes are meant to be a starting point from which you can build up the recognition logic for your specific needs. + +Note that the detector is initialized in the “init” phase of the Op mode. It usually takes 3-5 seconds for the tflite engine to “warm up”. The idea is that by the time you press Start, the detection results are available. + +## References +1) https://github.com/MHSRoboticEagles/FtcRobotController/tree/master/TeamCode +2) https://www.youtube.com/watch?v=aeMWWvteF2U + +## IMPORTANT NOTE +This custom opmode is directly imported from the MHSRoboticEagles directory. In other words, there could be compatibility issues. +MHSEagles uses the android mobile phone as controller to their robot. I am not sure if our Hub also run android the same way, thus we need to test the library out first. +However, as we all know, before we work on the fancy stuffs like Image Recognition, lets make the drivetain and pickup functions work first. diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/opmode/GenericRecognitionTest.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/opmode/GenericRecognitionTest.java new file mode 100644 index 000000000000..01bc8694ae56 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/opmode/GenericRecognitionTest.java @@ -0,0 +1,69 @@ +package org.firstinspires.ftc.teamcode.OpModes; + +import com.qualcomm.robotcore.eventloop.opmode.Disabled; +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; +import com.qualcomm.robotcore.eventloop.opmode.TeleOp; + +import org.firstinspires.ftc.teamcode.Project1Hardware.java; +import org.firstinspires.ftc.teamcode.skills.GenericDetector; +import org.firstinspires.ftc.teamcode.skills.RingDetector; + +// Control Hub ADB Terminal Command for Reference +// adb.exe connect 192.168.43.1:5555 + +@TeleOp(name = "Rec test Thread", group = "Robot15173") +@Disabled +public class GenericRecognitionTest extends LinearOpMode { + + // Declare OpMode members. + private GenericDetector rf = null; + private String result = ""; + private Project1Hardware bot = new Project1Hardware(); + + @Override + public void runOpMode() { + try { + try { + //initialize the bot + bot.init(this, this.hardwareMap, telemetry); + + //initialize the detector. It will run on its own thread continuously + rf = new GenericDetector(this.hardwareMap, this, telemetry); + Thread detectThread = new Thread(rf); + detectThread.start(); + telemetry.update(); + } catch (Exception ex) { + telemetry.addData("Error", String.format("Unable to initialize Detector. %s", ex.getMessage())); + telemetry.update(); + sleep(5000); + return; + } + + // Wait for the game to start (driver presses PLAY) + telemetry.update(); + waitForStart(); + + rf.stopDetection(); + + result = rf.getResult(); + + // run until the end of the match (driver presses STOP) + while (opModeIsActive()) { + //move the bot + double drive = gamepad1.left_stick_y; + bot.move(drive); + + //show recognition result + telemetry.addData("Detection result", result); + telemetry.update(); + } + } catch (Exception ex) { + telemetry.addData("Init Error", ex.getMessage()); + telemetry.update(); + } finally { + if (rf != null) { + rf.stopDetection(); + } + } + } +} diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/skills/GenericDetector.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/skills/GenericDetector.java new file mode 100644 index 000000000000..1544563c32b3 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/skills/GenericDetector.java @@ -0,0 +1,144 @@ + package org.firstinspires.ftc.teamcode.skills; + + import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; + import com.qualcomm.robotcore.hardware.HardwareMap; + import com.qualcomm.robotcore.util.ElapsedTime; + + import org.firstinspires.ftc.robotcore.external.Telemetry; + import org.firstinspires.ftc.teamcode.TFLiteimgmethod.GenericDetector; + import org.firstinspires.ftc.teamcode.TFLiteimgmethod.classification.Classifier; + + import java.util.ArrayList; + import java.util.List; + + public class GenericDetector implements Runnable{ + Telemetry telemetry; + private Detector tfDetector = null; + private HardwareMap hardwareMap; + + private boolean isRunning = true; + + + private String modelFileName = "rings_float.tflite";//"croppedRingRec.tflite"; + private String labelFileName = "labels.txt";//"croppedLabels.txt"; + private static Classifier.Model MODEl_TYPE = Classifier.Model.FLOAT_EFFICIENTNET; + private static final String LABEL_A = "None"; + private static final String LABEL_B = "Single"; + private static final String LABEL_C = "Quad"; + + private String result = LABEL_B; //just a default value. + + private LinearOpMode caller = null; + + + + public GenericDetector(HardwareMap hMap, LinearOpMode caller, Telemetry t) throws Exception { + hardwareMap = hMap; + telemetry = t; + initDetector(); + activateDetector(); + this.caller = caller; + + } + + public GenericDetector(HardwareMap hMap, LinearOpMode caller, Telemetry t, String model, String labels) throws Exception { + hardwareMap = hMap; + telemetry = t; + setModelFileName(model); + setLabelFileName(labels); + initDetector(); + activateDetector(); + this.caller = caller; + } + + + + + public void detectRingThread() { + + ElapsedTime runtime = new ElapsedTime(); + runtime.reset(); + while (isRunning && caller.opModeIsActive()) { + if (tfDetector != null) { + List results = tfDetector.getLastResults(); + if (results == null || results.size() == 0) { + telemetry.addData("Nada", "No results"); + } else { + for (Classifier.Recognition r : results) { + if (r.getConfidence() >= 0.8) { + telemetry.addData("PrintZone", r.getTitle()); + if (r.getTitle().contains(LABEL_C)) { + this.result = LABEL_C; + } + else if(r.getTitle().contains(LABEL_B)){ + this.result = LABEL_B; + } + else if(r.getTitle().contains(LABEL_A)){ + this.result = LABEL_A; + } + } + } + } + } + telemetry.update(); + } + + } + + + public void initDetector() throws Exception { + tfDetector = new Detector(MODEl_TYPE, getModelFileName(), getLabelFileName(), hardwareMap.appContext, telemetry); + } + + protected void activateDetector() throws Exception { + if (tfDetector != null) { + tfDetector.activate(); + } + telemetry.addData("Info", "TF Activated"); + } + + + public void stopDetection() { + stopThread(); + if (tfDetector != null) { + tfDetector.stopProcessing(); + } + tfDetector = null; + } + + public void stopThread() { + isRunning = false; + } + + @Override + public void run() { + while(isRunning) { + detectRingThread(); + } + } + + + public String getModelFileName() { + return modelFileName; + } + + public void setModelFileName(String modelFileName) { + this.modelFileName = modelFileName; + } + + public String getLabelFileName() { + return labelFileName; + } + + public void setLabelFileName(String labelFileName) { + this.labelFileName = labelFileName; + } + + public String getResult() { + return result; + } + + public void setResult(String result) { + this.result = result; + } + } diff --git a/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/skills/RingDetector.java b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/skills/RingDetector.java new file mode 100644 index 000000000000..8dd45bbd1ac2 --- /dev/null +++ b/TeamCode/src/main/java/org/firstinspires/ftc/teamcode/skills/RingDetector.java @@ -0,0 +1,289 @@ + package org.firstinspires.ftc.teamcode.skills; + +import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode; +import com.qualcomm.robotcore.hardware.HardwareMap; +import com.qualcomm.robotcore.util.ElapsedTime; + +import org.firstinspires.ftc.robotcore.external.Telemetry; +import org.firstinspires.ftc.teamcode.TFLiteimgmethod.GenericDetector; +import org.firstinspires.ftc.teamcode.TFLiteimgmethod.classification.Classifier; + +import java.util.ArrayList; +import java.util.List; + +public class RingDetector implements Runnable{ + Telemetry telemetry; + private Detector tfDetector = null; + private HardwareMap hardwareMap; + + private boolean isRunning = true; + + private AutoDot recogZone = null; + private float distance = 0; + + private String modelFileName = "rings_float.tflite";//"croppedRingRec.tflite"; + private String labelFileName = "labels.txt";//"croppedLabels.txt"; + private static Classifier.Model MODEl_TYPE = Classifier.Model.FLOAT_EFFICIENTNET; + private static final String LABEL_A = "None"; + private static final String LABEL_B = "Single"; + private static final String LABEL_C = "Quad"; + // default zone + private String targetZone = LABEL_B; + + private String side = AutoRoute.NAME_RED; + private LinearOpMode caller = null; + + private ArrayList namedCoordinates = new ArrayList<>(); + + private static AutoDot zoneA = new AutoDot("A", 75, 75, -1, AutoRoute.NAME_RED); + private static AutoDot zoneB = new AutoDot("B", 65, 100, -1, AutoRoute.NAME_RED); + private static AutoDot zoneC = new AutoDot("C", 75, 120, -1, AutoRoute.NAME_RED); + + public RingDetector(HardwareMap hMap, String side, LinearOpMode caller, ArrayList namedCoordinates, Telemetry t) throws Exception { + hardwareMap = hMap; + telemetry = t; + initDetector(); + activateDetector(); + this.side = side; + this.caller = caller; + if (namedCoordinates != null) { + this.namedCoordinates = namedCoordinates; + } + configZones(side); + } + + public RingDetector(HardwareMap hMap, String side, LinearOpMode caller, ArrayList namedCoordinates, Telemetry t, String model, String labels) throws Exception { + hardwareMap = hMap; + telemetry = t; + setModelFileName(model); + setLabelFileName(labels); + initDetector(); + activateDetector(); + this.side = side; + this.caller = caller; + if (namedCoordinates != null) { + this.namedCoordinates = namedCoordinates; + } + configZones(side); + } + + protected void configZones(String side){ + if (this.namedCoordinates.size() > 0){ + for(AutoDot d : namedCoordinates){ + if (d.getFieldSide().equals(this.side)) { + if (d.getFieldSide().equals(side)) { + if (d.getDotName().equals("A")) { + zoneA = d; + } else if (d.getDotName().equals("B")) { + zoneB = d; + } else if (d.getDotName().equals("C")) { + zoneC = d; + } + } + } + } + } + } + + public AutoDot detectRing(int timeout, String side, Telemetry telemetry, LinearOpMode caller) { + configZones(side); + AutoDot zone = zoneB; + + ElapsedTime runtime = new ElapsedTime(); + runtime.reset(); + boolean fromConfig = this.namedCoordinates.size() > 0; + while (runtime.seconds() <= timeout) { + telemetry.addData("this.namedCoordinates.size() > 0", fromConfig); + if (tfDetector != null) { + List results = tfDetector.getLastResults(); + if (results == null || results.size() == 0) { + telemetry.addData("Nada", "No results"); + } else { + for (Classifier.Recognition r : results) { + if (r.getConfidence() >= 0.8) { + telemetry.addData("PrintZone", r.getTitle()); + if (r.getTitle().contains(LABEL_C)) { + zone = zoneC; + } + else if(r.getTitle().contains(LABEL_B)){ + zone = zoneB; + } + else if(r.getTitle().contains(LABEL_A)){ + zone = zoneA; + } + + targetZone = zone.getDotName(); + distance = (r.getLocation().centerX()); + + telemetry.addData("Zone", targetZone); + telemetry.addData("left", r.getLocation().centerX()); + telemetry.addData("right", r.getLocation().centerY()); + } + } + } + } + telemetry.update(); + } + + return zone; + } + + public float detectRingsPos(int timeout, Telemetry telemetry, LinearOpMode caller) { +// configZones(side); + AutoDot zone = zoneB; + + ElapsedTime runtime = new ElapsedTime(); + runtime.reset(); + boolean fromConfig = this.namedCoordinates.size() > 0; + while (runtime.seconds() <= timeout) { + telemetry.addData("this.namedCoordinates.size() > 0", fromConfig); + if (tfDetector != null) { + List results = tfDetector.getLastResults(); + if (results == null || results.size() == 0) { + telemetry.addData("Nada", "No results"); + } else { + for (Classifier.Recognition r : results) { + if (r.getConfidence() >= 0.5) { + telemetry.addData("PrintZone", r.getTitle()); + if (r.getTitle().contains(LABEL_C)) { + zone = zoneC; + } + else if(r.getTitle().contains(LABEL_B)){ + zone = zoneB; + } + else if(r.getTitle().contains(LABEL_A)){ + zone = zoneA; + } + + targetZone = zone.getDotName(); + distance = (r.getLocation().left + r.getLocation().right)/2; + + telemetry.addData("CroppedCenter", distance); + telemetry.addData("left", r.getLocation().left); + telemetry.addData("right", r.getLocation().right); + } + } + } + } + telemetry.update(); + } + + return distance; + } + + public void detectRingThread() { + this.recogZone = zoneB; + + ElapsedTime runtime = new ElapsedTime(); + runtime.reset(); + boolean fromConfig = this.namedCoordinates.size() > 0; + while (isRunning) { + telemetry.addData("this.namedCoordinates.size() > 0", fromConfig); + if (tfDetector != null) { + List results = tfDetector.getLastResults(); + if (results == null || results.size() == 0) { + telemetry.addData("Nada", "No results"); + } else { + for (Classifier.Recognition r : results) { + if (r.getConfidence() >= 0.6) { + telemetry.addData("PrintZone", r.getTitle()); + if (r.getTitle().contains(LABEL_C)) { + this.recogZone = zoneC; + } + else if(r.getTitle().contains(LABEL_B)){ + this.recogZone = zoneB; + } + else if(r.getTitle().contains(LABEL_A)){ + this.recogZone = zoneA; + } + + distance = r.getLocation().centerX(); + targetZone = this.recogZone.getDotName(); + + telemetry.addData("Zone", targetZone); + telemetry.addData("CenterX", distance); + telemetry.addData("left", r.getLocation().centerX()); + telemetry.addData("right", r.getLocation().centerY()); + } + } + } + } + telemetry.update(); + } + + } + +// public void displayLights() { +// switch (targetZone) { +// case "C": +// this.lights.blue(); +// break; +// case "B": +// this.lights.orange(); +// break; +// case "A": +// this.lights.pink(); +// break; +// } +// } + + public void initDetector() throws Exception { + tfDetector = new Detector(MODEl_TYPE, getModelFileName(), getLabelFileName(), hardwareMap.appContext, telemetry); + } + + protected void activateDetector() throws Exception { + if (tfDetector != null) { + tfDetector.activate(); + } + telemetry.addData("Info", "TF Activated"); + } + + public String returnZone() { + return targetZone; + } + + public void stopDetection() { + stopThread(); + if (tfDetector != null) { + tfDetector.stopProcessing(); + } + tfDetector = null; + } + + public void stopThread() { + isRunning = false; + } + + @Override + public void run() { + while(isRunning) { + detectRingThread(); + } + } + + public AutoDot getRecogZone() { + return recogZone; + } + + public float getRingCenter() { return distance;} + + public void setRecogZone(AutoDot recogZone) { + this.recogZone = recogZone; + } + + public String getModelFileName() { + return modelFileName; + } + + public void setModelFileName(String modelFileName) { + this.modelFileName = modelFileName; + } + + public String getLabelFileName() { + return labelFileName; + } + + public void setLabelFileName(String labelFileName) { + this.labelFileName = labelFileName; + } +}