Vision: Add client library

This commit is contained in:
Marvin W 2020-12-10 23:29:09 +01:00
parent a3d6f1aed5
commit 0e0ac35e51
No known key found for this signature in database
GPG Key ID: 072E9235DB996F2A
15 changed files with 1217 additions and 1 deletions

View File

@ -0,0 +1,104 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.common.images;
import org.microg.gms.common.PublicApi;
/**
* Immutable class for describing width and height dimensions in pixels.
*/
@PublicApi
public class Size {
private int width;
private int height;
/**
* Create a new immutable Size instance.
*
* @param width The width of the size, in pixels
* @param height The height of the size, in pixels
*/
public Size(int width, int height) {
this.width = width;
this.height = height;
}
/**
* Check if this size is equal to another size.
* <p>
* Two sizes are equal if and only if both their widths and heights are equal.
* <p>
* A size object is never equal to any other type of object.
*
* @return {@code true} if the objects were equal, {@code false} otherwise
*/
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Size)) return false;
Size size = (Size) o;
if (width != size.width) return false;
return height == size.height;
}
/**
* Get the height of the size (in pixels).
*
* @return height
*/
public int getHeight() {
return height;
}
/**
* Get the width of the size (in pixels).
*
* @return width
*/
public int getWidth() {
return width;
}
@Override
public int hashCode() {
int result = width;
result = 31 * result + height;
return result;
}
/**
* Parses the specified string as a size value.
* <p>
* The ASCII characters {@code \}{@code u002a} ('*') and {@code \}{@code u0078} ('x') are recognized as separators between the width and height.
* <p>
* For any {@code Size s}: {@code Size.parseSize(s.toString()).equals(s)}. However, the method also handles sizes expressed in the following forms:
* <p>
* "width{@code x}height" or "width{@code *}height" => new Size(width, height), where width and height are string integers potentially containing a sign, such as "-10", "+7" or "5".
*
* @param string the string representation of a size value.
* @return the size value represented by {@code string}.
* @throws NumberFormatException if {@code string} cannot be parsed as a size value.
* @throws NullPointerException if {@code string} was null
*/
public static Size parseSize(String string) {
if (string == null) throw new NullPointerException("string must not be null");
int split = string.indexOf('*');
if (split < 0) split = string.indexOf('x');
if (split < 0) throw new NumberFormatException("Invalid Size: \"" + string + "\"");
return new Size(Integer.parseInt(string.substring(0, split)), Integer.parseInt(string.substring(split + 1)));
}
/**
* Return the size represented as a string with the format {@code "WxH"}
* @return string representation of the size
*/
@Override
public String toString() {
return width + "x" + height;
}
}

View File

@ -30,6 +30,7 @@ description = 'microG API for play-services-vision'
dependencies {
api project(':play-services-basement')
api project(':play-services-base-api')
api project(':play-services-vision-common-api')
implementation "androidx.annotation:annotation:$annotationVersion"
}

View File

@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
apply plugin: 'com.android.library'
apply plugin: 'maven-publish'
apply plugin: 'signing'
android {
compileSdkVersion androidCompileSdk
buildToolsVersion "$androidBuildVersionTools"
defaultConfig {
versionName version
minSdkVersion androidMinSdk
targetSdkVersion androidTargetSdk
}
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
}
apply from: '../gradle/publish-android.gradle'
description = 'microG API for play-services-vision-common'
dependencies {
api project(':play-services-basement')
api project(':play-services-base-api')
implementation "androidx.annotation:annotation:$annotationVersion"
}

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<manifest package="org.microg.gms.vision.common.api"/>

View File

@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
apply plugin: 'com.android.library'
apply plugin: 'maven-publish'
apply plugin: 'signing'
android {
compileSdkVersion androidCompileSdk
buildToolsVersion "$androidBuildVersionTools"
defaultConfig {
versionName version
minSdkVersion androidMinSdk
targetSdkVersion androidTargetSdk
}
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
}
apply from: '../gradle/publish-android.gradle'
description = 'microG implementation of play-services-vision-common'
dependencies {
api project(':play-services-base')
api project(':play-services-vision-api')
implementation "androidx.annotation:annotation:$annotationVersion"
}

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<manifest package="org.microg.gms.vision.common"/>

View File

@ -0,0 +1,481 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
* Notice: Portions of this file are reproduced from work created and shared by Google and used
* according to terms described in the Creative Commons 4.0 Attribution License.
* See https://developers.google.com/readme/policies for details.
*/
package com.google.android.gms.vision;
import android.content.Context;
import android.graphics.ImageFormat;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.os.SystemClock;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.WindowManager;
import androidx.annotation.GuardedBy;
import com.google.android.gms.common.images.Size;
import org.microg.gms.common.PublicApi;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
/**
* Manages the camera in conjunction with an underlying {@link Detector}. This receives preview frames from the camera
* at a specified rate, sending those frames to the detector as fast as it is able to process those frames.
* <p>
* This camera source makes a best effort to manage processing on preview frames as fast as possible, while at the same
* time minimizing lag. As such, frames may be dropped if the detector is unable to keep up with the rate of frames
* generated by the camera. You should use {@link CameraSource.Builder#setRequestedFps(float)} to specify a frame rate
* that works well with the capabilities of the camera hardware and the detector options that you have selected.
* If CPU utilization is higher than you'd like, then you may want to consider reducing FPS.
* If the camera preview or detector results are too "jerky", then you may want to consider increasing FPS.
* <p>
* The following Android permission is required to use the camera:
* <ul>
* <li>android.permissions.CAMERA</li>
* </ul>
*/
@PublicApi
public class CameraSource {
public static final int CAMERA_FACING_BACK = 0;
public static final int CAMERA_FACING_FRONT = 1;
private final Object cameraLock = new Object();
@GuardedBy("cameraLock")
private Camera camera;
private Context context;
private int facing = CAMERA_FACING_BACK;
private boolean autoFocusEnabled;
private String focusMode;
private float requestedFps = 30;
private int requestedWidth = 1024;
private int requestedHeight = 768;
private Size previewSize;
private int rotation;
private Thread detectorThread;
private DetectorRunner detectorRunner = new DetectorRunner();
private Detector<?> detector;
/**
* Returns the selected camera; one of {@link #CAMERA_FACING_BACK} or {@link #CAMERA_FACING_FRONT}.
*/
public int getCameraFacing() {
return facing;
}
/**
* Returns the preview size that is currently in use by the underlying camera.
*/
public Size getPreviewSize() {
return previewSize;
}
/**
* Stops the camera and releases the resources of the camera and underlying detector.
*/
public void release() {
synchronized (cameraLock) {
stop();
if (detector != null) detector.release();
}
}
/**
* Opens the camera and starts sending preview frames to the underlying detector. The preview frames are not displayed.
*
* @throws IOException if the camera's preview texture or display could not be initialized
*/
public CameraSource start() throws IOException {
synchronized (cameraLock) {
if (camera == null) {
camera = createCamera();
camera.setPreviewTexture(new SurfaceTexture(100));
camera.startPreview();
startDetectorThread();
}
return this;
}
}
/**
* Opens the camera and starts sending preview frames to the underlying detector.
* The supplied surface holder is used for the preview so frames can be displayed to the user.
*
* @param surfaceHolder the surface holder to use for the preview frames
* @throws IOException if the supplied surface holder could not be used as the preview display
*/
public CameraSource start(SurfaceHolder surfaceHolder) throws IOException {
synchronized (cameraLock) {
if (camera == null) {
camera = createCamera();
camera.setPreviewDisplay(surfaceHolder);
camera.startPreview();
startDetectorThread();
}
return this;
}
}
/**
* Closes the camera and stops sending frames to the underlying frame detector.
* <p>
* This camera source may be restarted again by calling {@link #start()} or {@link #start(SurfaceHolder)}.
* <p>
* Call {@link #release()} instead to completely shut down this camera source and release the resources of the underlying detector.
*/
public void stop() {
synchronized (cameraLock) {
detectorRunner.setActive(false);
if (detectorThread != null) {
try {
detectorThread.join();
} catch (InterruptedException e) {
// Ignore
}
detectorThread = null;
}
if (camera != null) {
camera.stopPreview();
camera.setPreviewCallbackWithBuffer(null);
try {
camera.setPreviewTexture(null);
} catch (IOException e) {
// Ignore
}
try {
camera.setPreviewDisplay(null);
} catch (IOException e) {
// Ignore
}
camera.release();
camera = null;
}
}
}
/**
* Initiates taking a picture, which happens asynchronously.
* The camera source should have been activated previously with {@link #start()} or {@link #start(SurfaceHolder)}.
* The camera preview is suspended while the picture is being taken, but will resume once picture taking is done.
*
* @param shutter the callback for image capture moment, or null
* @param jpeg the callback for JPEG image data, or null
*/
public void takePicture(CameraSource.ShutterCallback shutter, CameraSource.PictureCallback jpeg) {
synchronized (this.cameraLock) {
if (camera != null) {
camera.takePicture(shutter::onShutter, null, null, (data, camera) -> jpeg.onPictureTaken(data));
}
}
}
/**
* Builder for configuring and creating an associated camera source.
*/
public static class Builder {
private CameraSource cameraSource = new CameraSource();
/**
* Creates a camera source builder with the supplied context and detector.
* Camera preview images will be streamed to the associated detector upon starting the camera source.
*/
public Builder(Context context, Detector<?> detector) {
if (context == null) throw new IllegalArgumentException("No context supplied.");
if (detector == null) throw new IllegalArgumentException("No detector supplied.");
this.cameraSource.context = context;
this.cameraSource.detector = detector;
}
/**
* Creates an instance of the camera source.
*/
public CameraSource build() {
return cameraSource;
}
/**
* Sets whether to enable camera auto focus. If set to false (default), the camera's default focus setting is used. If set to true, a continuous video focus setting is used (if supported by the camera hardware).
* Default: false.
*/
public Builder setAutoFocusEnabled(boolean autoFocusEnabled) {
this.cameraSource.autoFocusEnabled = autoFocusEnabled;
return this;
}
/**
* Sets the camera to use (either {@link CameraSource#CAMERA_FACING_BACK} or {@link CameraSource#CAMERA_FACING_FRONT}).
* Default: back facing.
*/
public Builder setFacing(int facing) {
this.cameraSource.facing = facing;
return this;
}
/**
* Sets which FocusMode will be used for camera focus. Only FOCUS_MODE_CONTINUOUS_PICTURE and FOCUS_MODE_CONTINUOUS_VIDEO are supported for now.
*/
public Builder setFocusMode(String focusMode) {
this.cameraSource.focusMode = focusMode;
return this;
}
/**
* Sets the requested frame rate in frames per second. If the exact requested value is not not available, the best matching available value is selected.
* Default: 30.
*/
public Builder setRequestedFps(float fps) {
this.cameraSource.requestedFps = fps;
return this;
}
/**
* Sets the desired width and height of the camera frames in pixels. If the exact desired values are not available options, the best matching available options are selected. Also, we try to select a preview size which corresponds to the aspect ratio of an associated full picture size, if applicable.
* Default: 1024x768.
*/
public Builder setRequestedPreviewSize(int width, int height) {
this.cameraSource.requestedWidth = width;
this.cameraSource.requestedHeight = height;
return this;
}
}
/**
* Callback interface used to supply image data from a photo capture.
*/
public interface PictureCallback {
/**
* Called when image data is available after a picture is taken. The format of the data is a jpeg binary.
*/
void onPictureTaken(byte[] data);
}
/**
* Callback interface used to signal the moment of actual image capture.
*/
public interface ShutterCallback {
/**
* Called as near as possible to the moment when a photo is captured from the sensor. This is a good opportunity to play a shutter sound or give other feedback of camera operation. This may be some time after the photo was triggered, but some time before the actual data is available.
*/
void onShutter();
}
private void startDetectorThread() {
if (detectorThread != null && detectorThread.isAlive()) return;
detectorThread = new Thread(detectorRunner);
detectorThread.setName("gms.vision.CameraSource");
detectorRunner.setActive(true);
detectorThread.start();
}
private Camera createCamera() throws IOException {
int cameraId = getFacingCameraId();
Camera camera = Camera.open(cameraId);
try {
Camera.Parameters parameters = camera.getParameters();
// Set size
SizePair size = pickCameraSize(camera);
previewSize = size.preview;
parameters.setPreviewSize(previewSize.getWidth(), previewSize.getHeight());
if (size.picture != null) {
parameters.setPictureSize(size.picture.getWidth(), size.picture.getHeight());
}
// Set FPS
int[] fpsRange = pickFpsRange(camera);
parameters.setPreviewFpsRange(fpsRange[0], fpsRange[1]);
// Set focus mode
if (autoFocusEnabled && (focusMode == null || !parameters.getSupportedFocusModes().contains(focusMode))) {
if (parameters.getSupportedFocusModes().contains("continuous-video")) {
focusMode = "continuous-video";
} else {
focusMode = null;
}
}
if (focusMode != null && parameters.getSupportedFocusModes().contains(focusMode)) {
parameters.setFocusMode(focusMode);
}
// Handle rotation
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
int displayRotation = 0;
switch (windowManager.getDefaultDisplay().getRotation()) {
case Surface.ROTATION_90: displayRotation = 90; break;
case Surface.ROTATION_180: displayRotation = 180; break;
case Surface.ROTATION_270: displayRotation = 270; break;
}
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
Camera.getCameraInfo(cameraId, cameraInfo);
if (facing == CAMERA_FACING_FRONT) {
rotation = ((cameraInfo.orientation + displayRotation) % 360) / 90;
camera.setDisplayOrientation((360 - (cameraInfo.orientation + displayRotation)) % 360);
} else {
rotation = ((cameraInfo.orientation - displayRotation + 360) % 360) / 90;
camera.setDisplayOrientation((cameraInfo.orientation - displayRotation + 360) % 360);
}
parameters.setRotation(rotation * 90);
parameters.setPreviewFormat(ImageFormat.NV21);
camera.setParameters(parameters);
// Add processing
camera.setPreviewCallbackWithBuffer(new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera frameCamera) {
detectorRunner.onPreviewFrame(data, frameCamera);
}
});
camera.addCallbackBuffer(new byte[(int) Math.ceil((double) ((long) (previewSize.getHeight() * previewSize.getWidth() * ImageFormat.getBitsPerPixel(parameters.getPreviewFormat()))) / 8.0D) + 1]);
return camera;
} catch (Exception e) {
camera.release();
throw e;
}
}
private int getFacingCameraId() throws IOException {
Camera.CameraInfo info = new Camera.CameraInfo();
for (int i = 0; i < Camera.getNumberOfCameras(); i++) {
Camera.getCameraInfo(i, info);
if (info.facing == facing) {
return i;
}
}
throw new IOException("Could not find requested camera.");
}
private int[] pickFpsRange(Camera camera) throws IOException {
int requestsFpms = (int) (requestedFps * 1000.0F);
int[] selectedFpsRange = null;
int selectedPreviewFpsOffset = Integer.MAX_VALUE;
for (int[] previewFpsRange : camera.getParameters().getSupportedPreviewFpsRange()) {
int minOffset = requestsFpms - previewFpsRange[0];
int maxOffset = requestsFpms - previewFpsRange[1];
int previewFpsOffset;
if ((previewFpsOffset = Math.abs(minOffset) + Math.abs(maxOffset)) < selectedPreviewFpsOffset) {
selectedFpsRange = previewFpsRange;
selectedPreviewFpsOffset = previewFpsOffset;
}
}
if (selectedFpsRange == null) throw new IOException("Could not find suitable preview frames per second range.");
return selectedFpsRange;
}
private SizePair pickCameraSize(Camera camera) throws IOException {
ArrayList<SizePair> sizeCandidates = new ArrayList<>();
for (Camera.Size previewSize : camera.getParameters().getSupportedPreviewSizes()) {
float previewAspectRatio = (float) (previewSize).width / (float) previewSize.height;
for (Camera.Size pictureSize : camera.getParameters().getSupportedPictureSizes()) {
float pictureAspectRatio = (float) (pictureSize).width / (float) pictureSize.height;
if (Math.abs(previewAspectRatio - pictureAspectRatio) < 0.01F) { // Approximately same aspect ratio
sizeCandidates.add(new SizePair(previewSize, pictureSize));
break;
}
}
}
if (sizeCandidates.size() == 0) {
for (Camera.Size previewSize : camera.getParameters().getSupportedPreviewSizes()) {
sizeCandidates.add(new SizePair(previewSize, null));
}
}
SizePair sizePair = null;
int selectedSizeOffset = Integer.MAX_VALUE;
int sizeOffset;
for (SizePair candidate : sizeCandidates) {
Size candidatePreviewSize = candidate.preview;
sizeOffset = Math.abs(candidatePreviewSize.getWidth() - requestedWidth) + Math.abs(candidatePreviewSize.getHeight() - requestedHeight);
if (sizeOffset < selectedSizeOffset) {
sizePair = candidate;
selectedSizeOffset = sizeOffset;
}
}
if (sizePair == null) throw new IOException("Could not find suitable preview size.");
return sizePair;
}
private static class SizePair {
Size preview;
Size picture;
public SizePair(Camera.Size preview, Camera.Size picture) {
this.preview = new Size(preview.width, preview.height);
if (picture != null) {
this.picture = new Size(picture.width, picture.height);
}
}
}
private class DetectorRunner implements Runnable {
private final Object lock = new Object();
private long startElapsedRealtime = SystemClock.elapsedRealtime();
private long timestampMillis;
private byte[] frameData;
private ByteBuffer frameBuffer;
private ByteBuffer frameDataBuffer;
private int frameId = 0;
private boolean active;
public void setActive(boolean active) {
synchronized (lock) {
this.active = active;
lock.notifyAll();
}
}
@Override
public void run() {
while (true) {
Frame frame;
synchronized (lock) {
while (active && frameBuffer == null) {
try {
lock.wait();
} catch (InterruptedException e) {
return;
}
}
if (!active) return;
frame = new Frame.Builder().setId(frameId).setImageData(frameBuffer, previewSize.getWidth(), previewSize.getHeight(), ImageFormat.NV21).setTimestampMillis(timestampMillis).setRotation(rotation).build();
frameBuffer = null;
}
try {
detector.receiveFrame(frame);
} catch (Exception e) {
// Ignore
} finally {
camera.addCallbackBuffer(frameData);
}
}
}
public void onPreviewFrame(byte[] data, Camera camera) {
synchronized (lock) {
if (frameData == null) {
frameData = data;
frameDataBuffer = ByteBuffer.wrap(data);
}
if (data == frameData) {
frameId++;
timestampMillis = SystemClock.elapsedRealtime();
frameBuffer = frameDataBuffer;
lock.notifyAll();
}
}
}
}
}

View File

@ -0,0 +1,153 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
* Notice: Portions of this file are reproduced from work created and shared by Google and used
* according to terms described in the Creative Commons 4.0 Attribution License.
* See https://developers.google.com/readme/policies for details.
*/
package com.google.android.gms.vision;
import android.util.SparseArray;
import androidx.annotation.GuardedBy;
import org.microg.gms.common.PublicApi;
/**
* Detector is the base class for implementing specific detector instances, such as a barcode detector or face detector. A detector receives a Frame as input, and produces a number of detected items as output. The Detector implementation is generic, parameterized by T, the type of the detected items.
*/
@PublicApi
public abstract class Detector<T> {
private final Object processorLock = new Object();
@GuardedBy("processorLock")
private Processor<T> processor;
/**
* Analyzes the supplied frame to find target item instances (e.g., the face detector finds faces). Subclasses implement this method for calling specific detection code, returning result objects with associated tracking ID mappings.
*
* @return mapping of int to detected object, where the int domain represents the ID of the associated item. If tracking is enabled, as the same object is detected in consecutive frames, the detector will return the same ID for that item.
*/
public abstract SparseArray<T> detect(Frame frame);
/**
* Indicates whether the detector has all of the required dependencies available locally in order to do detection.
* <p>
* When an app is first installed, it may be necessary to download required files. If this returns false, those files are not yet available. Usually this download is taken care of at application install time, but this is not guaranteed. In some cases the download may have been delayed.
* <p>
* If your code has added a processor, an indication of the detector operational state is also indicated with the {@link Detector.Detections#detectorIsOperational()} method. You can check this in your app as it processes detection results, and can convey this state to the user if appropriate.
*
* @return true if the detector is operational, false if the dependency download is in progress
*/
public boolean isOperational() {
return true;
}
/**
* Pipeline method (see class level documentation above) for receiving frames for detection. Detection results are forwarded onto a processor that was previously registered with this class (see {@link #setProcessor(Detector.Processor)}).
* <p>
* Alternatively, if you are just looking to synchronously run the detector on a single frame, use {@link #detect(Frame)} instead.
*/
public void receiveFrame(Frame frame) {
Detections<T> detections = new Detections<>(detect(frame), frame.getMetadata().withRotationAppliedToSize(), isOperational());
synchronized (processorLock) {
if (processor == null) {
throw new IllegalStateException("Detector processor must first be set with setProcessor in order to receive detection results.");
} else {
processor.receiveDetections(detections);
}
}
}
/**
* Shuts down the detector, releasing any underlying resources.
*/
public void release() {
synchronized (processorLock) {
if (processor != null) {
processor.release();
processor = null;
}
}
}
/**
* Sets the ID of the detected item in which to exclusively track in future use of the detector. This can be used to avoid unnecessary work in detecting all items in future frames, when it's only necessary to receive results for a specific item. After setting this ID, the detector may only return results for the associated tracked item. When that item is no longer present in a frame, the detector will revert back to detecting all items.
* <p>
* Optionally, subclasses may override this to support optimized tracking.
*
* @param id tracking ID to become the focus for future detections. This is a mapping ID as returned from {@link #detect(Frame)} or received from {@link Detector.Detections#getDetectedItems()}.
*/
public boolean setFocus(int id) {
return true;
}
/**
* Pipeline method (see class level documentation above) which sets the {@link Detector.Processor} instance.
* This is used in creating the pipeline structure, associating a post-processor with the detector.
*/
public void setProcessor(Processor<T> processor) {
synchronized (processorLock) {
if (this.processor != null) {
this.processor.release();
}
this.processor = processor;
}
}
/**
* Detection result object containing both detected items and the associated frame metadata.
*/
public static class Detections<T> {
private final SparseArray<T> detectedItems;
private final Frame.Metadata frameMetadata;
private final boolean isOperational;
@PublicApi(exclude = true)
public Detections(SparseArray<T> detectedItems, Frame.Metadata frameMetadata, boolean isOperational) {
this.isOperational = isOperational;
this.detectedItems = detectedItems;
this.frameMetadata = frameMetadata;
}
/**
* Returns true if the detector is operational, false if it is not operational. In the non-operational case, the detector will return no results.
* <p>
* A detector may be non-operational for a while when starting an app for the first time, if a download is required to obtain the associated library and model files required to do detection.
*/
public boolean detectorIsOperational() {
return isOperational;
}
/**
* Returns a collection of the detected items that were identified in the frame.
*
* @return mapping of int to detected object, where the int domain represents the consistent tracking ID of the associated item. As the same object is detected in consecutive frames, the detector will return the same ID for that item.
*/
public SparseArray<T> getDetectedItems() {
return detectedItems;
}
/**
* Returns the metadata of the associated frame in which the detection originated.
*/
public Frame.Metadata getFrameMetadata() {
return frameMetadata;
}
}
/**
* Interface for defining a post-processing action to be executed for each detection, when using the detector as part of a pipeline (see the class level docs above). An instance of a processor is associated with the detector via the {@link Detector#setProcessor(Detector.Processor)} method.
*/
public interface Processor<T> {
/**
* Called by the detector to deliver detection results to the processor.
*/
void receiveDetections(Detections<T> detections);
/**
* Shuts down and releases associated processor resources.
*/
void release();
}
}

View File

@ -0,0 +1,257 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
* Notice: Portions of this file are reproduced from work created and shared by Google and used
* according to terms described in the Creative Commons 4.0 Attribution License.
* See https://developers.google.com/readme/policies for details.
*/
package com.google.android.gms.vision;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.ImageFormat;
import android.view.Display;
import com.google.android.gms.vision.internal.FrameMetadataParcel;
import org.microg.gms.common.PublicApi;
import java.nio.ByteBuffer;
/**
* Image data with associated {@link Metadata}.
* <p>
* A frame is constructed via the {@link Builder} class, specifying the image data, dimensions, and sequencing information (frame ID, timestamp).
*/
public class Frame {
public static final int ROTATION_0 = 0;
public static final int ROTATION_90 = 1;
public static final int ROTATION_180 = 2;
public static final int ROTATION_270 = 3;
private Bitmap bitmap;
private ByteBuffer imageData;
private final Metadata metadata = new Metadata();
/**
* Returns the bitmap which was specified in creating this frame, or null if no bitmap was used to create this frame. If the bitmap is not available, then {@link #getGrayscaleImageData()} should be called instead.
*/
public Bitmap getBitmap() {
return bitmap;
}
/**
* Returns the grayscale version of the frame data, with one byte per pixel. Note that the returned byte buffer will be prefixed by the Y channel (i.e., the grayscale image data), but may optionally include additional image data beyond the Y channel (this can be ignored).
* <p>
* If a bitmap was specified when creating this frame, the bitmap is first converted to a grayscale byte[] (allocation / copy required). It is recommended that you use the bitmap directly through {@link #getBitmap()} if the associated native detection code supports it, since this would move the grayscale conversion into native code where it will be faster.
*/
public ByteBuffer getGrayscaleImageData() {
if (bitmap == null) {
return imageData;
}
int width = metadata.width;
int height = metadata.height;
int[] pixels = new int[width * height];
bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
byte[] values = new byte[width * height];
for (int i = 0; i < pixels.length; i++) {
values[i] = (byte)((int)((float) Color.red(pixels[i]) * 0.299F + (float)Color.green(pixels[i]) * 0.587F + (float)Color.blue(pixels[i]) * 0.114F));
}
return ByteBuffer.wrap(values);
}
/**
* Returns the metadata associated with the frame.
*/
public Frame.Metadata getMetadata() {
return metadata;
}
/**
* Builder for creating a frame instance.
* At a minimum, image information must be specified either through {@link #setBitmap(Bitmap)} or {@link #setImageData(ByteBuffer, int, int, int)}.
*/
public static class Builder {
private final Frame frame = new Frame();
/**
* Creates the frame instance.
*
* @throws IllegalStateException if the image data has not been set via {@link #setBitmap(Bitmap)} or {@link #setImageData(ByteBuffer, int, int, int)}.
*/
public Frame build() {
if (this.frame.bitmap == null && this.frame.imageData == null) {
throw new IllegalStateException("Missing image data. Call either setBitmap or setImageData to specify the image");
}
return frame;
}
/**
* Sets the image data for the frame from a supplied bitmap.
* <p>
* While this is a convenient way to specify certain images (e.g., images read from a file), note that a copy is required to extract pixel information for use in detectors -- this could mean extra GC overhead. Using {@link #setImageData(ByteBuffer, int, int, int)} is the preferred way to specify image data if you can handle the data in a supported byte format and reuse the byte buffer, since it does not require a making a copy.
*/
public Builder setBitmap(Bitmap bitmap) {
this.frame.bitmap = bitmap;
this.frame.metadata.width = bitmap.getWidth();
this.frame.metadata.height = bitmap.getHeight();
return this;
}
/**
* Sets the frame ID. A frame source such as a live video camera or a video player is expected to assign IDs in monotonically increasing order, to indicate the sequence that the frame appeared relative to other frames.
* <p>
* A {@link Detector.Processor} implementation may rely upon this sequence ID to detect frame sequence gaps, to compute velocity, etc.
*/
public Builder setId(int id) {
this.frame.metadata.id = id;
return this;
}
/**
* Sets the image data from the supplied byte buffer, size, and format.
*
* @param data contains image byte data according to the associated format.
* @param width
* @param height
* @param format one of {@link ImageFormat#NV16}, {@link ImageFormat#NV21}, or {@link ImageFormat#YV12}.
* @throws IllegalArgumentException if the supplied data is null, or an invalid image format was supplied.
*/
public Builder setImageData(ByteBuffer data, int width, int height, int format) {
if (data == null) throw new IllegalArgumentException("Null image data supplied");
if (data.capacity() < width * height) throw new IllegalArgumentException("Invalid image data size");
if (format != ImageFormat.NV16 && format != ImageFormat.NV21 && format != ImageFormat.YV12)
throw new IllegalArgumentException("Unsupported image format: " + format);
this.frame.imageData = data;
this.frame.metadata.width = width;
this.frame.metadata.height = height;
this.frame.metadata.format = format;
return this;
}
/**
* Sets the image rotation, indicating the rotation from the upright orientation.
* <p>
* Since the camera may deliver images that are rotated (e.g., if the user holds the device upside down), specifying the rotation with the image indicates how to make the image be upright, if necessary. Some detectors may rely upon rotating the image before attempting detection, whereas others may not. In preserving the original image from the camera along with this value, the detector may choose whether to make this correction (and to assume the associated cost).
* <p>
* However, note that the detector is expected to report detection position coordinates that are relative to the upright version of the image (whether or not the image was actually rotated by the detector). The {@link Detector} will always deliver frame metadata to the {@link Detector.Processor} that indicates the dimensions and orientation of an unrotated, upright frame.
*
* @param rotation one of {@link Frame#ROTATION_0}, {@link Frame#ROTATION_90}, {@link Frame#ROTATION_180}, {@link Frame#ROTATION_270}. Has the same meaning as {@link Display#getRotation()}.
*/
public Builder setRotation(int rotation) {
this.frame.metadata.rotation = rotation;
return this;
}
/**
* Sets the frame timestamp, in milliseconds. A frame source such as a live video camera or a video player is expected to assign timestamps in a way that makes sense for the medium. For example, live video may use the capture time of each frame, whereas a video player may use the elapsed time to the frame within the video. Timestamps should be in monotonically increasing order, to indicate the passage of time.
* <p>
* A {@link Detector.Processor} implementation may rely upon this sequence ID to detect frame sequence gaps, to compute velocity, etc.
*/
public Builder setTimestampMillis(long timestampMillis) {
this.frame.metadata.timestampMillis = timestampMillis;
return this;
}
}
/**
* Frame metadata, describing the image dimensions, rotation, and sequencing information.
*/
public static class Metadata {
private int format = -1;
private int height;
private int id;
private int rotation;
private long timestampMillis;
private int width;
public Metadata() {
}
private Metadata(Metadata metadata) {
this.format = metadata.format;
this.height = metadata.height;
this.id = metadata.id;
this.rotation = metadata.rotation;
this.timestampMillis = metadata.timestampMillis;
this.width = metadata.width;
}
/**
* Returns the format of this image if image data is set.
*
* @return one of {@link ImageFormat#NV16}, {@link ImageFormat#NV21, {@link ImageFormat#YV12} or {@link ImageFormat#YUV_420_888}.
*/
public int getFormat() {
return format;
}
/**
* Returns the frame height.
*/
public int getHeight() {
return height;
}
/**
* Returns the frame ID. A frame source such as a live video camera or a video player is expected to assign IDs in monotonically increasing order, to indicate the sequence that the frame appeared relative to other frames.
*/
public int getId() {
return id;
}
/**
* Returns the image rotation, indicating the counter-clockwise rotation from the upright orientation. Has the same meaning as in {@link Display#getRotation()}.
* <p>
* Since the camera may deliver images that are rotated (e.g., if the user holds the device upside down), specifying the rotation with the image indicates how to make the image be upright, if necessary. Some detectors may rely upon rotating the image before attempting detection, whereas others may not. In preserving the original image from the camera along with this value, the detector may choose whether to make this correction (and to assume the associated cost).
* <p>
* However, note that the detector is expected to report detection position coordinates that are relative to the upright version of the image (whether or not the image was actually rotated by the detector). The {@link Detector} will always deliver frame metadata to the {@link Detector.Processor} that indicates the dimensions and orientation of an unrotated, upright frame.
*
* @return one of {@link Frame#ROTATION_0}, {@link Frame#ROTATION_90}, {@link Frame#ROTATION_180}, {@link Frame#ROTATION_270}.
*/
public int getRotation() {
return rotation;
}
/**
* Returns the timestamp, in milliseconds.
* <p>
* A frame source such as a live video camera or a video player is expected to assign timestamps in a way that makes sense for the medium. For example, live video may use the capture time of each frame, whereas a video player may use the elapsed time to the frame within the video. Timestamps should be in monotonically increasing order, to indicate the passage of time.
*/
public long getTimestampMillis() {
return timestampMillis;
}
/**
* Returns the frame width.
*/
public int getWidth() {
return width;
}
@PublicApi(exclude = true)
public FrameMetadataParcel createParcel() {
FrameMetadataParcel parcel = new FrameMetadataParcel();
parcel.width = width;
parcel.height = height;
parcel.id = id;
parcel.timestampMillis = timestampMillis;
parcel.rotation = rotation;
return parcel;
}
@PublicApi(exclude = true)
public Metadata withRotationAppliedToSize() {
Metadata metadata = new Metadata(this);
if (metadata.rotation % 2 != 0) {
metadata.width = height;
metadata.height = width;
}
metadata.rotation = 0;
return metadata;
}
}
}

View File

@ -15,7 +15,7 @@ import com.google.android.gms.vision.barcode.internal.client.INativeBarcodeDetec
import org.microg.gms.vision.barcode.BarcodeDetector
@Keep
class ChimeraNativeBarcodeDetectorCreator : INativeBarcodeDetectorCreator.Stub() {
class DynamiteNativeBarcodeDetectorCreator : INativeBarcodeDetectorCreator.Stub() {
override fun create(context: IObjectWrapper, options: BarcodeDetectorOptions): INativeBarcodeDetector {
return BarcodeDetector(context.unwrap<Context>()!!, options)
}

View File

@ -30,4 +30,5 @@ description = 'microG implementation of play-services-vision'
dependencies {
api project(':play-services-base')
api project(':play-services-vision-api')
api project(':play-services-vision-common')
}

View File

@ -0,0 +1,135 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.vision.barcode;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.IInterface;
import android.os.RemoteException;
import android.util.SparseArray;
import com.google.android.gms.dynamic.ObjectWrapper;
import com.google.android.gms.vision.Detector;
import com.google.android.gms.vision.Frame;
import com.google.android.gms.vision.barcode.internal.client.BarcodeDetectorOptions;
import com.google.android.gms.vision.barcode.internal.client.INativeBarcodeDetector;
import com.google.android.gms.vision.barcode.internal.client.INativeBarcodeDetectorCreator;
import com.google.android.gms.vision.internal.FrameMetadataParcel;
import org.microg.gms.common.PublicApi;
/**
* Recognizes barcodes (in a variety of 1D and 2D formats) in a supplied {@link Frame}.
* <p>
* Build new BarcodeDetector instances using {@link BarcodeDetector.Builder}. By default, BarcodeDetector searches for barcodes in every supported format. For the best performance it is highly recommended that you specify a narrower set of barcode formats to detect.
* <p>
* Recognition results are returned by {@link #detect(Frame)} as Barcode instances.
*/
@PublicApi
public class BarcodeDetector extends Detector<Barcode> {
private INativeBarcodeDetector remote;
private BarcodeDetector(INativeBarcodeDetector remote) {
this.remote = remote;
}
/**
* Recognizes barcodes in the supplied {@link Frame}.
*
* @return mapping of int to {@link Barcode}, where the int domain represents an opaque ID for the barcode. Identical barcodes (as determined by their raw value) will have the same ID across frames.
*/
@Override
public SparseArray<Barcode> detect(Frame frame) {
if (frame == null) throw new IllegalArgumentException("No frame supplied.");
SparseArray<Barcode> result = new SparseArray<>();
if (remote != null) {
FrameMetadataParcel metadataParcel = frame.getMetadata().createParcel();
Barcode[] barcodes = null;
if (frame.getBitmap() != null) {
try {
barcodes = remote.detectBitmap(ObjectWrapper.wrap(frame.getBitmap()), metadataParcel);
} catch (RemoteException e) {
// Ignore
}
} else {
try {
barcodes = remote.detectBytes(ObjectWrapper.wrap(frame.getGrayscaleImageData()), metadataParcel);
} catch (RemoteException e) {
// Ignore
}
}
if (barcodes != null) {
for (Barcode barcode : barcodes) {
result.append(barcode.rawValue.hashCode(), barcode);
}
}
}
return result;
}
@Override
public boolean isOperational() {
return remote != null && super.isOperational();
}
@Override
public void release() {
super.release();
try {
remote.close();
} catch (RemoteException e) {
// Ignore
}
remote = null;
}
/**
* Barcode detector builder.
*/
public static class Builder {
private Context context;
private BarcodeDetectorOptions options = new BarcodeDetectorOptions();
/**
* Builder for BarcodeDetector.
*/
public Builder(Context context) {
this.context = context;
}
/**
* Bit mask (containing values like {@link Barcode#QR_CODE} and so on) that selects which formats this barcode detector should recognize.
* <p>
* By default, the detector will recognize all supported formats. This corresponds to the special {@link Barcode#ALL_FORMATS} constant.
*/
public Builder setBarcodeFormats(int formats) {
options.formats = formats;
return this;
}
/**
* Builds a barcode detector instance using the provided settings. If the underlying native implementation is unavailable (e.g. hasn't been downloaded yet), the detector will always return an empty result set. In this case, it will report that it is non-operational via {@link BarcodeDetector#isOperational()}.
* <p>
* Note that this method may cause blocking disk reads and should not be called on an application's main thread. To avoid blocking the main thread, consider moving Detector construction to a background thread using {@link android.os.AsyncTask}. Enable {@link android.os.StrictMode} to automatically detect blocking operations on the main thread.
*
* @return new {@link BarcodeDetector} instance
*/
public BarcodeDetector build() {
// TODO: Actually implement dynamite or load from remote
INativeBarcodeDetector remote = null;
try {
Class<?> clazz = Class.forName("com.google.android.gms.vision.barcode.ChimeraNativeBarcodeDetectorCreator");
Object instance = clazz.getConstructor().newInstance();
INativeBarcodeDetectorCreator creator = INativeBarcodeDetectorCreator.Stub.asInterface(((IInterface) instance).asBinder());
remote = creator.create(ObjectWrapper.wrap(context), options);
} catch (Exception e) {
// Ignore
}
return new BarcodeDetector(remote);
}
}
}

View File

@ -10,6 +10,7 @@ include ':play-services-iid-api'
include ':play-services-location-api'
include ':play-services-nearby-api'
include ':play-services-vision-api'
include ':play-services-vision-common-api'
include ':play-services-wearable-api'
include ':play-services-api'
@ -45,6 +46,7 @@ include ':play-services-iid'
include ':play-services-location'
include ':play-services-nearby'
include ':play-services-vision'
include ':play-services-vision-common'
include ':play-services-wearable'
include ':play-services'