The NV21 format consists of a single byte array containing the Y, U and V values. For an 83 | * image of size S, the first S positions of the array contain all the Y values. The remaining 84 | * positions contain interleaved V and U values. U and V are subsampled by a factor of 2 in both 85 | * dimensions, so there are S/4 U values and S/4 V values. In summary, the NV21 array will contain 86 | * S Y values followed by S/4 VU values: YYYYYYYYYYYYYY(...)YVUVUVUVU(...)VU 87 | * 88 | *
YUV_420_888 is a generic format that can describe any YUV image where U and V are subsampled 89 | * by a factor of 2 in both dimensions. {@link Image#getPlanes} returns an array with the Y, U and 90 | * V planes. The Y plane is guaranteed not to be interleaved, so we can just copy its values into 91 | * the first part of the NV21 array. The U and V planes may already have the representation in the 92 | * NV21 format. This happens if the planes share the same buffer, the V buffer is one position 93 | * before the U buffer and the planes have a pixelStride of 2. If this is case, we can just copy 94 | * them to the NV21 array. 95 | */ 96 | private static ByteBuffer yuv420ThreePlanesToNV21( 97 | Plane[] yuv420888planes, int width, int height) { 98 | int imageSize = width * height; 99 | byte[] out = new byte[imageSize + 2 * (imageSize / 4)]; 100 | 101 | if (areUVPlanesNV21(yuv420888planes, width, height)) { 102 | // Copy the Y values. 103 | yuv420888planes[0].getBuffer().get(out, 0, imageSize); 104 | 105 | ByteBuffer uBuffer = yuv420888planes[1].getBuffer(); 106 | ByteBuffer vBuffer = yuv420888planes[2].getBuffer(); 107 | // Get the first V value from the V buffer, since the U buffer does not contain it. 108 | vBuffer.get(out, imageSize, 1); 109 | // Copy the first U value and the remaining VU values from the U buffer. 110 | uBuffer.get(out, imageSize + 1, 2 * imageSize / 4 - 1); 111 | } else { 112 | // Fallback to copying the UV values one by one, which is slower but also works. 113 | // Unpack Y. 114 | unpackPlane(yuv420888planes[0], width, height, out, 0, 1); 115 | // Unpack U. 116 | unpackPlane(yuv420888planes[1], width, height, out, imageSize + 1, 2); 117 | // Unpack V. 118 | unpackPlane(yuv420888planes[2], width, height, out, imageSize, 2); 119 | } 120 | 121 | return ByteBuffer.wrap(out); 122 | } 123 | 124 | /** Checks if the UV plane buffers of a YUV_420_888 image are in the NV21 format. */ 125 | private static boolean areUVPlanesNV21(Plane[] planes, int width, int height) { 126 | int imageSize = width * height; 127 | 128 | ByteBuffer uBuffer = planes[1].getBuffer(); 129 | ByteBuffer vBuffer = planes[2].getBuffer(); 130 | 131 | // Backup buffer properties. 132 | int vBufferPosition = vBuffer.position(); 133 | int uBufferLimit = uBuffer.limit(); 134 | 135 | // Advance the V buffer by 1 byte, since the U buffer will not contain the first V value. 136 | vBuffer.position(vBufferPosition + 1); 137 | // Chop off the last byte of the U buffer, since the V buffer will not contain the last U value. 138 | uBuffer.limit(uBufferLimit - 1); 139 | 140 | // Check that the buffers are equal and have the expected number of elements. 141 | boolean areNV21 = 142 | (vBuffer.remaining() == (2 * imageSize / 4 - 2)) && (vBuffer.compareTo(uBuffer) == 0); 143 | 144 | // Restore buffers to their initial state. 145 | vBuffer.position(vBufferPosition); 146 | uBuffer.limit(uBufferLimit); 147 | 148 | return areNV21; 149 | } 150 | 151 | /** 152 | * Unpack an image plane into a byte array. 153 | * 154 | *
The input plane data will be copied in 'out', starting at 'offset' and every pixel will be
155 | * spaced by 'pixelStride'. Note that there is no row padding on the output.
156 | */
157 | private static void unpackPlane(
158 | Plane plane, int width, int height, byte[] out, int offset, int pixelStride) {
159 | ByteBuffer buffer = plane.getBuffer();
160 | buffer.rewind();
161 |
162 | // Compute the size of the current plane.
163 | // We assume that it has the aspect ratio as the original image.
164 | int numRow = (buffer.limit() + plane.getRowStride() - 1) / plane.getRowStride();
165 | if (numRow == 0) {
166 | return;
167 | }
168 | int scaleFactor = height / numRow;
169 | int numCol = width / scaleFactor;
170 |
171 | // Extract the data in the output buffer.
172 | int outputPos = offset;
173 | int rowStart = 0;
174 | for (int row = 0; row < numRow; row++) {
175 | int inputPos = rowStart;
176 | for (int col = 0; col < numCol; col++) {
177 | out[outputPos] = buffer.get(inputPos);
178 | outputPos += pixelStride;
179 | inputPos += plane.getPixelStride();
180 | }
181 | rowStart += plane.getRowStride();
182 | }
183 | }
184 | }
185 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/buildtoapp/mlbarcodescanner/CameraImageGraphic.java:
--------------------------------------------------------------------------------
1 | package com.buildtoapp.mlbarcodescanner;
2 |
3 | import android.graphics.Bitmap;
4 | import android.graphics.Canvas;
5 |
6 | /** Draw camera image to background. */
7 | class CameraImageGraphic extends GraphicOverlay.Graphic {
8 |
9 | private final Bitmap bitmap;
10 |
11 | public CameraImageGraphic(GraphicOverlay overlay, Bitmap bitmap) {
12 | super(overlay);
13 | this.bitmap = bitmap;
14 | }
15 |
16 | @Override
17 | public void draw(Canvas canvas) {
18 | canvas.drawBitmap(bitmap, getTransformationMatrix(), null);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/buildtoapp/mlbarcodescanner/CameraXViewModel.java:
--------------------------------------------------------------------------------
1 | package com.buildtoapp.mlbarcodescanner;
2 |
3 | import android.app.Application;
4 | import android.util.Log;
5 |
6 | import androidx.annotation.NonNull;
7 | import androidx.camera.lifecycle.ProcessCameraProvider;
8 | import androidx.core.content.ContextCompat;
9 | import androidx.lifecycle.AndroidViewModel;
10 | import androidx.lifecycle.LiveData;
11 | import androidx.lifecycle.MutableLiveData;
12 |
13 | import com.google.common.util.concurrent.ListenableFuture;
14 |
15 | import java.util.concurrent.ExecutionException;
16 |
17 | /** View model for interacting with CameraX. */
18 | public final class CameraXViewModel extends AndroidViewModel {
19 |
20 | private static final String TAG = "CameraXViewModel";
21 | private MutableLiveData Supports scaling and mirroring of the graphics relative the camera's preview properties. The
21 | * idea is that detection items are expressed in terms of an image size, but need to be scaled up
22 | * to the full view size, and also mirrored in the case of the front-facing camera.
23 | *
24 | * Associated {@link Graphic} items should use the following methods to convert to view
25 | * coordinates for the graphics that are drawn:
26 | *
27 | * Runnables that have already started to execute will continue.
42 | */
43 | public void shutdown() {
44 | shutdown.set(true);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/buildtoapp/mlbarcodescanner/VisionImageProcessor.kt:
--------------------------------------------------------------------------------
1 | package com.buildtoapp.mlbarcodescanner
2 |
3 | import androidx.camera.core.ImageProxy
4 | import com.google.mlkit.common.MlKitException
5 |
6 | /**
7 | * An interface to process the images with different vision detectors and custom image models.
8 | */
9 | internal interface VisionImageProcessor {
10 | /**
11 | * Processes ImageProxy image data, e.g. used for CameraX live preview case.
12 | */
13 | @Throws(MlKitException::class)
14 | fun processImageProxy(image: ImageProxy, graphicOverlay: GraphicOverlay)
15 |
16 | /**
17 | * Stops the underlying machine learning model and release resources.
18 | */
19 | fun stop()
20 | }
--------------------------------------------------------------------------------
/lib/src/main/java/com/buildtoapp/mlbarcodescanner/VisionProcessorBase.kt:
--------------------------------------------------------------------------------
1 | package com.buildtoapp.mlbarcodescanner
2 |
3 | import android.content.Context
4 | import android.graphics.Bitmap
5 | import androidx.annotation.GuardedBy
6 | import androidx.camera.core.ExperimentalGetImage
7 | import androidx.camera.core.ImageProxy
8 | import com.google.android.gms.tasks.Task
9 | import com.google.android.gms.tasks.TaskExecutors
10 | import com.google.android.gms.tasks.Tasks
11 | import com.google.android.odml.image.MediaMlImageBuilder
12 | import com.google.android.odml.image.MlImage
13 | import com.google.mlkit.common.MlKitException
14 | import com.google.mlkit.vision.common.InputImage
15 | import java.nio.ByteBuffer
16 |
17 | /**
18 | * Abstract base class for ML Kit frame processors. Subclasses need to implement {@link
19 | * #onSuccess(T, FrameMetadata, GraphicOverlay)} to define what they want to with the detection
20 | * results and {@link #detectInImage(VisionImage)} to specify the detector object.
21 | *
22 | * @param
28 | *
33 | */
34 | public class GraphicOverlay extends View {
35 | private final Object lock = new Object();
36 | private final List graphics = new ArrayList<>();
37 | // Matrix for transforming from image coordinates to overlay view coordinates.
38 | private final Matrix transformationMatrix = new Matrix();
39 |
40 | private int imageWidth;
41 | private int imageHeight;
42 | // The factor of overlay View size to image size. Anything in the image coordinates need to be
43 | // scaled by this amount to fit with the area of overlay View.
44 | private float scaleFactor = 1.0f;
45 | // The number of horizontal pixels needed to be cropped on each side to fit the image with the
46 | // area of overlay View after scaling.
47 | private float postScaleWidthOffset;
48 | // The number of vertical pixels needed to be cropped on each side to fit the image with the
49 | // area of overlay View after scaling.
50 | private float postScaleHeightOffset;
51 | private boolean isImageFlipped;
52 | private boolean needUpdateTransformation = true;
53 |
54 | /**
55 | * Base class for a custom graphics object to be rendered within the graphic overlay. Subclass
56 | * this and implement the {@link Graphic#draw(Canvas)} method to define the graphics element. Add
57 | * instances to the overlay using {@link GraphicOverlay#add(Graphic)}.
58 | */
59 | public abstract static class Graphic {
60 | private GraphicOverlay overlay;
61 |
62 | public Graphic(GraphicOverlay overlay) {
63 | this.overlay = overlay;
64 | }
65 |
66 | /**
67 | * Draw the graphic on the supplied canvas. Drawing should use the following methods to convert
68 | * to view coordinates for the graphics that are drawn:
69 | *
70 | *
71 | *
76 | *
77 | * @param canvas drawing canvas
78 | */
79 | public abstract void draw(Canvas canvas);
80 |
81 | protected void drawRect(
82 | Canvas canvas, float left, float top, float right, float bottom, Paint paint) {
83 | canvas.drawRect(left, top, right, bottom, paint);
84 | }
85 |
86 | protected void drawText(Canvas canvas, String text, float x, float y, Paint paint) {
87 | canvas.drawText(text, x, y, paint);
88 | }
89 |
90 | /** Adjusts the supplied value from the image scale to the view scale. */
91 | public float scale(float imagePixel) {
92 | return imagePixel * overlay.scaleFactor;
93 | }
94 |
95 | /** Returns the application context of the app. */
96 | public Context getApplicationContext() {
97 | return overlay.getContext().getApplicationContext();
98 | }
99 |
100 | public boolean isImageFlipped() {
101 | return overlay.isImageFlipped;
102 | }
103 |
104 | /**
105 | * Adjusts the x coordinate from the image's coordinate system to the view coordinate system.
106 | */
107 | public float translateX(float x) {
108 | if (overlay.isImageFlipped) {
109 | return overlay.getWidth() - (scale(x) - overlay.postScaleWidthOffset);
110 | } else {
111 | return scale(x) - overlay.postScaleWidthOffset;
112 | }
113 | }
114 |
115 | /**
116 | * Adjusts the y coordinate from the image's coordinate system to the view coordinate system.
117 | */
118 | public float translateY(float y) {
119 | return scale(y) - overlay.postScaleHeightOffset;
120 | }
121 |
122 | /**
123 | * Returns a {@link Matrix} for transforming from image coordinates to overlay view coordinates.
124 | */
125 | public Matrix getTransformationMatrix() {
126 | return overlay.transformationMatrix;
127 | }
128 |
129 | public void postInvalidate() {
130 | overlay.postInvalidate();
131 | }
132 | }
133 |
134 | public GraphicOverlay(Context context, AttributeSet attrs) {
135 | super(context, attrs);
136 | addOnLayoutChangeListener(
137 | (view, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->
138 | needUpdateTransformation = true);
139 | }
140 |
141 | /** Removes all graphics from the overlay. */
142 | public void clear() {
143 | synchronized (lock) {
144 | graphics.clear();
145 | }
146 | postInvalidate();
147 | }
148 |
149 | /** Adds a graphic to the overlay. */
150 | public void add(Graphic graphic) {
151 | synchronized (lock) {
152 | graphics.add(graphic);
153 | }
154 | }
155 |
156 | /** Removes a graphic from the overlay. */
157 | public void remove(Graphic graphic) {
158 | synchronized (lock) {
159 | graphics.remove(graphic);
160 | }
161 | postInvalidate();
162 | }
163 |
164 | /**
165 | * Sets the source information of the image being processed by detectors, including size and
166 | * whether it is flipped, which informs how to transform image coordinates later.
167 | *
168 | * @param imageWidth the width of the image sent to ML Kit detectors
169 | * @param imageHeight the height of the image sent to ML Kit detectors
170 | * @param isFlipped whether the image is flipped. Should set it to true when the image is from the
171 | * front camera.
172 | */
173 | public void setImageSourceInfo(int imageWidth, int imageHeight, boolean isFlipped) {
174 | Preconditions.checkState(imageWidth > 0, "image width must be positive");
175 | Preconditions.checkState(imageHeight > 0, "image height must be positive");
176 | synchronized (lock) {
177 | this.imageWidth = imageWidth;
178 | this.imageHeight = imageHeight;
179 | this.isImageFlipped = isFlipped;
180 | needUpdateTransformation = true;
181 | }
182 | postInvalidate();
183 | }
184 |
185 | public int getImageWidth() {
186 | return imageWidth;
187 | }
188 |
189 | public int getImageHeight() {
190 | return imageHeight;
191 | }
192 |
193 | private void updateTransformationIfNeeded() {
194 | if (!needUpdateTransformation || imageWidth <= 0 || imageHeight <= 0) {
195 | return;
196 | }
197 | float viewAspectRatio = (float) getWidth() / getHeight();
198 | float imageAspectRatio = (float) imageWidth / imageHeight;
199 | postScaleWidthOffset = 0;
200 | postScaleHeightOffset = 0;
201 | if (viewAspectRatio > imageAspectRatio) {
202 | // The image needs to be vertically cropped to be displayed in this view.
203 | scaleFactor = (float) getWidth() / imageWidth;
204 | postScaleHeightOffset = ((float) getWidth() / imageAspectRatio - getHeight()) / 2;
205 | } else {
206 | // The image needs to be horizontally cropped to be displayed in this view.
207 | scaleFactor = (float) getHeight() / imageHeight;
208 | postScaleWidthOffset = ((float) getHeight() * imageAspectRatio - getWidth()) / 2;
209 | }
210 |
211 | transformationMatrix.reset();
212 | transformationMatrix.setScale(scaleFactor, scaleFactor);
213 | transformationMatrix.postTranslate(-postScaleWidthOffset, -postScaleHeightOffset);
214 |
215 | if (isImageFlipped) {
216 | transformationMatrix.postScale(-1f, 1f, getWidth() / 2f, getHeight() / 2f);
217 | }
218 |
219 | needUpdateTransformation = false;
220 | }
221 |
222 | /** Draws the overlay with its associated graphic objects. */
223 | @Override
224 | protected void onDraw(Canvas canvas) {
225 | super.onDraw(canvas);
226 |
227 | synchronized (lock) {
228 | updateTransformationIfNeeded();
229 |
230 | for (Graphic graphic : graphics) {
231 | graphic.draw(canvas);
232 | }
233 | }
234 | }
235 | }
236 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/buildtoapp/mlbarcodescanner/MLBarcodeScanner.kt:
--------------------------------------------------------------------------------
1 | package com.buildtoapp.mlbarcodescanner
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.util.Log
6 | import android.util.Size
7 | import androidx.camera.core.CameraSelector
8 | import androidx.camera.core.ImageAnalysis
9 | import androidx.camera.core.ImageProxy
10 | import androidx.camera.core.Preview
11 | import androidx.camera.lifecycle.ProcessCameraProvider
12 | import androidx.camera.view.PreviewView
13 | import androidx.core.content.ContextCompat
14 | import androidx.lifecycle.DefaultLifecycleObserver
15 | import androidx.lifecycle.LifecycleOwner
16 | import androidx.lifecycle.ViewModelProvider
17 | import androidx.lifecycle.ViewModelStoreOwner
18 | import com.google.mlkit.common.MlKitException
19 | import com.google.mlkit.vision.barcode.common.Barcode
20 |
21 | /**
22 | * Delegate class to wrap all functionalities of barcode scanning and handling it's resources in a lifecycle aware manner
23 | *
24 | * @param callback listen to new scanned barcode
25 | * @param focusBoxSize width/height of the focus box in pixels (box must be square)
26 | * @param graphicOverlay overlay graphic to be drawn on screen to recognize barcode
27 | * @param previewView camera preview object in your view
28 | * @param lifecycleOwner lifecycle owner of your view (viewLifecycleOwner in fragment and this in activity)
29 | * @param context ui context
30 | * @param drawOverlay if set to true, will display a rectangle around detected barcode (default to true)
31 | * @param drawBanner if set to true, will display detected barcode value on top of it's rectangle (default to false)
32 | * @param targetResolution resolution of the camera view (default to 768 * 1024)
33 | * @param supportedBarcodeFormats list of all supported barcode formats (default to all)
34 | */
35 | class MLBarcodeScanner(
36 | private val callback: MLBarcodeCallback,
37 | private val context: Context,
38 | private val lifecycleOwner: LifecycleOwner,
39 | private val focusBoxSize: Int,
40 | private val graphicOverlay: GraphicOverlay,
41 | private val previewView: PreviewView,
42 | private val drawOverlay: Boolean = true,
43 | private val drawBanner: Boolean = false,
44 | private val targetResolution: Size = Size(768, 1024),
45 | private val supportedBarcodeFormats: List