1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.hardware.camera2.legacy; 18 19 import android.graphics.Matrix; 20 import android.graphics.Point; 21 import android.graphics.Rect; 22 import android.graphics.RectF; 23 import android.hardware.Camera; 24 import android.hardware.Camera.Area; 25 import android.hardware.camera2.params.Face; 26 import android.hardware.camera2.params.MeteringRectangle; 27 import android.hardware.camera2.utils.ListUtils; 28 import android.hardware.camera2.utils.ParamsUtils; 29 import android.hardware.camera2.utils.SizeAreaComparator; 30 import android.util.Size; 31 import android.util.SizeF; 32 33 import android.util.Log; 34 35 import java.util.ArrayList; 36 import java.util.Arrays; 37 import java.util.List; 38 39 import static com.android.internal.util.Preconditions.*; 40 41 /** 42 * Various utilities for dealing with camera API1 parameters. 43 */ 44 @SuppressWarnings("deprecation") 45 public class ParameterUtils { 46 /** Upper/left minimal point of a normalized rectangle */ 47 public static final int NORMALIZED_RECTANGLE_MIN = -1000; 48 /** Lower/right maximal point of a normalized rectangle */ 49 public static final int NORMALIZED_RECTANGLE_MAX = 1000; 50 /** The default normalized rectangle spans the entire size of the preview viewport */ 51 public static final Rect NORMALIZED_RECTANGLE_DEFAULT = new Rect( 52 NORMALIZED_RECTANGLE_MIN, 53 NORMALIZED_RECTANGLE_MIN, 54 NORMALIZED_RECTANGLE_MAX, 55 NORMALIZED_RECTANGLE_MAX); 56 /** The default normalized area uses the default normalized rectangle with a weight=1 */ 57 public static final Camera.Area CAMERA_AREA_DEFAULT = 58 new Camera.Area(new Rect(NORMALIZED_RECTANGLE_DEFAULT), 59 /*weight*/1); 60 /** Empty rectangle {@code 0x0+0,0} */ 61 public static final Rect RECTANGLE_EMPTY = 62 new Rect(/*left*/0, /*top*/0, /*right*/0, /*bottom*/0); 63 64 private static final double ASPECT_RATIO_TOLERANCE = 0.05f; 65 66 /** 67 * Calculate effective/reported zoom data from a user-specified crop region. 68 */ 69 public static class ZoomData { 70 /** Zoom index used by {@link Camera.Parameters#setZoom} */ 71 public final int zoomIndex; 72 /** Effective crop-region given the zoom index, coordinates relative to active-array */ 73 public final Rect previewCrop; 74 /** Reported crop-region given the zoom index, coordinates relative to active-array */ 75 public final Rect reportedCrop; 76 /** Reported zoom ratio given the zoom index */ 77 public final float reportedZoomRatio; 78 ZoomData(int zoomIndex, Rect previewCrop, Rect reportedCrop, float reportedZoomRatio)79 public ZoomData(int zoomIndex, Rect previewCrop, Rect reportedCrop, 80 float reportedZoomRatio) { 81 this.zoomIndex = zoomIndex; 82 this.previewCrop = previewCrop; 83 this.reportedCrop = reportedCrop; 84 this.reportedZoomRatio = reportedZoomRatio; 85 } 86 } 87 88 /** 89 * Calculate effective/reported metering data from a user-specified metering region. 90 */ 91 public static class MeteringData { 92 /** 93 * The metering area scaled to the range of [-1000, 1000]. 94 * <p>Values outside of this range are clipped to be within the range.</p> 95 */ 96 public final Camera.Area meteringArea; 97 /** 98 * Effective preview metering region, coordinates relative to active-array. 99 * 100 * <p>Clipped to fit inside of the (effective) preview crop region.</p> 101 */ 102 public final Rect previewMetering; 103 /** 104 * Reported metering region, coordinates relative to active-array. 105 * 106 * <p>Clipped to fit inside of the (reported) resulting crop region.</p> 107 */ 108 public final Rect reportedMetering; 109 MeteringData(Area meteringArea, Rect previewMetering, Rect reportedMetering)110 public MeteringData(Area meteringArea, Rect previewMetering, Rect reportedMetering) { 111 this.meteringArea = meteringArea; 112 this.previewMetering = previewMetering; 113 this.reportedMetering = reportedMetering; 114 } 115 } 116 117 /** 118 * A weighted rectangle is an arbitrary rectangle (the coordinate system is unknown) with an 119 * arbitrary weight. 120 * 121 * <p>The user of this class must know what the coordinate system ahead of time; it's 122 * then possible to convert to a more concrete type such as a metering rectangle or a face. 123 * </p> 124 * 125 * <p>When converting to a more concrete type, out-of-range values are clipped; this prevents 126 * possible illegal argument exceptions being thrown at runtime.</p> 127 */ 128 public static class WeightedRectangle { 129 /** Arbitrary rectangle (the range is user-defined); never {@code null}. */ 130 public final Rect rect; 131 /** Arbitrary weight (the range is user-defined). */ 132 public final int weight; 133 134 /** 135 * Create a new weighted-rectangle from a non-{@code null} rectangle; the {@code weight} 136 * can be unbounded. 137 */ WeightedRectangle(Rect rect, int weight)138 public WeightedRectangle(Rect rect, int weight) { 139 this.rect = checkNotNull(rect, "rect must not be null"); 140 this.weight = weight; 141 } 142 143 /** 144 * Convert to a metering rectangle, clipping any of the values to stay within range. 145 * 146 * <p>If values are clipped, a warning is printed to logcat.</p> 147 * 148 * @return a new metering rectangle 149 */ toMetering()150 public MeteringRectangle toMetering() { 151 int weight = clip(this.weight, 152 MeteringRectangle.METERING_WEIGHT_MIN, 153 MeteringRectangle.METERING_WEIGHT_MAX, 154 rect, 155 "weight"); 156 157 int x = clipLower(rect.left, /*lo*/0, rect, "left"); 158 int y = clipLower(rect.top, /*lo*/0, rect, "top"); 159 int w = clipLower(rect.width(), /*lo*/0, rect, "width"); 160 int h = clipLower(rect.height(), /*lo*/0, rect, "height"); 161 162 return new MeteringRectangle(x, y, w, h, weight); 163 } 164 165 /** 166 * Convert to a face; the rect is considered to be the bounds, and the weight 167 * is considered to be the score. 168 * 169 * <p>If the score is out of range of {@value Face#SCORE_MIN}, {@value Face#SCORE_MAX}, 170 * the score is clipped first and a warning is printed to logcat.</p> 171 * 172 * <p>If the id is negative, the id is changed to 0 and a warning is printed to 173 * logcat.</p> 174 * 175 * <p>All other parameters are passed-through as-is.</p> 176 * 177 * @return a new face with the optional features set 178 */ toFace( int id, Point leftEyePosition, Point rightEyePosition, Point mouthPosition)179 public Face toFace( 180 int id, Point leftEyePosition, Point rightEyePosition, Point mouthPosition) { 181 int idSafe = clipLower(id, /*lo*/0, rect, "id"); 182 int score = clip(weight, 183 Face.SCORE_MIN, 184 Face.SCORE_MAX, 185 rect, 186 "score"); 187 188 return new Face(rect, score, idSafe, leftEyePosition, rightEyePosition, mouthPosition); 189 } 190 191 /** 192 * Convert to a face; the rect is considered to be the bounds, and the weight 193 * is considered to be the score. 194 * 195 * <p>If the score is out of range of {@value Face#SCORE_MIN}, {@value Face#SCORE_MAX}, 196 * the score is clipped first and a warning is printed to logcat.</p> 197 * 198 * <p>All other parameters are passed-through as-is.</p> 199 * 200 * @return a new face without the optional features 201 */ toFace()202 public Face toFace() { 203 int score = clip(weight, 204 Face.SCORE_MIN, 205 Face.SCORE_MAX, 206 rect, 207 "score"); 208 209 return new Face(rect, score); 210 } 211 clipLower(int value, int lo, Rect rect, String name)212 private static int clipLower(int value, int lo, Rect rect, String name) { 213 return clip(value, lo, /*hi*/Integer.MAX_VALUE, rect, name); 214 } 215 clip(int value, int lo, int hi, Rect rect, String name)216 private static int clip(int value, int lo, int hi, Rect rect, String name) { 217 if (value < lo) { 218 Log.w(TAG, "toMetering - Rectangle " + rect + " " 219 + name + " too small, clip to " + lo); 220 value = lo; 221 } else if (value > hi) { 222 Log.w(TAG, "toMetering - Rectangle " + rect + " " 223 + name + " too small, clip to " + hi); 224 value = hi; 225 } 226 227 return value; 228 } 229 } 230 231 private static final String TAG = "ParameterUtils"; 232 private static final boolean DEBUG = false; 233 234 /** getZoomRatios stores zoom ratios in 1/100 increments, e.x. a zoom of 3.2 is 320 */ 235 private static final int ZOOM_RATIO_MULTIPLIER = 100; 236 237 /** 238 * Convert a camera API1 size into a util size 239 */ convertSize(Camera.Size size)240 public static Size convertSize(Camera.Size size) { 241 checkNotNull(size, "size must not be null"); 242 243 return new Size(size.width, size.height); 244 } 245 246 /** 247 * Convert a camera API1 list of sizes into a util list of sizes 248 */ convertSizeList(List<Camera.Size> sizeList)249 public static List<Size> convertSizeList(List<Camera.Size> sizeList) { 250 checkNotNull(sizeList, "sizeList must not be null"); 251 252 List<Size> sizes = new ArrayList<>(sizeList.size()); 253 for (Camera.Size s : sizeList) { 254 sizes.add(new Size(s.width, s.height)); 255 } 256 return sizes; 257 } 258 259 /** 260 * Convert a camera API1 list of sizes into an array of sizes 261 */ convertSizeListToArray(List<Camera.Size> sizeList)262 public static Size[] convertSizeListToArray(List<Camera.Size> sizeList) { 263 checkNotNull(sizeList, "sizeList must not be null"); 264 265 Size[] array = new Size[sizeList.size()]; 266 int ctr = 0; 267 for (Camera.Size s : sizeList) { 268 array[ctr++] = new Size(s.width, s.height); 269 } 270 return array; 271 } 272 273 /** 274 * Check if the camera API1 list of sizes contains a size with the given dimens. 275 */ containsSize(List<Camera.Size> sizeList, int width, int height)276 public static boolean containsSize(List<Camera.Size> sizeList, int width, int height) { 277 checkNotNull(sizeList, "sizeList must not be null"); 278 for (Camera.Size s : sizeList) { 279 if (s.height == height && s.width == width) { 280 return true; 281 } 282 } 283 return false; 284 } 285 286 /** 287 * Returns the largest supported picture size, as compared by its area. 288 */ getLargestSupportedJpegSizeByArea(Camera.Parameters params)289 public static Size getLargestSupportedJpegSizeByArea(Camera.Parameters params) { 290 checkNotNull(params, "params must not be null"); 291 292 List<Size> supportedJpegSizes = convertSizeList(params.getSupportedPictureSizes()); 293 return SizeAreaComparator.findLargestByArea(supportedJpegSizes); 294 } 295 296 /** 297 * Convert a camera area into a human-readable string. 298 */ stringFromArea(Camera.Area area)299 public static String stringFromArea(Camera.Area area) { 300 if (area == null) { 301 return null; 302 } else { 303 StringBuilder sb = new StringBuilder(); 304 Rect r = area.rect; 305 306 sb.setLength(0); 307 sb.append("(["); sb.append(r.left); sb.append(','); 308 sb.append(r.top); sb.append("]["); sb.append(r.right); 309 sb.append(','); sb.append(r.bottom); sb.append(']'); 310 311 sb.append(','); 312 sb.append(area.weight); 313 sb.append(')'); 314 315 return sb.toString(); 316 } 317 } 318 319 /** 320 * Convert a camera area list into a human-readable string 321 * @param areaList a list of areas (null is ok) 322 */ stringFromAreaList(List<Camera.Area> areaList)323 public static String stringFromAreaList(List<Camera.Area> areaList) { 324 StringBuilder sb = new StringBuilder(); 325 326 if (areaList == null) { 327 return null; 328 } 329 330 int i = 0; 331 for (Camera.Area area : areaList) { 332 if (area == null) { 333 sb.append("null"); 334 } else { 335 sb.append(stringFromArea(area)); 336 } 337 338 if (i != areaList.size() - 1) { 339 sb.append(", "); 340 } 341 342 i++; 343 } 344 345 return sb.toString(); 346 } 347 348 /** 349 * Calculate the closest zoom index for the user-requested crop region by rounding 350 * up to the closest (largest or equal) possible zoom crop. 351 * 352 * <p>If the requested crop region exceeds the size of the active array, it is 353 * shrunk to fit inside of the active array first.</p> 354 * 355 * <p>Since all api1 camera devices only support a discrete set of zooms, we have 356 * to translate the per-pixel-granularity requested crop region into a per-zoom-index 357 * granularity.</p> 358 * 359 * <p>Furthermore, since the zoom index and zoom levels also depends on the field-of-view 360 * of the preview, the current preview {@code streamSize} is also used.</p> 361 * 362 * <p>The calculated crop regions are then written to in-place to {@code reportedCropRegion} 363 * and {@code previewCropRegion}, in coordinates relative to the active array.</p> 364 * 365 * @param params non-{@code null} camera api1 parameters 366 * @param activeArray active array dimensions, in sensor space 367 * @param streamSize stream size dimensions, in pixels 368 * @param cropRegion user-specified crop region, in active array coordinates 369 * @param reportedCropRegion (out parameter) what the result for {@code cropRegion} looks like 370 * @param previewCropRegion (out parameter) what the visual preview crop is 371 * @return 372 * the zoom index inclusively between 0 and {@code Parameters#getMaxZoom}, 373 * where 0 means the camera is not zoomed 374 * 375 * @throws NullPointerException if any of the args were {@code null} 376 */ getClosestAvailableZoomCrop( Camera.Parameters params, Rect activeArray, Size streamSize, Rect cropRegion, Rect reportedCropRegion, Rect previewCropRegion)377 public static int getClosestAvailableZoomCrop( 378 Camera.Parameters params, Rect activeArray, 379 Size streamSize, Rect cropRegion, 380 /*out*/ 381 Rect reportedCropRegion, 382 Rect previewCropRegion) { 383 checkNotNull(params, "params must not be null"); 384 checkNotNull(activeArray, "activeArray must not be null"); 385 checkNotNull(streamSize, "streamSize must not be null"); 386 checkNotNull(reportedCropRegion, "reportedCropRegion must not be null"); 387 checkNotNull(previewCropRegion, "previewCropRegion must not be null"); 388 389 Rect actualCrop = new Rect(cropRegion); 390 391 /* 392 * Shrink requested crop region to fit inside of the active array size 393 */ 394 if (!actualCrop.intersect(activeArray)) { 395 Log.w(TAG, "getClosestAvailableZoomCrop - Crop region out of range; " + 396 "setting to active array size"); 397 actualCrop.set(activeArray); 398 } 399 400 Rect previewCrop = getPreviewCropRectangleUnzoomed(activeArray, streamSize); 401 402 // Make the user-requested crop region the same aspect ratio as the preview stream size 403 Rect cropRegionAsPreview = 404 shrinkToSameAspectRatioCentered(previewCrop, actualCrop); 405 406 if (DEBUG) { 407 Log.v(TAG, "getClosestAvailableZoomCrop - actualCrop = " + actualCrop); 408 Log.v(TAG, 409 "getClosestAvailableZoomCrop - previewCrop = " + previewCrop); 410 Log.v(TAG, 411 "getClosestAvailableZoomCrop - cropRegionAsPreview = " + cropRegionAsPreview); 412 } 413 414 /* 415 * Iterate all available zoom rectangles and find the closest zoom index 416 */ 417 Rect bestReportedCropRegion = null; 418 Rect bestPreviewCropRegion = null; 419 int bestZoomIndex = -1; 420 421 List<Rect> availableReportedCropRegions = 422 getAvailableZoomCropRectangles(params, activeArray); 423 List<Rect> availablePreviewCropRegions = 424 getAvailablePreviewZoomCropRectangles(params, activeArray, streamSize); 425 426 if (DEBUG) { 427 Log.v(TAG, 428 "getClosestAvailableZoomCrop - availableReportedCropRegions = " + 429 ListUtils.listToString(availableReportedCropRegions)); 430 Log.v(TAG, 431 "getClosestAvailableZoomCrop - availablePreviewCropRegions = " + 432 ListUtils.listToString(availablePreviewCropRegions)); 433 } 434 435 if (availableReportedCropRegions.size() != availablePreviewCropRegions.size()) { 436 throw new AssertionError("available reported/preview crop region size mismatch"); 437 } 438 439 for (int i = 0; i < availableReportedCropRegions.size(); ++i) { 440 Rect currentPreviewCropRegion = availablePreviewCropRegions.get(i); 441 Rect currentReportedCropRegion = availableReportedCropRegions.get(i); 442 443 boolean isBest; 444 if (bestZoomIndex == -1) { 445 isBest = true; 446 } else if (currentPreviewCropRegion.width() >= cropRegionAsPreview.width() && 447 currentPreviewCropRegion.height() >= cropRegionAsPreview.height()) { 448 isBest = true; 449 } else { 450 isBest = false; 451 } 452 453 // Sizes are sorted largest-to-smallest, so once the available crop is too small, 454 // we the rest are too small. Furthermore, this is the final best crop, 455 // since its the largest crop that still fits the requested crop 456 if (isBest) { 457 bestPreviewCropRegion = currentPreviewCropRegion; 458 bestReportedCropRegion = currentReportedCropRegion; 459 bestZoomIndex = i; 460 } else { 461 break; 462 } 463 } 464 465 if (bestZoomIndex == -1) { 466 // Even in the worst case, we should always at least return 0 here 467 throw new AssertionError("Should've found at least one valid zoom index"); 468 } 469 470 // Write the rectangles in-place 471 reportedCropRegion.set(bestReportedCropRegion); 472 previewCropRegion.set(bestPreviewCropRegion); 473 474 return bestZoomIndex; 475 } 476 477 /** 478 * Calculate the effective crop rectangle for this preview viewport; 479 * assumes the preview is centered to the sensor and scaled to fit across one of the dimensions 480 * without skewing. 481 * 482 * <p>The preview size must be a subset of the active array size; the resulting 483 * rectangle will also be a subset of the active array rectangle.</p> 484 * 485 * <p>The unzoomed crop rectangle is calculated only.</p> 486 * 487 * @param activeArray active array dimensions, in sensor space 488 * @param previewSize size of the preview buffer render target, in pixels (not in sensor space) 489 * @return a rectangle which serves as the preview stream's effective crop region (unzoomed), 490 * in sensor space 491 * 492 * @throws NullPointerException 493 * if any of the args were {@code null} 494 * @throws IllegalArgumentException 495 * if {@code previewSize} is wider or taller than {@code activeArray} 496 */ getPreviewCropRectangleUnzoomed(Rect activeArray, Size previewSize)497 private static Rect getPreviewCropRectangleUnzoomed(Rect activeArray, Size previewSize) { 498 if (previewSize.getWidth() > activeArray.width()) { 499 throw new IllegalArgumentException("previewSize must not be wider than activeArray"); 500 } else if (previewSize.getHeight() > activeArray.height()) { 501 throw new IllegalArgumentException("previewSize must not be taller than activeArray"); 502 } 503 504 float aspectRatioArray = activeArray.width() * 1.0f / activeArray.height(); 505 float aspectRatioPreview = previewSize.getWidth() * 1.0f / previewSize.getHeight(); 506 507 float cropH, cropW; 508 if (Math.abs(aspectRatioPreview - aspectRatioArray) < ASPECT_RATIO_TOLERANCE) { 509 cropH = activeArray.height(); 510 cropW = activeArray.width(); 511 } else if (aspectRatioPreview < aspectRatioArray) { 512 // The new width must be smaller than the height, so scale the width by AR 513 cropH = activeArray.height(); 514 cropW = cropH * aspectRatioPreview; 515 } else { 516 // The new height must be smaller (or equal) than the width, so scale the height by AR 517 cropW = activeArray.width(); 518 cropH = cropW / aspectRatioPreview; 519 } 520 521 Matrix translateMatrix = new Matrix(); 522 RectF cropRect = new RectF(/*left*/0, /*top*/0, cropW, cropH); 523 524 // Now center the crop rectangle so its center is in the center of the active array 525 translateMatrix.setTranslate(activeArray.exactCenterX(), activeArray.exactCenterY()); 526 translateMatrix.postTranslate(-cropRect.centerX(), -cropRect.centerY()); 527 528 translateMatrix.mapRect(/*inout*/cropRect); 529 530 // Round the rect corners towards the nearest integer values 531 return ParamsUtils.createRect(cropRect); 532 } 533 534 /** 535 * Shrink the {@code shrinkTarget} rectangle to snugly fit inside of {@code reference}; 536 * the aspect ratio of {@code shrinkTarget} will change to be the same aspect ratio as 537 * {@code reference}. 538 * 539 * <p>At most a single dimension will scale (down). Both dimensions will never be scaled.</p> 540 * 541 * @param reference the rectangle whose aspect ratio will be used as the new aspect ratio 542 * @param shrinkTarget the rectangle which will be scaled down to have a new aspect ratio 543 * 544 * @return a new rectangle, a subset of {@code shrinkTarget}, 545 * whose aspect ratio will match that of {@code reference} 546 */ shrinkToSameAspectRatioCentered(Rect reference, Rect shrinkTarget)547 private static Rect shrinkToSameAspectRatioCentered(Rect reference, Rect shrinkTarget) { 548 float aspectRatioReference = reference.width() * 1.0f / reference.height(); 549 float aspectRatioShrinkTarget = shrinkTarget.width() * 1.0f / shrinkTarget.height(); 550 551 float cropH, cropW; 552 if (aspectRatioShrinkTarget < aspectRatioReference) { 553 // The new width must be smaller than the height, so scale the width by AR 554 cropH = reference.height(); 555 cropW = cropH * aspectRatioShrinkTarget; 556 } else { 557 // The new height must be smaller (or equal) than the width, so scale the height by AR 558 cropW = reference.width(); 559 cropH = cropW / aspectRatioShrinkTarget; 560 } 561 562 Matrix translateMatrix = new Matrix(); 563 RectF shrunkRect = new RectF(shrinkTarget); 564 565 // Scale the rectangle down, but keep its center in the same place as before 566 translateMatrix.setScale(cropW / reference.width(), cropH / reference.height(), 567 shrinkTarget.exactCenterX(), shrinkTarget.exactCenterY()); 568 569 translateMatrix.mapRect(/*inout*/shrunkRect); 570 571 return ParamsUtils.createRect(shrunkRect); 572 } 573 574 /** 575 * Get the available 'crop' (zoom) rectangles for this camera that will be reported 576 * via a {@code CaptureResult} when a zoom is requested. 577 * 578 * <p>These crops ignores the underlying preview buffer size, and will always be reported 579 * the same values regardless of what configuration of outputs is used.</p> 580 * 581 * <p>When zoom is supported, this will return a list of {@code 1 + #getMaxZoom} size, 582 * where each crop rectangle corresponds to a zoom ratio (and is centered at the middle).</p> 583 * 584 * <p>Each crop rectangle is changed to have the same aspect ratio as {@code streamSize}, 585 * by shrinking the rectangle if necessary.</p> 586 * 587 * <p>To get the reported crop region when applying a zoom to the sensor, use {@code streamSize} 588 * = {@code activeArray size}.</p> 589 * 590 * @param params non-{@code null} camera api1 parameters 591 * @param activeArray active array dimensions, in sensor space 592 * @param streamSize stream size dimensions, in pixels 593 * 594 * @return a list of available zoom rectangles, sorted from least zoomed to most zoomed 595 */ getAvailableZoomCropRectangles( Camera.Parameters params, Rect activeArray)596 public static List<Rect> getAvailableZoomCropRectangles( 597 Camera.Parameters params, Rect activeArray) { 598 checkNotNull(params, "params must not be null"); 599 checkNotNull(activeArray, "activeArray must not be null"); 600 601 return getAvailableCropRectangles(params, activeArray, ParamsUtils.createSize(activeArray)); 602 } 603 604 /** 605 * Get the available 'crop' (zoom) rectangles for this camera. 606 * 607 * <p>This is the effective (real) crop that is applied by the camera api1 device 608 * when projecting the zoom onto the intermediate preview buffer. Use this when 609 * deciding which zoom ratio to apply.</p> 610 * 611 * <p>When zoom is supported, this will return a list of {@code 1 + #getMaxZoom} size, 612 * where each crop rectangle corresponds to a zoom ratio (and is centered at the middle).</p> 613 * 614 * <p>Each crop rectangle is changed to have the same aspect ratio as {@code streamSize}, 615 * by shrinking the rectangle if necessary.</p> 616 * 617 * <p>To get the reported crop region when applying a zoom to the sensor, use {@code streamSize} 618 * = {@code activeArray size}.</p> 619 * 620 * @param params non-{@code null} camera api1 parameters 621 * @param activeArray active array dimensions, in sensor space 622 * @param streamSize stream size dimensions, in pixels 623 * 624 * @return a list of available zoom rectangles, sorted from least zoomed to most zoomed 625 */ getAvailablePreviewZoomCropRectangles(Camera.Parameters params, Rect activeArray, Size previewSize)626 public static List<Rect> getAvailablePreviewZoomCropRectangles(Camera.Parameters params, 627 Rect activeArray, Size previewSize) { 628 checkNotNull(params, "params must not be null"); 629 checkNotNull(activeArray, "activeArray must not be null"); 630 checkNotNull(previewSize, "previewSize must not be null"); 631 632 return getAvailableCropRectangles(params, activeArray, previewSize); 633 } 634 635 /** 636 * Get the available 'crop' (zoom) rectangles for this camera. 637 * 638 * <p>When zoom is supported, this will return a list of {@code 1 + #getMaxZoom} size, 639 * where each crop rectangle corresponds to a zoom ratio (and is centered at the middle).</p> 640 * 641 * <p>Each crop rectangle is changed to have the same aspect ratio as {@code streamSize}, 642 * by shrinking the rectangle if necessary.</p> 643 * 644 * <p>To get the reported crop region when applying a zoom to the sensor, use {@code streamSize} 645 * = {@code activeArray size}.</p> 646 * 647 * @param params non-{@code null} camera api1 parameters 648 * @param activeArray active array dimensions, in sensor space 649 * @param streamSize stream size dimensions, in pixels 650 * 651 * @return a list of available zoom rectangles, sorted from least zoomed to most zoomed 652 */ getAvailableCropRectangles(Camera.Parameters params, Rect activeArray, Size streamSize)653 private static List<Rect> getAvailableCropRectangles(Camera.Parameters params, 654 Rect activeArray, Size streamSize) { 655 checkNotNull(params, "params must not be null"); 656 checkNotNull(activeArray, "activeArray must not be null"); 657 checkNotNull(streamSize, "streamSize must not be null"); 658 659 // TODO: change all uses of Rect activeArray to Size activeArray, 660 // since we want the crop to be active-array relative, not pixel-array relative 661 662 Rect unzoomedStreamCrop = getPreviewCropRectangleUnzoomed(activeArray, streamSize); 663 664 if (!params.isZoomSupported()) { 665 // Trivial case: No zoom -> only support the full size as the crop region 666 return new ArrayList<>(Arrays.asList(unzoomedStreamCrop)); 667 } 668 669 List<Rect> zoomCropRectangles = new ArrayList<>(params.getMaxZoom() + 1); 670 Matrix scaleMatrix = new Matrix(); 671 RectF scaledRect = new RectF(); 672 673 for (int zoom : params.getZoomRatios()) { 674 float shrinkRatio = ZOOM_RATIO_MULTIPLIER * 1.0f / zoom; // normalize to 1.0 and smaller 675 676 // set scaledRect to unzoomedStreamCrop 677 ParamsUtils.convertRectF(unzoomedStreamCrop, /*out*/scaledRect); 678 679 scaleMatrix.setScale( 680 shrinkRatio, shrinkRatio, 681 activeArray.exactCenterX(), 682 activeArray.exactCenterY()); 683 684 scaleMatrix.mapRect(scaledRect); 685 686 Rect intRect = ParamsUtils.createRect(scaledRect); 687 688 // Round the rect corners towards the nearest integer values 689 zoomCropRectangles.add(intRect); 690 } 691 692 return zoomCropRectangles; 693 } 694 695 /** 696 * Get the largest possible zoom ratio (normalized to {@code 1.0f} and higher) 697 * that the camera can support. 698 * 699 * <p>If the camera does not support zoom, it always returns {@code 1.0f}.</p> 700 * 701 * @param params non-{@code null} camera api1 parameters 702 * @return normalized max zoom ratio, at least {@code 1.0f} 703 */ getMaxZoomRatio(Camera.Parameters params)704 public static float getMaxZoomRatio(Camera.Parameters params) { 705 if (!params.isZoomSupported()) { 706 return 1.0f; // no zoom 707 } 708 709 List<Integer> zoomRatios = params.getZoomRatios(); // sorted smallest->largest 710 int zoom = zoomRatios.get(zoomRatios.size() - 1); // largest zoom ratio 711 float zoomRatio = zoom * 1.0f / ZOOM_RATIO_MULTIPLIER; // normalize to 1.0 and smaller 712 713 return zoomRatio; 714 } 715 716 /** 717 * Returns the component-wise zoom ratio (each greater or equal than {@code 1.0}); 718 * largest values means more zoom. 719 * 720 * @param activeArraySize active array size of the sensor (e.g. max jpeg size) 721 * @param cropSize size of the crop/zoom 722 * 723 * @return {@link SizeF} with width/height being the component-wise zoom ratio 724 * 725 * @throws NullPointerException if any of the args were {@code null} 726 * @throws IllegalArgumentException if any component of {@code cropSize} was {@code 0} 727 */ getZoomRatio(Size activeArraySize, Size cropSize)728 private static SizeF getZoomRatio(Size activeArraySize, Size cropSize) { 729 checkNotNull(activeArraySize, "activeArraySize must not be null"); 730 checkNotNull(cropSize, "cropSize must not be null"); 731 checkArgumentPositive(cropSize.getWidth(), "cropSize.width must be positive"); 732 checkArgumentPositive(cropSize.getHeight(), "cropSize.height must be positive"); 733 734 float zoomRatioWidth = activeArraySize.getWidth() * 1.0f / cropSize.getWidth(); 735 float zoomRatioHeight = activeArraySize.getHeight() * 1.0f / cropSize.getHeight(); 736 737 return new SizeF(zoomRatioWidth, zoomRatioHeight); 738 } 739 740 /** 741 * Convert the user-specified crop region/zoom into zoom data; which can be used 742 * to set the parameters to a specific zoom index, or to report back to the user what 743 * the actual zoom was, or for other calculations requiring the current preview crop region. 744 * 745 * <p>None of the parameters are mutated.<p> 746 * 747 * @param activeArraySize active array size of the sensor (e.g. max jpeg size) 748 * @param cropRegion the user-specified crop region 749 * @param zoomRatio the user-specified zoom ratio 750 * @param previewSize the current preview size (in pixels) 751 * @param params the current camera parameters (not mutated) 752 * 753 * @return the zoom index, and the effective/reported crop regions (relative to active array) 754 */ convertToLegacyZoom(Rect activeArraySize, Rect cropRegion, Float zoomRatio, Size previewSize, Camera.Parameters params)755 public static ZoomData convertToLegacyZoom(Rect activeArraySize, Rect 756 cropRegion, Float zoomRatio, Size previewSize, Camera.Parameters params) { 757 final float FLOAT_EQUAL_THRESHOLD = 0.0001f; 758 if (zoomRatio != null && 759 Math.abs(1.0f - zoomRatio) > FLOAT_EQUAL_THRESHOLD) { 760 // User uses CONTROL_ZOOM_RATIO to control zoom 761 return convertZoomRatio(activeArraySize, zoomRatio, previewSize, params); 762 } 763 764 return convertScalerCropRegion(activeArraySize, cropRegion, previewSize, params); 765 } 766 767 /** 768 * Convert the user-specified zoom ratio into zoom data; which can be used 769 * to set the parameters to a specific zoom index, or to report back to the user what the 770 * actual zoom was, or for other calculations requiring the current preview crop region. 771 * 772 * <p>None of the parameters are mutated.</p> 773 * 774 * @param activeArraySize active array size of the sensor (e.g. max jpeg size) 775 * @param zoomRatio the current zoom ratio 776 * @param previewSize the current preview size (in pixels) 777 * @param params the current camera parameters (not mutated) 778 * 779 * @return the zoom index, and the effective/reported crop regions (relative to active array) 780 */ convertZoomRatio(Rect activeArraySize, float zoomRatio, Size previewSize, Camera.Parameters params)781 public static ZoomData convertZoomRatio(Rect activeArraySize, float zoomRatio, 782 Size previewSize, Camera.Parameters params) { 783 if (DEBUG) { 784 Log.v(TAG, "convertZoomRatio - user zoom ratio was " + zoomRatio); 785 } 786 787 List<Rect> availableReportedCropRegions = 788 getAvailableZoomCropRectangles(params, activeArraySize); 789 List<Rect> availablePreviewCropRegions = 790 getAvailablePreviewZoomCropRectangles(params, activeArraySize, previewSize); 791 if (availableReportedCropRegions.size() != availablePreviewCropRegions.size()) { 792 throw new AssertionError("available reported/preview crop region size mismatch"); 793 } 794 795 // Find the best matched legacy zoom ratio for the requested camera2 zoom ratio. 796 int bestZoomIndex = 0; 797 Rect reportedCropRegion = new Rect(availableReportedCropRegions.get(0)); 798 Rect previewCropRegion = new Rect(availablePreviewCropRegions.get(0)); 799 float reportedZoomRatio = 1.0f; 800 if (params.isZoomSupported()) { 801 List<Integer> zoomRatios = params.getZoomRatios(); 802 for (int i = 1; i < zoomRatios.size(); i++) { 803 if (zoomRatio * ZOOM_RATIO_MULTIPLIER >= zoomRatios.get(i)) { 804 bestZoomIndex = i; 805 reportedCropRegion = availableReportedCropRegions.get(i); 806 previewCropRegion = availablePreviewCropRegions.get(i); 807 reportedZoomRatio = zoomRatios.get(i); 808 } else { 809 break; 810 } 811 } 812 } 813 814 if (DEBUG) { 815 Log.v(TAG, "convertZoomRatio - zoom calculated to: " + 816 "zoomIndex = " + bestZoomIndex + 817 ", reported crop region = " + reportedCropRegion + 818 ", preview crop region = " + previewCropRegion + 819 ", reported zoom ratio = " + reportedZoomRatio); 820 } 821 822 return new ZoomData(bestZoomIndex, reportedCropRegion, 823 previewCropRegion, reportedZoomRatio); 824 } 825 826 /** 827 * Convert the user-specified crop region into zoom data; which can be used 828 * to set the parameters to a specific zoom index, or to report back to the user what the 829 * actual zoom was, or for other calculations requiring the current preview crop region. 830 * 831 * <p>None of the parameters are mutated.</p> 832 * 833 * @param activeArraySize active array size of the sensor (e.g. max jpeg size) 834 * @param cropRegion the user-specified crop region 835 * @param previewSize the current preview size (in pixels) 836 * @param params the current camera parameters (not mutated) 837 * 838 * @return the zoom index, and the effective/reported crop regions (relative to active array) 839 */ convertScalerCropRegion(Rect activeArraySize, Rect cropRegion, Size previewSize, Camera.Parameters params)840 public static ZoomData convertScalerCropRegion(Rect activeArraySize, Rect 841 cropRegion, Size previewSize, Camera.Parameters params) { 842 Rect activeArraySizeOnly = new Rect( 843 /*left*/0, /*top*/0, 844 activeArraySize.width(), activeArraySize.height()); 845 846 Rect userCropRegion = cropRegion; 847 848 if (userCropRegion == null) { 849 userCropRegion = activeArraySizeOnly; 850 } 851 852 if (DEBUG) { 853 Log.v(TAG, "convertScalerCropRegion - user crop region was " + userCropRegion); 854 } 855 856 final Rect reportedCropRegion = new Rect(); 857 final Rect previewCropRegion = new Rect(); 858 final int zoomIdx = ParameterUtils.getClosestAvailableZoomCrop(params, activeArraySizeOnly, 859 previewSize, userCropRegion, 860 /*out*/reportedCropRegion, /*out*/previewCropRegion); 861 final float reportedZoomRatio = 1.0f; 862 863 if (DEBUG) { 864 Log.v(TAG, "convertScalerCropRegion - zoom calculated to: " + 865 "zoomIndex = " + zoomIdx + 866 ", reported crop region = " + reportedCropRegion + 867 ", preview crop region = " + previewCropRegion + 868 ", reported zoom ratio = " + reportedZoomRatio); 869 } 870 871 return new ZoomData(zoomIdx, previewCropRegion, reportedCropRegion, reportedZoomRatio); 872 } 873 874 /** 875 * Calculate the actual/effective/reported normalized rectangle data from a metering 876 * rectangle. 877 * 878 * <p>If any of the rectangles are out-of-range of their intended bounding box, 879 * the {@link #RECTANGLE_EMPTY empty rectangle} is substituted instead 880 * (with a weight of {@code 0}).</p> 881 * 882 * <p>The metering rectangle is bound by the crop region (effective/reported respectively). 883 * The metering {@link Camera.Area area} is bound by {@code [-1000, 1000]}.</p> 884 * 885 * <p>No parameters are mutated; returns the new metering data.</p> 886 * 887 * @param activeArraySize active array size of the sensor (e.g. max jpeg size) 888 * @param meteringRect the user-specified metering rectangle 889 * @param zoomData the calculated zoom data corresponding to this request 890 * 891 * @return the metering area, the reported/effective metering rectangles 892 */ convertMeteringRectangleToLegacy( Rect activeArray, MeteringRectangle meteringRect, ZoomData zoomData)893 public static MeteringData convertMeteringRectangleToLegacy( 894 Rect activeArray, MeteringRectangle meteringRect, ZoomData zoomData) { 895 Rect previewCrop = zoomData.previewCrop; 896 897 float scaleW = (NORMALIZED_RECTANGLE_MAX - NORMALIZED_RECTANGLE_MIN) * 1.0f / 898 previewCrop.width(); 899 float scaleH = (NORMALIZED_RECTANGLE_MAX - NORMALIZED_RECTANGLE_MIN) * 1.0f / 900 previewCrop.height(); 901 902 Matrix transform = new Matrix(); 903 // Move the preview crop so that top,left is at (0,0), otherwise after scaling 904 // the corner bounds will be outside of [-1000, 1000] 905 transform.setTranslate(-previewCrop.left, -previewCrop.top); 906 // Scale into [0, 2000] range about the center of the preview 907 transform.postScale(scaleW, scaleH); 908 // Move so that top left of a typical rect is at [-1000, -1000] 909 transform.postTranslate(/*dx*/NORMALIZED_RECTANGLE_MIN, /*dy*/NORMALIZED_RECTANGLE_MIN); 910 911 /* 912 * Calculate the preview metering region (effective), and the camera1 api 913 * normalized metering region. 914 */ 915 Rect normalizedRegionUnbounded = ParamsUtils.mapRect(transform, meteringRect.getRect()); 916 917 /* 918 * Try to intersect normalized area with [-1000, 1000] rectangle; otherwise 919 * it's completely out of range 920 */ 921 Rect normalizedIntersected = new Rect(normalizedRegionUnbounded); 922 923 Camera.Area meteringArea; 924 if (!normalizedIntersected.intersect(NORMALIZED_RECTANGLE_DEFAULT)) { 925 Log.w(TAG, 926 "convertMeteringRectangleToLegacy - metering rectangle too small, " + 927 "no metering will be done"); 928 normalizedIntersected.set(RECTANGLE_EMPTY); 929 meteringArea = new Camera.Area(RECTANGLE_EMPTY, 930 MeteringRectangle.METERING_WEIGHT_DONT_CARE); 931 } else { 932 meteringArea = new Camera.Area(normalizedIntersected, 933 meteringRect.getMeteringWeight()); 934 } 935 936 /* 937 * Calculate effective preview metering region 938 */ 939 Rect previewMetering = meteringRect.getRect(); 940 if (!previewMetering.intersect(previewCrop)) { 941 previewMetering.set(RECTANGLE_EMPTY); 942 } 943 944 /* 945 * Calculate effective reported metering region 946 * - Transform the calculated metering area back into active array space 947 * - Clip it to be a subset of the reported crop region 948 */ 949 Rect reportedMetering; 950 { 951 Camera.Area normalizedAreaUnbounded = new Camera.Area( 952 normalizedRegionUnbounded, meteringRect.getMeteringWeight()); 953 WeightedRectangle reportedMeteringRect = convertCameraAreaToActiveArrayRectangle( 954 activeArray, zoomData, normalizedAreaUnbounded, /*usePreviewCrop*/false); 955 reportedMetering = reportedMeteringRect.rect; 956 } 957 958 if (DEBUG) { 959 Log.v(TAG, String.format( 960 "convertMeteringRectangleToLegacy - activeArray = %s, meteringRect = %s, " + 961 "previewCrop = %s, meteringArea = %s, previewMetering = %s, " + 962 "reportedMetering = %s, normalizedRegionUnbounded = %s", 963 activeArray, meteringRect, 964 previewCrop, stringFromArea(meteringArea), previewMetering, 965 reportedMetering, normalizedRegionUnbounded)); 966 } 967 968 return new MeteringData(meteringArea, previewMetering, reportedMetering); 969 } 970 971 /** 972 * Convert the normalized camera area from [-1000, 1000] coordinate space 973 * into the active array-based coordinate space. 974 * 975 * <p>Values out of range are clipped to be within the resulting (reported) crop 976 * region. It is possible to have values larger than the preview crop.</p> 977 * 978 * <p>Weights out of range of [0, 1000] are clipped to be within the range.</p> 979 * 980 * @param activeArraySize active array size of the sensor (e.g. max jpeg size) 981 * @param zoomData the calculated zoom data corresponding to this request 982 * @param area the normalized camera area 983 * 984 * @return the weighed rectangle in active array coordinate space, with the weight 985 */ convertCameraAreaToActiveArrayRectangle( Rect activeArray, ZoomData zoomData, Camera.Area area)986 public static WeightedRectangle convertCameraAreaToActiveArrayRectangle( 987 Rect activeArray, ZoomData zoomData, Camera.Area area) { 988 return convertCameraAreaToActiveArrayRectangle(activeArray, zoomData, area, 989 /*usePreviewCrop*/true); 990 } 991 992 /** 993 * Convert an api1 face into an active-array based api2 face. 994 * 995 * <p>Out-of-ranges scores and ids will be clipped to be within range (with a warning).</p> 996 * 997 * @param face a non-{@code null} api1 face 998 * @param activeArraySize active array size of the sensor (e.g. max jpeg size) 999 * @param zoomData the calculated zoom data corresponding to this request 1000 * 1001 * @return a non-{@code null} api2 face 1002 * 1003 * @throws NullPointerException if the {@code face} was {@code null} 1004 */ convertFaceFromLegacy(Camera.Face face, Rect activeArray, ZoomData zoomData)1005 public static Face convertFaceFromLegacy(Camera.Face face, Rect activeArray, 1006 ZoomData zoomData) { 1007 checkNotNull(face, "face must not be null"); 1008 1009 Face api2Face; 1010 1011 Camera.Area fakeArea = new Camera.Area(face.rect, /*weight*/1); 1012 1013 WeightedRectangle faceRect = 1014 convertCameraAreaToActiveArrayRectangle(activeArray, zoomData, fakeArea); 1015 1016 Point leftEye = face.leftEye, rightEye = face.rightEye, mouth = face.mouth; 1017 if (leftEye != null && rightEye != null && mouth != null && leftEye.x != -2000 && 1018 leftEye.y != -2000 && rightEye.x != -2000 && rightEye.y != -2000 && 1019 mouth.x != -2000 && mouth.y != -2000) { 1020 leftEye = convertCameraPointToActiveArrayPoint(activeArray, zoomData, 1021 leftEye, /*usePreviewCrop*/true); 1022 rightEye = convertCameraPointToActiveArrayPoint(activeArray, zoomData, 1023 leftEye, /*usePreviewCrop*/true); 1024 mouth = convertCameraPointToActiveArrayPoint(activeArray, zoomData, 1025 leftEye, /*usePreviewCrop*/true); 1026 1027 api2Face = faceRect.toFace(face.id, leftEye, rightEye, mouth); 1028 } else { 1029 api2Face = faceRect.toFace(); 1030 } 1031 1032 return api2Face; 1033 } 1034 convertCameraPointToActiveArrayPoint( Rect activeArray, ZoomData zoomData, Point point, boolean usePreviewCrop)1035 private static Point convertCameraPointToActiveArrayPoint( 1036 Rect activeArray, ZoomData zoomData, Point point, boolean usePreviewCrop) { 1037 Rect pointedRect = new Rect(point.x, point.y, point.x, point.y); 1038 Camera.Area pointedArea = new Area(pointedRect, /*weight*/1); 1039 1040 WeightedRectangle adjustedRect = 1041 convertCameraAreaToActiveArrayRectangle(activeArray, 1042 zoomData, pointedArea, usePreviewCrop); 1043 1044 Point transformedPoint = new Point(adjustedRect.rect.left, adjustedRect.rect.top); 1045 1046 return transformedPoint; 1047 } 1048 convertCameraAreaToActiveArrayRectangle( Rect activeArray, ZoomData zoomData, Camera.Area area, boolean usePreviewCrop)1049 private static WeightedRectangle convertCameraAreaToActiveArrayRectangle( 1050 Rect activeArray, ZoomData zoomData, Camera.Area area, boolean usePreviewCrop) { 1051 Rect previewCrop = zoomData.previewCrop; 1052 Rect reportedCrop = zoomData.reportedCrop; 1053 1054 float scaleW = previewCrop.width() * 1.0f / 1055 (NORMALIZED_RECTANGLE_MAX - NORMALIZED_RECTANGLE_MIN); 1056 float scaleH = previewCrop.height() * 1.0f / 1057 (NORMALIZED_RECTANGLE_MAX - NORMALIZED_RECTANGLE_MIN); 1058 1059 /* 1060 * Calculate the reported metering region from the non-intersected normalized region 1061 * by scaling and translating back into active array-relative coordinates. 1062 */ 1063 Matrix transform = new Matrix(); 1064 1065 // Move top left from (-1000, -1000) to (0, 0) 1066 transform.setTranslate(/*dx*/NORMALIZED_RECTANGLE_MAX, /*dy*/NORMALIZED_RECTANGLE_MAX); 1067 1068 // Scale from [0, 2000] back into the preview rectangle 1069 transform.postScale(scaleW, scaleH); 1070 1071 // Move the rect so that the [-1000,-1000] point ends up at the preview [left, top] 1072 transform.postTranslate(previewCrop.left, previewCrop.top); 1073 1074 Rect cropToIntersectAgainst = usePreviewCrop ? previewCrop : reportedCrop; 1075 1076 // Now apply the transformation backwards to get the reported metering region 1077 Rect reportedMetering = ParamsUtils.mapRect(transform, area.rect); 1078 // Intersect it with the crop region, to avoid reporting out-of-bounds 1079 // metering regions 1080 if (!reportedMetering.intersect(cropToIntersectAgainst)) { 1081 reportedMetering.set(RECTANGLE_EMPTY); 1082 } 1083 1084 int weight = area.weight; 1085 if (weight < MeteringRectangle.METERING_WEIGHT_MIN) { 1086 Log.w(TAG, 1087 "convertCameraAreaToMeteringRectangle - rectangle " 1088 + stringFromArea(area) + " has too small weight, clip to 0"); 1089 weight = 0; 1090 } 1091 1092 return new WeightedRectangle(reportedMetering, area.weight); 1093 } 1094 1095 ParameterUtils()1096 private ParameterUtils() { 1097 throw new AssertionError(); 1098 } 1099 } 1100