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