1 /* 2 * Copyright (C) 2013 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 com.android.camera.settings; 18 19 import com.android.camera.util.ApiHelper; 20 import com.android.ex.camera2.portability.Size; 21 22 import java.math.BigInteger; 23 import java.util.ArrayList; 24 import java.util.Arrays; 25 import java.util.Collections; 26 import java.util.Comparator; 27 import java.util.HashMap; 28 import java.util.LinkedList; 29 import java.util.List; 30 31 /** 32 * This class is used to help manage the many different resolutions available on 33 * the device. <br/> 34 * It allows you to specify which aspect ratios to offer the user, and then 35 * chooses which resolutions are the most pertinent to avoid overloading the 36 * user with so many options. 37 */ 38 public class ResolutionUtil { 39 40 public static final String NEXUS_5_LARGE_16_BY_9 = "1836x3264"; 41 public static final float NEXUS_5_LARGE_16_BY_9_ASPECT_RATIO = 16f / 9f; 42 public static Size NEXUS_5_LARGE_16_BY_9_SIZE = new Size(1836, 3264); 43 44 /** 45 * These are the preferred aspect ratios for the settings. We will take HAL 46 * supported aspect ratios that are within RATIO_TOLERANCE of these values. 47 * We will also take the maximum supported resolution for full sensor image. 48 */ 49 private static Float[] sDesiredAspectRatios = { 50 16.0f / 9.0f, 4.0f / 3.0f 51 }; 52 53 private static Size[] sDesiredAspectRatioSizes = { 54 new Size(16, 9), new Size(4, 3) 55 }; 56 57 private static final float RATIO_TOLERANCE = .05f; 58 59 /** 60 * A resolution bucket holds a list of sizes that are of a given aspect 61 * ratio. 62 */ 63 private static class ResolutionBucket { 64 public Float aspectRatio; 65 /** 66 * This is a sorted list of sizes, going from largest to smallest. 67 */ 68 public List<Size> sizes = new LinkedList<Size>(); 69 /** 70 * This is the head of the sizes array. 71 */ 72 public Size largest; 73 /** 74 * This is the area of the largest size, used for sorting 75 * ResolutionBuckets. 76 */ 77 public Integer maxPixels = 0; 78 79 /** 80 * Use this to add a new resolution to this bucket. It will insert it 81 * into the sizes array and update appropriate members. 82 * 83 * @param size the new size to be added 84 */ add(Size size)85 public void add(Size size) { 86 sizes.add(size); 87 Collections.sort(sizes, new Comparator<Size>() { 88 @Override 89 public int compare(Size size, Size size2) { 90 // sort area greatest to least 91 return Integer.compare(size2.width() * size2.height(), 92 size.width() * size.height()); 93 } 94 }); 95 maxPixels = sizes.get(0).width() * sizes.get(0).height(); 96 } 97 } 98 99 /** 100 * Given a list of camera sizes, this uses some heuristics to decide which 101 * options to present to a user. It currently returns up to 3 sizes for each 102 * aspect ratio. The aspect ratios returned include the ones in 103 * sDesiredAspectRatios, and the largest full sensor ratio. T his guarantees 104 * that users can use a full-sensor size, as well as any of the preferred 105 * aspect ratios from above; 106 * 107 * @param sizes A super set of all sizes to be displayed 108 * @param isBackCamera true if these are sizes for the back camera 109 * @return The list of sizes to display grouped first by aspect ratio 110 * (sorted by maximum area), and sorted within aspect ratio by area) 111 */ getDisplayableSizesFromSupported(List<Size> sizes, boolean isBackCamera)112 public static List<Size> getDisplayableSizesFromSupported(List<Size> sizes, boolean isBackCamera) { 113 List<ResolutionBucket> buckets = parseAvailableSizes(sizes, isBackCamera); 114 115 List<Float> sortedDesiredAspectRatios = new ArrayList<Float>(); 116 // We want to make sure we support the maximum pixel aspect ratio, even 117 // if it doesn't match a desired aspect ratio 118 sortedDesiredAspectRatios.add(buckets.get(0).aspectRatio.floatValue()); 119 120 // Now go through the buckets from largest mp to smallest, adding 121 // desired ratios 122 for (ResolutionBucket bucket : buckets) { 123 Float aspectRatio = bucket.aspectRatio; 124 if (Arrays.asList(sDesiredAspectRatios).contains(aspectRatio) 125 && !sortedDesiredAspectRatios.contains(aspectRatio)) { 126 sortedDesiredAspectRatios.add(aspectRatio); 127 } 128 } 129 130 List<Size> result = new ArrayList<Size>(sizes.size()); 131 for (Float targetRatio : sortedDesiredAspectRatios) { 132 for (ResolutionBucket bucket : buckets) { 133 Number aspectRatio = bucket.aspectRatio; 134 if (Math.abs(aspectRatio.floatValue() - targetRatio) <= RATIO_TOLERANCE) { 135 result.addAll(pickUpToThree(bucket.sizes)); 136 } 137 } 138 } 139 return result; 140 } 141 142 /** 143 * Get the area in pixels of a size. 144 * 145 * @param size the size to measure 146 * @return the area. 147 */ area(Size size)148 private static int area(Size size) { 149 if (size == null) { 150 return 0; 151 } 152 return size.width() * size.height(); 153 } 154 155 /** 156 * Given a list of sizes of a similar aspect ratio, it tries to pick evenly 157 * spaced out options. It starts with the largest, then tries to find one at 158 * 50% of the last chosen size for the subsequent size. 159 * 160 * @param sizes A list of Sizes that are all of a similar aspect ratio 161 * @return A list of at least one, and no more than three representative 162 * sizes from the list. 163 */ pickUpToThree(List<Size> sizes)164 private static List<Size> pickUpToThree(List<Size> sizes) { 165 List<Size> result = new ArrayList<Size>(); 166 Size largest = sizes.get(0); 167 result.add(largest); 168 Size lastSize = largest; 169 for (Size size : sizes) { 170 double targetArea = Math.pow(.5, result.size()) * area(largest); 171 if (area(size) < targetArea) { 172 // This candidate is smaller than half the mega pixels of the 173 // last one. Let's see whether the previous size, or this size 174 // is closer to the desired target. 175 if (!result.contains(lastSize) 176 && (targetArea - area(lastSize) < area(size) - targetArea)) { 177 result.add(lastSize); 178 } else { 179 result.add(size); 180 } 181 } 182 lastSize = size; 183 if (result.size() == 3) { 184 break; 185 } 186 } 187 188 // If we have less than three, we can add the smallest size. 189 if (result.size() < 3 && !result.contains(lastSize)) { 190 result.add(lastSize); 191 } 192 return result; 193 } 194 195 /** 196 * Take an aspect ratio and squish it into a nearby desired aspect ratio, if 197 * possible. 198 * 199 * @param aspectRatio the aspect ratio to fuzz 200 * @return the closest desiredAspectRatio within RATIO_TOLERANCE, or the 201 * original ratio 202 */ fuzzAspectRatio(float aspectRatio)203 private static float fuzzAspectRatio(float aspectRatio) { 204 for (float desiredAspectRatio : sDesiredAspectRatios) { 205 if ((Math.abs(aspectRatio - desiredAspectRatio)) < RATIO_TOLERANCE) { 206 return desiredAspectRatio; 207 } 208 } 209 return aspectRatio; 210 } 211 212 /** 213 * This takes a bunch of supported sizes and buckets them by aspect ratio. 214 * The result is a list of buckets sorted by each bucket's largest area. 215 * They are sorted from largest to smallest. This will bucket aspect ratios 216 * that are close to the sDesiredAspectRatios in to the same bucket. 217 * 218 * @param sizes all supported sizes for a camera 219 * @param isBackCamera true if these are sizes for the back camera 220 * @return all of the sizes grouped by their closest aspect ratio 221 */ parseAvailableSizes(List<Size> sizes, boolean isBackCamera)222 private static List<ResolutionBucket> parseAvailableSizes(List<Size> sizes, boolean isBackCamera) { 223 HashMap<Float, ResolutionBucket> aspectRatioToBuckets = new HashMap<Float, ResolutionBucket>(); 224 225 for (Size size : sizes) { 226 Float aspectRatio = size.width() / (float) size.height(); 227 // If this aspect ratio is close to a desired Aspect Ratio, 228 // fuzz it so that they are bucketed together 229 aspectRatio = fuzzAspectRatio(aspectRatio); 230 ResolutionBucket bucket = aspectRatioToBuckets.get(aspectRatio); 231 if (bucket == null) { 232 bucket = new ResolutionBucket(); 233 bucket.aspectRatio = aspectRatio; 234 aspectRatioToBuckets.put(aspectRatio, bucket); 235 } 236 bucket.add(size); 237 } 238 if (ApiHelper.IS_NEXUS_5 && isBackCamera) { 239 aspectRatioToBuckets.get(16 / 9.0f).add(NEXUS_5_LARGE_16_BY_9_SIZE); 240 } 241 List<ResolutionBucket> sortedBuckets = new ArrayList<ResolutionBucket>( 242 aspectRatioToBuckets.values()); 243 Collections.sort(sortedBuckets, new Comparator<ResolutionBucket>() { 244 @Override 245 public int compare(ResolutionBucket resolutionBucket, ResolutionBucket resolutionBucket2) { 246 return Integer.compare(resolutionBucket2.maxPixels, resolutionBucket.maxPixels); 247 } 248 }); 249 return sortedBuckets; 250 } 251 252 /** 253 * Given a size, return a string describing the aspect ratio by reducing the 254 * 255 * @param size the size to describe 256 * @return a string description of the aspect ratio 257 */ aspectRatioDescription(Size size)258 public static String aspectRatioDescription(Size size) { 259 Size aspectRatio = reduce(size); 260 return aspectRatio.width() + "x" + aspectRatio.height(); 261 } 262 263 /** 264 * Reduce an aspect ratio to its lowest common denominator. The ratio of the 265 * input and output sizes is guaranteed to be the same. 266 * 267 * @param aspectRatio the aspect ratio to reduce 268 * @return The reduced aspect ratio which may equal the original. 269 */ reduce(Size aspectRatio)270 public static Size reduce(Size aspectRatio) { 271 BigInteger width = BigInteger.valueOf(aspectRatio.width()); 272 BigInteger height = BigInteger.valueOf(aspectRatio.height()); 273 BigInteger gcd = width.gcd(height); 274 int numerator = Math.max(width.intValue(), height.intValue()) / gcd.intValue(); 275 int denominator = Math.min(width.intValue(), height.intValue()) / gcd.intValue(); 276 return new Size(numerator, denominator); 277 } 278 279 /** 280 * Given a size return the numerator of its aspect ratio 281 * 282 * @param size the size to measure 283 * @return the numerator 284 */ aspectRatioNumerator(Size size)285 public static int aspectRatioNumerator(Size size) { 286 Size aspectRatio = reduce(size); 287 return aspectRatio.width(); 288 } 289 290 /** 291 * Given a size, return the closest aspect ratio that falls close to the 292 * given size. 293 * 294 * @param size the size to approximate 295 * @return the closest desired aspect ratio, or the original aspect ratio if 296 * none were close enough 297 */ getApproximateSize(Size size)298 public static Size getApproximateSize(Size size) { 299 Size aspectRatio = reduce(size); 300 float fuzzy = fuzzAspectRatio(size.width() / (float) size.height()); 301 int index = Arrays.asList(sDesiredAspectRatios).indexOf(fuzzy); 302 if (index != -1) { 303 aspectRatio = new Size(sDesiredAspectRatioSizes[index]); 304 } 305 return aspectRatio; 306 } 307 308 /** 309 * See {@link #getApproximateSize(Size)}. 310 * <p> 311 * TODO: Move this whole util to {@link android.util.Size} 312 */ getApproximateSize( com.android.camera.util.Size size)313 public static com.android.camera.util.Size getApproximateSize( 314 com.android.camera.util.Size size) { 315 Size result = getApproximateSize(new Size(size.getWidth(), size.getHeight())); 316 return new com.android.camera.util.Size(result.width(), result.height()); 317 } 318 319 /** 320 * Given a size return the numerator of its aspect ratio 321 * 322 * @param size 323 * @return the denominator 324 */ aspectRatioDenominator(Size size)325 public static int aspectRatioDenominator(Size size) { 326 BigInteger width = BigInteger.valueOf(size.width()); 327 BigInteger height = BigInteger.valueOf(size.height()); 328 BigInteger gcd = width.gcd(height); 329 int denominator = Math.min(width.intValue(), height.intValue()) / gcd.intValue(); 330 return denominator; 331 } 332 333 } 334