• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /* Copyright 2017 The TensorFlow Authors. All Rights Reserved.
2 
3 Licensed under the Apache License, Version 2.0 (the "License");
4 you may not use this file except in compliance with the License.
5 You may obtain a copy of the License at
6 
7     http://www.apache.org/licenses/LICENSE-2.0
8 
9 Unless required by applicable law or agreed to in writing, software
10 distributed under the License is distributed on an "AS IS" BASIS,
11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 See the License for the specific language governing permissions and
13 limitations under the License.
14 ==============================================================================*/
15 
16 package com.example.android.tflitecamerademo;
17 
18 import android.app.Activity;
19 import android.app.AlertDialog;
20 import android.app.Dialog;
21 import android.app.DialogFragment;
22 import android.app.Fragment;
23 import android.content.Context;
24 import android.content.DialogInterface;
25 import android.content.pm.PackageInfo;
26 import android.content.pm.PackageManager;
27 import android.content.res.Configuration;
28 import android.graphics.Bitmap;
29 import android.graphics.ImageFormat;
30 import android.graphics.Matrix;
31 import android.graphics.Point;
32 import android.graphics.RectF;
33 import android.graphics.SurfaceTexture;
34 import android.hardware.camera2.CameraAccessException;
35 import android.hardware.camera2.CameraCaptureSession;
36 import android.hardware.camera2.CameraCharacteristics;
37 import android.hardware.camera2.CameraDevice;
38 import android.hardware.camera2.CameraManager;
39 import android.hardware.camera2.CaptureRequest;
40 import android.hardware.camera2.CaptureResult;
41 import android.hardware.camera2.TotalCaptureResult;
42 import android.hardware.camera2.params.StreamConfigurationMap;
43 import android.media.ImageReader;
44 import android.os.Bundle;
45 import android.os.Handler;
46 import android.os.HandlerThread;
47 import android.support.annotation.NonNull;
48 import android.support.v4.content.ContextCompat;
49 import android.text.SpannableString;
50 import android.text.SpannableStringBuilder;
51 import android.util.Log;
52 import android.util.Size;
53 import android.view.LayoutInflater;
54 import android.view.Surface;
55 import android.view.TextureView;
56 import android.view.View;
57 import android.view.ViewGroup;
58 import android.widget.AdapterView;
59 import android.widget.ArrayAdapter;
60 import android.widget.ListView;
61 import android.widget.NumberPicker;
62 import android.widget.TextView;
63 import android.widget.Toast;
64 import android.support.v13.app.FragmentCompat;
65 import java.io.IOException;
66 import java.util.ArrayList;
67 import java.util.Arrays;
68 import java.util.Collections;
69 import java.util.Comparator;
70 import java.util.List;
71 import java.util.concurrent.Semaphore;
72 import java.util.concurrent.TimeUnit;
73 
74 /** Basic fragments for the Camera. */
75 public class Camera2BasicFragment extends Fragment
76     implements FragmentCompat.OnRequestPermissionsResultCallback {
77 
78   /** Tag for the {@link Log}. */
79   private static final String TAG = "TfLiteCameraDemo";
80 
81   private static final String FRAGMENT_DIALOG = "dialog";
82 
83   private static final String HANDLE_THREAD_NAME = "CameraBackground";
84 
85   private static final int PERMISSIONS_REQUEST_CODE = 1;
86 
87   private final Object lock = new Object();
88   private boolean runClassifier = false;
89   private boolean checkedPermissions = false;
90   private TextView textView;
91   private NumberPicker np;
92   private ImageClassifier classifier;
93   private ListView deviceView;
94   private ListView modelView;
95 
96 
97   /** Max preview width that is guaranteed by Camera2 API */
98   private static final int MAX_PREVIEW_WIDTH = 1920;
99 
100   /** Max preview height that is guaranteed by Camera2 API */
101   private static final int MAX_PREVIEW_HEIGHT = 1080;
102 
103   /**
104    * {@link TextureView.SurfaceTextureListener} handles several lifecycle events on a {@link
105    * TextureView}.
106    */
107   private final TextureView.SurfaceTextureListener surfaceTextureListener =
108       new TextureView.SurfaceTextureListener() {
109 
110         @Override
111         public void onSurfaceTextureAvailable(SurfaceTexture texture, int width, int height) {
112           openCamera(width, height);
113         }
114 
115         @Override
116         public void onSurfaceTextureSizeChanged(SurfaceTexture texture, int width, int height) {
117           configureTransform(width, height);
118         }
119 
120         @Override
121         public boolean onSurfaceTextureDestroyed(SurfaceTexture texture) {
122           return true;
123         }
124 
125         @Override
126         public void onSurfaceTextureUpdated(SurfaceTexture texture) {}
127       };
128 
129   // Model parameter constants.
130   private String gpu;
131   private String cpu;
132   private String nnApi;
133   private String mobilenetV1Quant;
134   private String mobilenetV1Float;
135 
136 
137 
138   /** ID of the current {@link CameraDevice}. */
139   private String cameraId;
140 
141   /** An {@link AutoFitTextureView} for camera preview. */
142   private AutoFitTextureView textureView;
143 
144   /** A {@link CameraCaptureSession } for camera preview. */
145   private CameraCaptureSession captureSession;
146 
147   /** A reference to the opened {@link CameraDevice}. */
148   private CameraDevice cameraDevice;
149 
150   /** The {@link android.util.Size} of camera preview. */
151   private Size previewSize;
152 
153   /** {@link CameraDevice.StateCallback} is called when {@link CameraDevice} changes its state. */
154   private final CameraDevice.StateCallback stateCallback =
155       new CameraDevice.StateCallback() {
156 
157         @Override
158         public void onOpened(@NonNull CameraDevice currentCameraDevice) {
159           // This method is called when the camera is opened.  We start camera preview here.
160           cameraOpenCloseLock.release();
161           cameraDevice = currentCameraDevice;
162           createCameraPreviewSession();
163         }
164 
165         @Override
166         public void onDisconnected(@NonNull CameraDevice currentCameraDevice) {
167           cameraOpenCloseLock.release();
168           currentCameraDevice.close();
169           cameraDevice = null;
170         }
171 
172         @Override
173         public void onError(@NonNull CameraDevice currentCameraDevice, int error) {
174           cameraOpenCloseLock.release();
175           currentCameraDevice.close();
176           cameraDevice = null;
177           Activity activity = getActivity();
178           if (null != activity) {
179             activity.finish();
180           }
181         }
182       };
183 
184   private ArrayList<String> deviceStrings = new ArrayList<String>();
185   private ArrayList<String> modelStrings = new ArrayList<String>();
186 
187   /** Current indices of device and model. */
188   int currentDevice = -1;
189 
190   int currentModel = -1;
191 
192   int currentNumThreads = -1;
193 
194   /** An additional thread for running tasks that shouldn't block the UI. */
195   private HandlerThread backgroundThread;
196 
197   /** A {@link Handler} for running tasks in the background. */
198   private Handler backgroundHandler;
199 
200   /** An {@link ImageReader} that handles image capture. */
201   private ImageReader imageReader;
202 
203   /** {@link CaptureRequest.Builder} for the camera preview */
204   private CaptureRequest.Builder previewRequestBuilder;
205 
206   /** {@link CaptureRequest} generated by {@link #previewRequestBuilder} */
207   private CaptureRequest previewRequest;
208 
209   /** A {@link Semaphore} to prevent the app from exiting before closing the camera. */
210   private Semaphore cameraOpenCloseLock = new Semaphore(1);
211 
212   /** A {@link CameraCaptureSession.CaptureCallback} that handles events related to capture. */
213   private CameraCaptureSession.CaptureCallback captureCallback =
214       new CameraCaptureSession.CaptureCallback() {
215 
216         @Override
217         public void onCaptureProgressed(
218             @NonNull CameraCaptureSession session,
219             @NonNull CaptureRequest request,
220             @NonNull CaptureResult partialResult) {}
221 
222         @Override
223         public void onCaptureCompleted(
224             @NonNull CameraCaptureSession session,
225             @NonNull CaptureRequest request,
226             @NonNull TotalCaptureResult result) {}
227       };
228 
229   /**
230    * Shows a {@link Toast} on the UI thread for the classification results.
231    *
232    * @param text The message to show
233    */
showToast(String s)234   private void showToast(String s) {
235     SpannableStringBuilder builder = new SpannableStringBuilder();
236     SpannableString str1 = new SpannableString(s);
237     builder.append(str1);
238     showToast(builder);
239   }
240 
showToast(SpannableStringBuilder builder)241   private void showToast(SpannableStringBuilder builder) {
242     final Activity activity = getActivity();
243     if (activity != null) {
244       activity.runOnUiThread(
245           new Runnable() {
246             @Override
247             public void run() {
248               textView.setText(builder, TextView.BufferType.SPANNABLE);
249             }
250           });
251     }
252   }
253 
254   /**
255    * Resizes image.
256    *
257    * Attempting to use too large a preview size could  exceed the camera bus' bandwidth limitation,
258    * resulting in gorgeous previews but the storage of garbage capture data.
259    *
260    * Given {@code choices} of {@code Size}s supported by a camera, choose the smallest one that is
261    * at least as large as the respective texture view size, and that is at most as large as the
262    * respective max size, and whose aspect ratio matches with the specified value. If such size
263    * doesn't exist, choose the largest one that is at most as large as the respective max size, and
264    * whose aspect ratio matches with the specified value.
265    *
266    * @param choices The list of sizes that the camera supports for the intended output class
267    * @param textureViewWidth The width of the texture view relative to sensor coordinate
268    * @param textureViewHeight The height of the texture view relative to sensor coordinate
269    * @param maxWidth The maximum width that can be chosen
270    * @param maxHeight The maximum height that can be chosen
271    * @param aspectRatio The aspect ratio
272    * @return The optimal {@code Size}, or an arbitrary one if none were big enough
273    */
chooseOptimalSize( Size[] choices, int textureViewWidth, int textureViewHeight, int maxWidth, int maxHeight, Size aspectRatio)274   private static Size chooseOptimalSize(
275       Size[] choices,
276       int textureViewWidth,
277       int textureViewHeight,
278       int maxWidth,
279       int maxHeight,
280       Size aspectRatio) {
281 
282     // Collect the supported resolutions that are at least as big as the preview Surface
283     List<Size> bigEnough = new ArrayList<>();
284     // Collect the supported resolutions that are smaller than the preview Surface
285     List<Size> notBigEnough = new ArrayList<>();
286     int w = aspectRatio.getWidth();
287     int h = aspectRatio.getHeight();
288     for (Size option : choices) {
289       if (option.getWidth() <= maxWidth
290           && option.getHeight() <= maxHeight
291           && option.getHeight() == option.getWidth() * h / w) {
292         if (option.getWidth() >= textureViewWidth && option.getHeight() >= textureViewHeight) {
293           bigEnough.add(option);
294         } else {
295           notBigEnough.add(option);
296         }
297       }
298     }
299 
300     // Pick the smallest of those big enough. If there is no one big enough, pick the
301     // largest of those not big enough.
302     if (bigEnough.size() > 0) {
303       return Collections.min(bigEnough, new CompareSizesByArea());
304     } else if (notBigEnough.size() > 0) {
305       return Collections.max(notBigEnough, new CompareSizesByArea());
306     } else {
307       Log.e(TAG, "Couldn't find any suitable preview size");
308       return choices[0];
309     }
310   }
311 
newInstance()312   public static Camera2BasicFragment newInstance() {
313     return new Camera2BasicFragment();
314   }
315 
316   /** Layout the preview and buttons. */
317   @Override
onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)318   public View onCreateView(
319       LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
320     return inflater.inflate(R.layout.fragment_camera2_basic, container, false);
321   }
322 
updateActiveModel()323   private void updateActiveModel() {
324     // Get UI information before delegating to background
325     final int modelIndex = modelView.getCheckedItemPosition();
326     final int deviceIndex = deviceView.getCheckedItemPosition();
327     final int numThreads = np.getValue();
328 
329     backgroundHandler.post(() -> {
330       if (modelIndex == currentModel && deviceIndex == currentDevice
331               && numThreads == currentNumThreads) {
332         return;
333       }
334       currentModel = modelIndex;
335       currentDevice = deviceIndex;
336       currentNumThreads = numThreads;
337 
338       // Disable classifier while updating
339       if (classifier != null) {
340         classifier.close();
341         classifier = null;
342       }
343 
344       // Lookup names of parameters.
345       String model = modelStrings.get(modelIndex);
346       String device = deviceStrings.get(deviceIndex);
347 
348       Log.i(TAG, "Changing model to " + model + " device " + device);
349 
350       // Try to load model.
351       try {
352         if (model.equals(mobilenetV1Quant)) {
353           classifier = new ImageClassifierQuantizedMobileNet(getActivity());
354         } else if (model.equals(mobilenetV1Float)) {
355           classifier = new ImageClassifierFloatMobileNet(getActivity());
356         } else {
357           showToast("Failed to load model");
358         }
359       } catch (IOException e) {
360         Log.d(TAG, "Failed to load", e);
361         classifier = null;
362       }
363 
364       // Customize the interpreter to the type of device we want to use.
365       if (classifier == null) {
366         return;
367       }
368       classifier.setNumThreads(numThreads);
369       if (device.equals(cpu)) {
370       } else if (device.equals(gpu)) {
371         if (!GpuDelegateHelper.isGpuDelegateAvailable()) {
372           showToast("gpu not in this build.");
373           classifier = null;
374         } else if (model.equals(mobilenetV1Quant)) {
375           showToast("gpu requires float model.");
376           classifier = null;
377         } else {
378           classifier.useGpu();
379         }
380       } else if (device.equals(nnApi)) {
381         classifier.useNNAPI();
382       }
383     });
384   }
385 
386   /** Connect the buttons to their event handler. */
387   @Override
onViewCreated(final View view, Bundle savedInstanceState)388   public void onViewCreated(final View view, Bundle savedInstanceState) {
389     gpu = getString(R.string.gpu);
390     cpu = getString(R.string.cpu);
391     nnApi = getString(R.string.nnapi);
392     mobilenetV1Quant = getString(R.string.mobilenetV1Quant);
393     mobilenetV1Float = getString(R.string.mobilenetV1Float);
394 
395     // Get references to widgets.
396     textureView = (AutoFitTextureView) view.findViewById(R.id.texture);
397     textView = (TextView) view.findViewById(R.id.text);
398     deviceView = (ListView) view.findViewById(R.id.device);
399     modelView = (ListView) view.findViewById(R.id.model);
400 
401     // Build list of models
402     modelStrings.add(mobilenetV1Quant);
403     modelStrings.add(mobilenetV1Float);
404 
405     // Build list of devices
406     int defaultModelIndex = 0;
407     deviceStrings.add(cpu);
408     if (GpuDelegateHelper.isGpuDelegateAvailable()) {
409       deviceStrings.add(gpu);
410     }
411     deviceStrings.add(nnApi);
412 
413     deviceView.setAdapter(
414         new ArrayAdapter<String>(
415             getContext(), R.layout.listview_row, R.id.listview_row_text, deviceStrings));
416     deviceView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
417     deviceView.setOnItemClickListener(
418         new AdapterView.OnItemClickListener() {
419           @Override
420           public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
421             updateActiveModel();
422           }
423         });
424     deviceView.setItemChecked(0, true);
425 
426     modelView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
427     ArrayAdapter<String> modelAdapter =
428         new ArrayAdapter<>(
429             getContext(), R.layout.listview_row, R.id.listview_row_text, modelStrings);
430     modelView.setAdapter(modelAdapter);
431     modelView.setItemChecked(defaultModelIndex, true);
432     modelView.setOnItemClickListener(
433         new AdapterView.OnItemClickListener() {
434           @Override
435           public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
436             updateActiveModel();
437           }
438         });
439 
440     np = (NumberPicker) view.findViewById(R.id.np);
441     np.setMinValue(1);
442     np.setMaxValue(10);
443     np.setWrapSelectorWheel(true);
444     np.setOnValueChangedListener(
445         new NumberPicker.OnValueChangeListener() {
446           @Override
447           public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
448             updateActiveModel();
449           }
450         });
451 
452     // Start initial model.
453   }
454 
455   /** Load the model and labels. */
456   @Override
onActivityCreated(Bundle savedInstanceState)457   public void onActivityCreated(Bundle savedInstanceState) {
458     super.onActivityCreated(savedInstanceState);
459     startBackgroundThread();
460   }
461 
462   @Override
onResume()463   public void onResume() {
464     super.onResume();
465     startBackgroundThread();
466 
467     // When the screen is turned off and turned back on, the SurfaceTexture is already
468     // available, and "onSurfaceTextureAvailable" will not be called. In that case, we can open
469     // a camera and start preview from here (otherwise, we wait until the surface is ready in
470     // the SurfaceTextureListener).
471     if (textureView.isAvailable()) {
472       openCamera(textureView.getWidth(), textureView.getHeight());
473     } else {
474       textureView.setSurfaceTextureListener(surfaceTextureListener);
475     }
476   }
477 
478   @Override
onPause()479   public void onPause() {
480     closeCamera();
481     stopBackgroundThread();
482     super.onPause();
483   }
484 
485   @Override
onDestroy()486   public void onDestroy() {
487     if (classifier != null) {
488       classifier.close();
489     }
490     super.onDestroy();
491   }
492 
493   /**
494    * Sets up member variables related to camera.
495    *
496    * @param width The width of available size for camera preview
497    * @param height The height of available size for camera preview
498    */
setUpCameraOutputs(int width, int height)499   private void setUpCameraOutputs(int width, int height) {
500     Activity activity = getActivity();
501     CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
502     try {
503       for (String cameraId : manager.getCameraIdList()) {
504         CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);
505 
506         // We don't use a front facing camera in this sample.
507         Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
508         if (facing != null && facing == CameraCharacteristics.LENS_FACING_FRONT) {
509           continue;
510         }
511 
512         StreamConfigurationMap map =
513             characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
514         if (map == null) {
515           continue;
516         }
517 
518         // // For still image captures, we use the largest available size.
519         Size largest =
520             Collections.max(
521                 Arrays.asList(map.getOutputSizes(ImageFormat.JPEG)), new CompareSizesByArea());
522         imageReader =
523             ImageReader.newInstance(
524                 largest.getWidth(), largest.getHeight(), ImageFormat.JPEG, /*maxImages*/ 2);
525 
526         // Find out if we need to swap dimension to get the preview size relative to sensor
527         // coordinate.
528         int displayRotation = activity.getWindowManager().getDefaultDisplay().getRotation();
529         // noinspection ConstantConditions
530         /* Orientation of the camera sensor */
531         int sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
532         boolean swappedDimensions = false;
533         switch (displayRotation) {
534           case Surface.ROTATION_0:
535           case Surface.ROTATION_180:
536             if (sensorOrientation == 90 || sensorOrientation == 270) {
537               swappedDimensions = true;
538             }
539             break;
540           case Surface.ROTATION_90:
541           case Surface.ROTATION_270:
542             if (sensorOrientation == 0 || sensorOrientation == 180) {
543               swappedDimensions = true;
544             }
545             break;
546           default:
547             Log.e(TAG, "Display rotation is invalid: " + displayRotation);
548         }
549 
550         Point displaySize = new Point();
551         activity.getWindowManager().getDefaultDisplay().getSize(displaySize);
552         int rotatedPreviewWidth = width;
553         int rotatedPreviewHeight = height;
554         int maxPreviewWidth = displaySize.x;
555         int maxPreviewHeight = displaySize.y;
556 
557         if (swappedDimensions) {
558           rotatedPreviewWidth = height;
559           rotatedPreviewHeight = width;
560           maxPreviewWidth = displaySize.y;
561           maxPreviewHeight = displaySize.x;
562         }
563 
564         if (maxPreviewWidth > MAX_PREVIEW_WIDTH) {
565           maxPreviewWidth = MAX_PREVIEW_WIDTH;
566         }
567 
568         if (maxPreviewHeight > MAX_PREVIEW_HEIGHT) {
569           maxPreviewHeight = MAX_PREVIEW_HEIGHT;
570         }
571 
572         previewSize =
573             chooseOptimalSize(
574                 map.getOutputSizes(SurfaceTexture.class),
575                 rotatedPreviewWidth,
576                 rotatedPreviewHeight,
577                 maxPreviewWidth,
578                 maxPreviewHeight,
579                 largest);
580 
581         // We fit the aspect ratio of TextureView to the size of preview we picked.
582         int orientation = getResources().getConfiguration().orientation;
583         if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
584           textureView.setAspectRatio(previewSize.getWidth(), previewSize.getHeight());
585         } else {
586           textureView.setAspectRatio(previewSize.getHeight(), previewSize.getWidth());
587         }
588 
589         this.cameraId = cameraId;
590         return;
591       }
592     } catch (CameraAccessException e) {
593       Log.e(TAG, "Failed to access Camera", e);
594     } catch (NullPointerException e) {
595       // Currently an NPE is thrown when the Camera2API is used but not supported on the
596       // device this code runs.
597       ErrorDialog.newInstance(getString(R.string.camera_error))
598           .show(getChildFragmentManager(), FRAGMENT_DIALOG);
599     }
600   }
601 
getRequiredPermissions()602   private String[] getRequiredPermissions() {
603     Activity activity = getActivity();
604     try {
605       PackageInfo info =
606           activity
607               .getPackageManager()
608               .getPackageInfo(activity.getPackageName(), PackageManager.GET_PERMISSIONS);
609       String[] ps = info.requestedPermissions;
610       if (ps != null && ps.length > 0) {
611         return ps;
612       } else {
613         return new String[0];
614       }
615     } catch (Exception e) {
616       return new String[0];
617     }
618   }
619 
620   /** Opens the camera specified by {@link Camera2BasicFragment#cameraId}. */
openCamera(int width, int height)621   private void openCamera(int width, int height) {
622     if (!checkedPermissions && !allPermissionsGranted()) {
623       FragmentCompat.requestPermissions(this, getRequiredPermissions(), PERMISSIONS_REQUEST_CODE);
624       return;
625     } else {
626       checkedPermissions = true;
627     }
628     setUpCameraOutputs(width, height);
629     configureTransform(width, height);
630     Activity activity = getActivity();
631     CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
632     try {
633       if (!cameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
634         throw new RuntimeException("Time out waiting to lock camera opening.");
635       }
636       manager.openCamera(cameraId, stateCallback, backgroundHandler);
637     } catch (CameraAccessException e) {
638       Log.e(TAG, "Failed to open Camera", e);
639     } catch (InterruptedException e) {
640       throw new RuntimeException("Interrupted while trying to lock camera opening.", e);
641     }
642   }
643 
allPermissionsGranted()644   private boolean allPermissionsGranted() {
645     for (String permission : getRequiredPermissions()) {
646       if (ContextCompat.checkSelfPermission(getActivity(), permission)
647           != PackageManager.PERMISSION_GRANTED) {
648         return false;
649       }
650     }
651     return true;
652   }
653 
654   @Override
onRequestPermissionsResult( int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)655   public void onRequestPermissionsResult(
656       int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
657     super.onRequestPermissionsResult(requestCode, permissions, grantResults);
658   }
659 
660   /** Closes the current {@link CameraDevice}. */
closeCamera()661   private void closeCamera() {
662     try {
663       cameraOpenCloseLock.acquire();
664       if (null != captureSession) {
665         captureSession.close();
666         captureSession = null;
667       }
668       if (null != cameraDevice) {
669         cameraDevice.close();
670         cameraDevice = null;
671       }
672       if (null != imageReader) {
673         imageReader.close();
674         imageReader = null;
675       }
676     } catch (InterruptedException e) {
677       throw new RuntimeException("Interrupted while trying to lock camera closing.", e);
678     } finally {
679       cameraOpenCloseLock.release();
680     }
681   }
682 
683   /** Starts a background thread and its {@link Handler}. */
startBackgroundThread()684   private void startBackgroundThread() {
685     backgroundThread = new HandlerThread(HANDLE_THREAD_NAME);
686     backgroundThread.start();
687     backgroundHandler = new Handler(backgroundThread.getLooper());
688     // Start the classification train & load an initial model.
689     synchronized (lock) {
690       runClassifier = true;
691     }
692     backgroundHandler.post(periodicClassify);
693     updateActiveModel();
694   }
695 
696   /** Stops the background thread and its {@link Handler}. */
stopBackgroundThread()697   private void stopBackgroundThread() {
698     backgroundThread.quitSafely();
699     try {
700       backgroundThread.join();
701       backgroundThread = null;
702       backgroundHandler = null;
703       synchronized (lock) {
704         runClassifier = false;
705       }
706     } catch (InterruptedException e) {
707       Log.e(TAG, "Interrupted when stopping background thread", e);
708     }
709   }
710 
711   /** Takes photos and classify them periodically. */
712   private Runnable periodicClassify =
713       new Runnable() {
714         @Override
715         public void run() {
716           synchronized (lock) {
717             if (runClassifier) {
718               classifyFrame();
719             }
720           }
721           backgroundHandler.post(periodicClassify);
722         }
723       };
724 
725   /** Creates a new {@link CameraCaptureSession} for camera preview. */
createCameraPreviewSession()726   private void createCameraPreviewSession() {
727     try {
728       SurfaceTexture texture = textureView.getSurfaceTexture();
729       assert texture != null;
730 
731       // We configure the size of default buffer to be the size of camera preview we want.
732       texture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight());
733 
734       // This is the output Surface we need to start preview.
735       Surface surface = new Surface(texture);
736 
737       // We set up a CaptureRequest.Builder with the output Surface.
738       previewRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
739       previewRequestBuilder.addTarget(surface);
740 
741       // Here, we create a CameraCaptureSession for camera preview.
742       cameraDevice.createCaptureSession(
743           Arrays.asList(surface),
744           new CameraCaptureSession.StateCallback() {
745 
746             @Override
747             public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
748               // The camera is already closed
749               if (null == cameraDevice) {
750                 return;
751               }
752 
753               // When the session is ready, we start displaying the preview.
754               captureSession = cameraCaptureSession;
755               try {
756                 // Auto focus should be continuous for camera preview.
757                 previewRequestBuilder.set(
758                     CaptureRequest.CONTROL_AF_MODE,
759                     CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
760 
761                 // Finally, we start displaying the camera preview.
762                 previewRequest = previewRequestBuilder.build();
763                 captureSession.setRepeatingRequest(
764                     previewRequest, captureCallback, backgroundHandler);
765               } catch (CameraAccessException e) {
766                 Log.e(TAG, "Failed to set up config to capture Camera", e);
767               }
768             }
769 
770             @Override
771             public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
772               showToast("Failed");
773             }
774           },
775           null);
776     } catch (CameraAccessException e) {
777       Log.e(TAG, "Failed to preview Camera", e);
778     }
779   }
780 
781   /**
782    * Configures the necessary {@link android.graphics.Matrix} transformation to `textureView`. This
783    * method should be called after the camera preview size is determined in setUpCameraOutputs and
784    * also the size of `textureView` is fixed.
785    *
786    * @param viewWidth The width of `textureView`
787    * @param viewHeight The height of `textureView`
788    */
configureTransform(int viewWidth, int viewHeight)789   private void configureTransform(int viewWidth, int viewHeight) {
790     Activity activity = getActivity();
791     if (null == textureView || null == previewSize || null == activity) {
792       return;
793     }
794     int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
795     Matrix matrix = new Matrix();
796     RectF viewRect = new RectF(0, 0, viewWidth, viewHeight);
797     RectF bufferRect = new RectF(0, 0, previewSize.getHeight(), previewSize.getWidth());
798     float centerX = viewRect.centerX();
799     float centerY = viewRect.centerY();
800     if (Surface.ROTATION_90 == rotation || Surface.ROTATION_270 == rotation) {
801       bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY());
802       matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL);
803       float scale =
804           Math.max(
805               (float) viewHeight / previewSize.getHeight(),
806               (float) viewWidth / previewSize.getWidth());
807       matrix.postScale(scale, scale, centerX, centerY);
808       matrix.postRotate(90 * (rotation - 2), centerX, centerY);
809     } else if (Surface.ROTATION_180 == rotation) {
810       matrix.postRotate(180, centerX, centerY);
811     }
812     textureView.setTransform(matrix);
813   }
814 
815   /** Classifies a frame from the preview stream. */
classifyFrame()816   private void classifyFrame() {
817     if (classifier == null || getActivity() == null || cameraDevice == null) {
818       // It's important to not call showToast every frame, or else the app will starve and
819       // hang. updateActiveModel() already puts a error message up with showToast.
820       // showToast("Uninitialized Classifier or invalid context.");
821       return;
822     }
823     SpannableStringBuilder textToShow = new SpannableStringBuilder();
824     Bitmap bitmap = textureView.getBitmap(classifier.getImageSizeX(), classifier.getImageSizeY());
825     classifier.classifyFrame(bitmap, textToShow);
826     bitmap.recycle();
827     showToast(textToShow);
828   }
829 
830   /** Compares two {@code Size}s based on their areas. */
831   private static class CompareSizesByArea implements Comparator<Size> {
832 
833     @Override
compare(Size lhs, Size rhs)834     public int compare(Size lhs, Size rhs) {
835       // We cast here to ensure the multiplications won't overflow
836       return Long.signum(
837           (long) lhs.getWidth() * lhs.getHeight() - (long) rhs.getWidth() * rhs.getHeight());
838     }
839   }
840 
841   /** Shows an error message dialog. */
842   public static class ErrorDialog extends DialogFragment {
843 
844     private static final String ARG_MESSAGE = "message";
845 
newInstance(String message)846     public static ErrorDialog newInstance(String message) {
847       ErrorDialog dialog = new ErrorDialog();
848       Bundle args = new Bundle();
849       args.putString(ARG_MESSAGE, message);
850       dialog.setArguments(args);
851       return dialog;
852     }
853 
854     @Override
onCreateDialog(Bundle savedInstanceState)855     public Dialog onCreateDialog(Bundle savedInstanceState) {
856       final Activity activity = getActivity();
857       return new AlertDialog.Builder(activity)
858           .setMessage(getArguments().getString(ARG_MESSAGE))
859           .setPositiveButton(
860               android.R.string.ok,
861               new DialogInterface.OnClickListener() {
862                 @Override
863                 public void onClick(DialogInterface dialogInterface, int i) {
864                   activity.finish();
865                 }
866               })
867           .create();
868     }
869   }
870 }
871