保持纵横比的正确相机预览尺寸计算方法

8

问题

如何编程地并准确地确定在设备屏幕大小(或任何可变尺寸的视图内)显示相机预览的最佳预览大小?

在您试图将其标记为SO上数百万其他宽高比相关问题之一之前,请理解我正在寻找与通常可用解决方案不同的解决方案。 我提出这个问题是因为我已经阅读了很多“答案”,但它们都指向了一个我认为不完整的解决方案(并且可能存在缺陷,正如我将在这里描述的那样)。 如果这不是一个缺陷,请帮助我理解我做错了什么。

我已经阅读了很多关于应用程序如何选择预览大小的不同实现,并且其中大多数采用了我称之为“趋近”的方法(通过从尺寸选项的比率中减去屏幕分辨率的比率,并找到具有最低值的选项来确定最佳选项)。 这种方法似乎不能确保选择了最佳选项,但确保不会选择最差选项

例如,如果我尝试在屏幕分辨率为720x1184的设备上迭代遍历每个可用的预览大小,并在全屏幕(720x1184)显示预览,则相机预览的结果如下(按选项与屏幕分辨率比例最接近的格式abs(ratio of size option - screen resolution ratio)排序)。 所有大小均来自.getSupportedPreviewSizes。(“结果”是使用出现在相机取景器中的静态圆形的测试用例进行视觉观察得出的,而非以编程方式)
720.0/1184.0 = 0.6081081081081081

Res         Ratio         Delta in ratios  Result       
----------------------------------------------------   
176/144   = 1.22222222222 (0.614114114114) (vertically stretched)
352/288   = 1.22222222222 (0.614114114114) (vertically stretched)
320/240   = 1.33333333333 (0.725225225225) (vertically stretched)
640/480   = 1.33333333333 (0.725225225225) (vertically stretched)
720/480   = 1.5           (0.891891891892) (vertically stretched)
800/480   = 1.66666666667 (1.05855855856)  (looks good)
640/360   = 1.77777777778 (1.16966966967)  (horizontally squashed)
1280/720  = 1.77777777778 (1.16966966967)  (slight horizontal squash)
1920/1080 = 1.77777777778 (1.16966966967)  (slight horizontal squash) 

这是一份关于编程的 Android 代码测试,为了确保正确性,需要在另一台设备上运行。下面是在分辨率为800x1216的设备上显示相机预览并以同样的分辨率(800x1216)显示预览的结果。
800/1216.0 = 0.657894736842

Res         Ratio         Delta in ratios  Results
------------------------------------------------
176/144   = 1.22222222222 (0.56432748538)  (looks vertically stretched)
352/288   = 1.22222222222 (0.56432748538)  (looks vertically stretched)
480/368   = 1.30434782609 (0.646453089245) (looks vertically stretched)
320/240   = 1.33333333333 (0.675438596491) (looks vertically stretched)
640/480   = 1.33333333333 (0.675438596491) (looks vertically stretched)
800/600   = 1.33333333333 (0.675438596491) (looks vertically stretched)
480/320   = 1.5           (0.842105263158) (looks good)
720/480   = 1.5           (0.842105263158) (looks good)
800/480   = 1.66666666667 (1.00877192982)  (looks horizontally squashed)
960/540   = 1.77777777778 (1.11988304094)  (looks horizontally squashed)
1280/720  = 1.77777777778 (1.11988304094)  (looks horizontally squashed)
1920/1080 = 1.77777777778 (1.11988304094)  (looks horizontally squashed)
864/480   = 1.8           (1.14210526316)  (looks horizontally squashed)

“差不多”方法(假设任何比率的差异都等于或小于1.4d是可以接受的)将在从最低值到最高值迭代时,在两个设备上都返回1920x1080。如果在从最高值到最低值迭代中,它会为DeviceA选择176x144,并为DeviceB选择176x144。这两个选项(虽然“差不多”),都不是最佳选项。

问题

通过研究上述结果,我如何以编程方式推导出“看起来好”的值?我不能使用“差不多”方法得到这些值,因此我误解了屏幕大小、我正在显示预览的视图和预览大小之间的关系。我错过了什么?

           Screen dimensions      = 720x1184
             View dimensions      = 720x1184
Screen and View Aspect Ratio      = 0.6081081081081081
Best Preview Size ratio (720x480) = 1.5  

为什么最佳选择不是具有最低比率差的值? 结果令人惊讶,因为其他人似乎认为计算比率最小差异的最佳选项,但我看到的是最佳选项似乎在所有选项的中间。而且它们的宽度更接近将显示预览的视图的宽度。
基于以上观察(最佳选项不是具有最低比率差的值),我开发了这个算法,迭代所有可能的预览尺寸,检查是否符合我的“足够接近”标准,存储符合此标准的尺寸,最后找到至少大于或等于提供的宽度的值。
public static Size getBestAspectPreviewSize(int displayOrientation,
                                            int width,
                                            int height,
                                            Camera.Parameters parameters,
                                            double closeEnough) {


    double targetRatio=(double)width / height;
    Size bestSize = null;

    if (displayOrientation == 90 || displayOrientation == 270) {
        targetRatio=(double)height / width;
    }

    List<Size> sizes=parameters.getSupportedPreviewSizes();
    TreeMap<Double, List> diffs = new TreeMap<Double, List>();


    for (Size size : sizes) {

        double ratio=(double)size.width / size.height;

        double diff = Math.abs(ratio - targetRatio);
        if (diff < closeEnough){
            if (diffs.keySet().contains(diff)){
                //add the value to the list
                diffs.get(diff).add(size);
            } else {
                List newList = new ArrayList<Camera.Size>();
                newList.add(size);
                diffs.put(diff, newList);
            }

            Logging.format("usable: %sx%s %s", size.width, size.height, diff);
        }
    }

    //diffs now contains all of the usable sizes
    //now let's see which one has the least amount of
    for (Map.Entry entry: diffs.entrySet()){
        List<Size> entries = (List)entry.getValue();
        for (Camera.Size s: entries) {

            if (s.width >= width && s.height >= width) {
                bestSize = s;
            }
            Logging.format("results: %s %sx%s", entry.getKey(), s.width, s.height);
        }
    }

    //if we don't have bestSize then just use whatever the default was to begin with
    if (bestSize==null){
        if (parameters.getPreviewSize()!=null){
            bestSize = parameters.getPreviewSize();
            return bestSize;
        }

        //pick the smallest difference in ratio?  or pick the largest resolution?
        //right now we are just picking the lowest ratio difference
        for (Map.Entry entry: diffs.entrySet()){
            List<Size> entries = (List)entry.getValue();
            for (Camera.Size s: entries) {
                if (bestSize == null){
                    bestSize = s;
                }
            }
        }
    }

    return bestSize;
}

显然,这个算法不知道如何选择最佳选项,只知道选择一个不是最差的选项,就像我在其他实现中看到的一样。在我改进算法以实际选择最佳选项之前,我需要了解实际看起来好的尺寸与预览将显示的视图尺寸之间的关系。
我已经查看了CommonWares' camera-cwac项目处理此问题的实现,它似乎也使用“足够接近”的算法。如果我将同样的逻辑应用于我的项目,那么我会得到相当不错但不是“完美”的尺寸值。对于两个设备,我都得到了1920x1080。虽然这个值不是最差的选项,但也稍微有些压缩。我将在我的测试应用程序中运行他的代码,并进行测试用例以确定它是否也会轻微地压缩图像,因为我已经知道它会返回一个不太理想的尺寸。

我认为你需要重新计算显示比例和增量,因为显示器处于纵向模式。 - nana
2个回答

4
为什么最佳选项不是比率中具有最低delta值的数值?
确实是这样!但您需要根据当前屏幕方向计算您的屏幕比率! :)
因此,由于您的设备处于横向模式,因此第一个表格应如下所示:
1184.0/720.0 = 1.6444444

Res         Ratio         Delta in ratios  Result       
----------------------------------------------------   
176/144   = 1.222 ( 0.422) (vertically stretched)
352/288   = 1.222 ( 0.422) (vertically stretched)
320/240   = 1.333 ( 0.311) (vertically stretched)
640/480   = 1.333 ( 0.311) (vertically stretched)
720/480   = 1.500 ( 0.144) (vertically stretched)
800/480   = 1.666 (-0.016) (looks good)
640/360   = 1.777 (-0.126) (horizontally squashed)
1280/720  = 1.777 (-0.126) (slight horizontal squash)
1920/1080 = 1.777 (-0.126) (slight horizontal squash) 

看到了吗?最小的绝对差是你最好的选择。

我不清楚这个。但我非常有信心它__不是所有设备的默认设置__。你可以查询设备的方向。 - nana
好的,你应该对 delta 值取绝对值。如果差异较小,你才关心它。 - nana
这是在运行时获取屏幕方向的方法:getSystemService(WINDOW_SERVICE)).getDefaultDisplay().getOrientation(); - nana
横屏/竖屏模式更加复杂,一些平板电脑(如三星...)默认为竖屏模式(像手机一样),而一些(Nexus7-1Gen、Nexus10)则默认为横屏模式。我提到的Github示例处理了这个问题:https://github.com/seanpjanson/AndyCam/blob/master/src/main/java/com/andyscan/andycam/Util.java#L53。此外,在'surfaceChanged()'中的预览大小是横向的:https://github.com/seanpjanson/AndyCam/blob/master/src/main/java/com/andyscan/andycam/CamVw.java#L56,因此您必须预先旋转视图并后置旋转图片。 - seanpj
通常,平板电脑上的横向/纵向问题可以从显示尺寸(宽度/高度)和平板电脑在任何给定时刻的旋转之间的差异中解决。真是一团糟。 - seanpj
显示剩余2条评论

1
你永远不会得到一个“完美适合”的比例。由于其他屏幕元素的存在(标题栏、旋转等),不同设备上的表面将发生变化。你必须选择最接近的比例,并将预览定位在屏幕上,使其在两侧溢出或留下(黑色)条带(居中)。查看此 GitHub exercise ,我尝试解决这个问题。最好的方法是让预览“溢出”显示表面。
此外,'takePicture'获取的图片大小和比例也不同。如果您需要将结果图片裁剪为预览比例,有一种方法可以实现(我提到的示例使用了不同的方法)。但是,您试图解决的问题无法完美解决。例如,如果您的表面积为1000x800,最接近的可用预览比例为1200x800,则必须选择该比例并在此处设置'fit-in'为false。现在,您将看到所选预览大小的裁剪部分(每侧溢出100个像素,但谁关心,它不可见)。
当你稍后获得图片时,你需要选择适合你需求的尺寸。不仅仅是最接近的比例,还包括图片本身的大小。例如,如果你选择了2000x1500的图片大小,并且你只想保留屏幕上可见的部分,那么你需要将其裁剪成你表面(5:4)的1000x800比例,得到的图片尺寸为1875x1500。或者,例如,如果你的内存不足并且使用图片进行上传/查看,则可以选择较小的图片尺寸,如800x600,并将其裁剪为750x600。
但要注意,我提到的GitHub示例是相反的,它试图获取所需尺寸为1600x1200(4:3比例)的最终图片。
我希望我的回答能够帮助你解决一些无法简单回答的问题。仅仅是Camera API在这里有这么多问题,就表明解决方案并不完美。

超出视图范围似乎是一个不错的选择,因为图像不会变形 - 唯一棘手的部分在于裁剪最终图像。在我的场景中,我有一个正方形的FrameLayout,充当“掩模”,只允许用户看到相机的SurfaceView的正方形部分(类似Instagram相机视图)。它将始终是一个正方形,因此最终图像的尺寸将为“屏幕宽度x屏幕宽度”。假设我的预览大小和图片大小不同,您能指导我如何裁剪图像吗? - Max Worg
因此,我将始终以设备的完整分辨率显示我的预览,但用户只会看到其中的一个正方形部分。 - Max Worg
@MaxWorg 假设您正在运行Android Studio,请尝试拉取GitHub示例(我相信FIT_IN设置为“true”),选择1:1的预览(希望相机有一个),并玩弄此代码部分:https://github.com/seanpjanson/AndyCam/blob/master/src/main/java/com/andyscan/andycam/CamVw.java#L54 - seanpj
如果您找不到1.000的比例,请选择最接近的一个,并使用正方形模板(FrameLayout 包含 SurfaceView + 具有方形透明开口的Imageview)遮盖表面。很抱歉我无法提供更多帮助,因为我正在忙于完全无关的Android问题。 - seanpj

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接