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