/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.camera.settings; import com.android.camera.util.ApiHelper; import com.android.ex.camera2.portability.Size; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.LinkedList; import java.util.List; /** * This class is used to help manage the many different resolutions available on * the device.
* It allows you to specify which aspect ratios to offer the user, and then * chooses which resolutions are the most pertinent to avoid overloading the * user with so many options. */ public class ResolutionUtil { public static final String NEXUS_5_LARGE_16_BY_9 = "1836x3264"; public static final float NEXUS_5_LARGE_16_BY_9_ASPECT_RATIO = 16f / 9f; public static Size NEXUS_5_LARGE_16_BY_9_SIZE = new Size(1836, 3264); /** * These are the preferred aspect ratios for the settings. We will take HAL * supported aspect ratios that are within RATIO_TOLERANCE of these values. * We will also take the maximum supported resolution for full sensor image. */ private static Float[] sDesiredAspectRatios = { 16.0f / 9.0f, 4.0f / 3.0f }; private static Size[] sDesiredAspectRatioSizes = { new Size(16, 9), new Size(4, 3) }; private static final float RATIO_TOLERANCE = .05f; /** * A resolution bucket holds a list of sizes that are of a given aspect * ratio. */ private static class ResolutionBucket { public Float aspectRatio; /** * This is a sorted list of sizes, going from largest to smallest. */ public List sizes = new LinkedList(); /** * This is the head of the sizes array. */ public Size largest; /** * This is the area of the largest size, used for sorting * ResolutionBuckets. */ public Integer maxPixels = 0; /** * Use this to add a new resolution to this bucket. It will insert it * into the sizes array and update appropriate members. * * @param size the new size to be added */ public void add(Size size) { sizes.add(size); Collections.sort(sizes, new Comparator() { @Override public int compare(Size size, Size size2) { // sort area greatest to least return Integer.compare(size2.width() * size2.height(), size.width() * size.height()); } }); maxPixels = sizes.get(0).width() * sizes.get(0).height(); } } /** * Given a list of camera sizes, this uses some heuristics to decide which * options to present to a user. It currently returns up to 3 sizes for each * aspect ratio. The aspect ratios returned include the ones in * sDesiredAspectRatios, and the largest full sensor ratio. T his guarantees * that users can use a full-sensor size, as well as any of the preferred * aspect ratios from above; * * @param sizes A super set of all sizes to be displayed * @param isBackCamera true if these are sizes for the back camera * @return The list of sizes to display grouped first by aspect ratio * (sorted by maximum area), and sorted within aspect ratio by area) */ public static List getDisplayableSizesFromSupported(List sizes, boolean isBackCamera) { List buckets = parseAvailableSizes(sizes, isBackCamera); List sortedDesiredAspectRatios = new ArrayList(); // We want to make sure we support the maximum pixel aspect ratio, even // if it doesn't match a desired aspect ratio sortedDesiredAspectRatios.add(buckets.get(0).aspectRatio.floatValue()); // Now go through the buckets from largest mp to smallest, adding // desired ratios for (ResolutionBucket bucket : buckets) { Float aspectRatio = bucket.aspectRatio; if (Arrays.asList(sDesiredAspectRatios).contains(aspectRatio) && !sortedDesiredAspectRatios.contains(aspectRatio)) { sortedDesiredAspectRatios.add(aspectRatio); } } List result = new ArrayList(sizes.size()); for (Float targetRatio : sortedDesiredAspectRatios) { for (ResolutionBucket bucket : buckets) { Number aspectRatio = bucket.aspectRatio; if (Math.abs(aspectRatio.floatValue() - targetRatio) <= RATIO_TOLERANCE) { result.addAll(pickUpToThree(bucket.sizes)); } } } return result; } /** * Get the area in pixels of a size. * * @param size the size to measure * @return the area. */ private static int area(Size size) { if (size == null) { return 0; } return size.width() * size.height(); } /** * Given a list of sizes of a similar aspect ratio, it tries to pick evenly * spaced out options. It starts with the largest, then tries to find one at * 50% of the last chosen size for the subsequent size. * * @param sizes A list of Sizes that are all of a similar aspect ratio * @return A list of at least one, and no more than three representative * sizes from the list. */ private static List pickUpToThree(List sizes) { List result = new ArrayList(); Size largest = sizes.get(0); result.add(largest); Size lastSize = largest; for (Size size : sizes) { double targetArea = Math.pow(.5, result.size()) * area(largest); if (area(size) < targetArea) { // This candidate is smaller than half the mega pixels of the // last one. Let's see whether the previous size, or this size // is closer to the desired target. if (!result.contains(lastSize) && (targetArea - area(lastSize) < area(size) - targetArea)) { result.add(lastSize); } else { result.add(size); } } lastSize = size; if (result.size() == 3) { break; } } // If we have less than three, we can add the smallest size. if (result.size() < 3 && !result.contains(lastSize)) { result.add(lastSize); } return result; } /** * Take an aspect ratio and squish it into a nearby desired aspect ratio, if * possible. * * @param aspectRatio the aspect ratio to fuzz * @return the closest desiredAspectRatio within RATIO_TOLERANCE, or the * original ratio */ private static float fuzzAspectRatio(float aspectRatio) { for (float desiredAspectRatio : sDesiredAspectRatios) { if ((Math.abs(aspectRatio - desiredAspectRatio)) < RATIO_TOLERANCE) { return desiredAspectRatio; } } return aspectRatio; } /** * This takes a bunch of supported sizes and buckets them by aspect ratio. * The result is a list of buckets sorted by each bucket's largest area. * They are sorted from largest to smallest. This will bucket aspect ratios * that are close to the sDesiredAspectRatios in to the same bucket. * * @param sizes all supported sizes for a camera * @param isBackCamera true if these are sizes for the back camera * @return all of the sizes grouped by their closest aspect ratio */ private static List parseAvailableSizes(List sizes, boolean isBackCamera) { HashMap aspectRatioToBuckets = new HashMap(); for (Size size : sizes) { Float aspectRatio = size.width() / (float) size.height(); // If this aspect ratio is close to a desired Aspect Ratio, // fuzz it so that they are bucketed together aspectRatio = fuzzAspectRatio(aspectRatio); ResolutionBucket bucket = aspectRatioToBuckets.get(aspectRatio); if (bucket == null) { bucket = new ResolutionBucket(); bucket.aspectRatio = aspectRatio; aspectRatioToBuckets.put(aspectRatio, bucket); } bucket.add(size); } if (ApiHelper.IS_NEXUS_5 && isBackCamera) { aspectRatioToBuckets.get(16 / 9.0f).add(NEXUS_5_LARGE_16_BY_9_SIZE); } List sortedBuckets = new ArrayList( aspectRatioToBuckets.values()); Collections.sort(sortedBuckets, new Comparator() { @Override public int compare(ResolutionBucket resolutionBucket, ResolutionBucket resolutionBucket2) { return Integer.compare(resolutionBucket2.maxPixels, resolutionBucket.maxPixels); } }); return sortedBuckets; } /** * Given a size, return a string describing the aspect ratio by reducing the * * @param size the size to describe * @return a string description of the aspect ratio */ public static String aspectRatioDescription(Size size) { Size aspectRatio = reduce(size); return aspectRatio.width() + "x" + aspectRatio.height(); } /** * Reduce an aspect ratio to its lowest common denominator. The ratio of the * input and output sizes is guaranteed to be the same. * * @param aspectRatio the aspect ratio to reduce * @return The reduced aspect ratio which may equal the original. */ public static Size reduce(Size aspectRatio) { BigInteger width = BigInteger.valueOf(aspectRatio.width()); BigInteger height = BigInteger.valueOf(aspectRatio.height()); BigInteger gcd = width.gcd(height); int numerator = Math.max(width.intValue(), height.intValue()) / gcd.intValue(); int denominator = Math.min(width.intValue(), height.intValue()) / gcd.intValue(); return new Size(numerator, denominator); } /** * Given a size return the numerator of its aspect ratio * * @param size the size to measure * @return the numerator */ public static int aspectRatioNumerator(Size size) { Size aspectRatio = reduce(size); return aspectRatio.width(); } /** * Given a size, return the closest aspect ratio that falls close to the * given size. * * @param size the size to approximate * @return the closest desired aspect ratio, or the original aspect ratio if * none were close enough */ public static Size getApproximateSize(Size size) { Size aspectRatio = reduce(size); float fuzzy = fuzzAspectRatio(size.width() / (float) size.height()); int index = Arrays.asList(sDesiredAspectRatios).indexOf(fuzzy); if (index != -1) { aspectRatio = new Size(sDesiredAspectRatioSizes[index]); } return aspectRatio; } /** * See {@link #getApproximateSize(Size)}. *

* TODO: Move this whole util to {@link android.util.Size} */ public static com.android.camera.util.Size getApproximateSize( com.android.camera.util.Size size) { Size result = getApproximateSize(new Size(size.getWidth(), size.getHeight())); return new com.android.camera.util.Size(result.width(), result.height()); } /** * Given a size return the numerator of its aspect ratio * * @param size * @return the denominator */ public static int aspectRatioDenominator(Size size) { BigInteger width = BigInteger.valueOf(size.width()); BigInteger height = BigInteger.valueOf(size.height()); BigInteger gcd = width.gcd(height); int denominator = Math.min(width.intValue(), height.intValue()) / gcd.intValue(); return denominator; } }