iOS 二维码有效区域rectOfInterest详解

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS 二维码有效区域rectOfInterest详解相关的知识,希望对你有一定的参考价值。

参考技术A demo

rectOfInterest跟2个属性息息相关:一个是AVCaptureSession(会话对象)的sessionPreset属性,另一个是AVCaptureVideoPreviewLayer(预览图层)的videoGravity属性。在这里,我简单讲一下这2个属性的意义:

该属性是设置图像音频等输出分辨率,大约一共有11个:

该属性共有3个值:如果你不了解,我建议你先去熟悉一下UIView的contentMode属性,光了解没有用,必须知道它的原理以及计算方式

一般的,扫描区域就是预览视图previewLayer的frame对应的矩形框,一般是设置全屏。如果我们想要设置一个有效区域怎么办,如同支付宝、微信等将扫码区域限制在一个小正方形内。这就要用到输出流AVCaptureMetadataOutput的一个rectOfInterest属性。

大家应该提出质疑:为什么宽高才为1?这也太小了吧,然而这个区域却是全屏。这是肿么肥四呢? 聪明的你应该猜到了,rectOfInterest肯定是经过某种转化而来,而且x,y, w, h的范围均在0~1之间。究竟是如何转化的,且听我慢慢说给你听:
假如在手机屏幕中,我想限制有效扫描区域在矩形框(10,10,100,100)内,是不是这样设置:

这样对吗?肯定不对咯,因为还没有转化为0~1的范围内呢。
好的,我们一起来转化一下,由于图像都是显示在预览视图previewLayer中,所以自然是通过previewLayer的frame来转化.

到此,转化结束!就这样完了吗?还早着呢!

我问大家一个问题:矩形框rectOfInterest=(0,0,1,1)应该在屏幕的哪个位置?
大家应该会回答在屏幕的左上方,没错,不仅是你,就连官方文档的解释都是这样说的,官方文档说:

好。如果按照我们的想象或者官方文档所说,我们设置的有效区域:(10 ,10 , 100 ,100)应该会偏左上方。然而,结果并非如此,显示结果是这样的:

有没有发现,显示结果和我们想的完全相反,偏右上角,也就是说:

既然我们已经知道了坐标原点,那么我们想让扫描区域(10 ,10 , 100 ,100)显示在左上方,就不是难事了。

如上图:左边红色矩形框就是我们实际要的扫描区域所在位置,最关键是要求出图中蓝点相对原点(右上角)的坐标。

// 由此,我们可以推导出一个转化公式:

到了这里, 离成功似乎很近了,但是很遗憾, 漫长的路才刚起步!用此公式代入计算,发现扫码区域完全不对,好桑心,为什么会这样?于是猜想: AVCapture输出的图片大小都是横着的,而iPhone的屏幕是竖着的,那么我把它旋转90°呢:
旋转90°也就意味着x与y互换,w和h互换,即:rectOfInterest的x, y, w , h 应该对应y, x , h, w;转换如下:

// 这个公式上升了一个级别,有了一定的正确性,但是它太“死”了,不够灵活,也就是说,假如我随意更换设备,随意修改sessionPreset和videoGravity属性的话,此公式计算出来的扫描区域是不准确的。这下该怎么办,我差点就要放弃了,到这里就结束算了,但是心里总感觉有点希望,于是彻夜都在想这个问题。
大家还记得我开篇讲的sessionPreset和videoGravity属性吧,在这里,这俩属性就要闪亮登场了。

既然是相对图像,由于图像的输出有多种模式,这些模式通过AVCaptureVideoPreviewLayer的videoGravity属性设置,如AVLayerVideoGravityResizeAspectFill;由于这些模式的设置,导致图像会被裁减、留白或者拉伸,所以我们计算出来的结果是相对图像而言的,我们需要将其转化到预览图层AVCaptureVideoPreviewLayer上来。所以我开始要求大家去熟悉一下UIView的contentMode模式。
我不废话了,我直接上转化过程,我将其封装成了一个方法.

上面那个公式就是最终的转化公式,有一点要声明一下,当开发者设置输出分辨率为AVCaptureSessionPresetHigh、AVCaptureSessionPresetMedium、AVCaptureSessionPresetLow、AVCaptureSessionPresetPhoto等不确定性分辨率时,我都是默认给了一个对应的明确的分辨率,例如AVCaptureSessionPresetHigh我计算时采用的是1920x1080,因为我测试时是采用的iPhone6s,其他机型未必是这个分辨率,所以当分辨率取决于设备时,你自己需要根据设备的不同去修改一下。本人没有那么多真机,所以我无法给出通用的答案。

我想有人肯定一直在怀疑,为什么不用系统自带的metadataOutputRectOfInterestForRect方法,这个方法就是我上面那个公式的功能啊,甚至更权威。但是,试了就知道,metadataOutputRectOfInterestForRect在输入流格式发生变化之前设置是无效的,你需要监听一个通知:AVCaptureInputPortFormatDescriptionDidChangeNotification,在通知方法中调用metadataOutputRectOfInterestForRect才起作用,或者你开启扫码startRunning之后再设置也行,这些做法确实也能计算出扫码有效区域,但是会卡顿,开启扫描之后,总是会卡一下,才开始扫描,这非常影响用户体验,所以不建议使用。

你可以用我的公式和采用系统的metadataOutputRectOfInterestForRect方法的转化结果对比一下,你会发现结果的差距非常微妙,只有零点零零几的误差

有效查找高密度区域的最佳方法

【中文标题】有效查找高密度区域的最佳方法【英文标题】:Best way to efficiently find high density regions 【发布时间】:2013-11-01 05:43:15 【问题描述】:

在我的编码过程中,我遇到了如下问题: 在二维空间中找到具有最高粒子密度的固定大小的区域。粒子一般可以认为是随机分布在整个空间中,但理论上应该有一些区域密度较高。

例如,100 个粒子随机放置在 500x500 的 2D 网格中,我需要找到 50x50 的区域,其中粒子最多(密度最高)。

除了蛮力测试每个可能的区域(在这种情况下大约超过 200000 个区域)之外,还有其他方法来计算最佳区域吗?对于 n 长度的轴,这将在 O(n^2) 时按比例放大。

【问题讨论】:

@MillieSmith:这是个好主意,但除非您知道需要多少查询,否则它不是很有帮助。 这和这个问题完全一样:poj.org/problem?id=1916 最好的解决方案是O(N^2 + K*D^2) 其中K 是粒子数(在你的情况下是100),D 是区域的大小(在您的情况下为 50),如以下 C++ 代码所示:tausiq.wordpress.com/2012/08/24/uva-10360-rat-attack 【参考方案1】:

算法 1

创建一个 500x500 二维数组,其中每个单元格包含该单元格中粒子数的计数。然后将该数组与 50x50 内核卷积,生成的数组将包含每个单元格中 50x50 区域中的粒子数。然后找到具有最大值的单元格。

如果您使用 50x50 的框作为区域,内核可以分解为两个独立的卷积,每个轴一个。生成的算法是 O(n^2) 空间和时间,其中 n 是您正在搜索的 2D 空间的宽度和高度。

提醒一下,一个带有 boxcar 函数的一维卷积可以在 O(n) 的时间和空间内完成,并且可以就地完成。设 x(t) 为 t=1..n 的输入,设 y(t) 为输出。为 tn 定义 x(t)=0 和 y(t)=0。将内核 f(t) 定义为 0..d-1 的 1 和其他地方的 0。卷积的定义给了我们以下公式:

y(t) = sum i x(t-i) * f(i) = sum i=0..d-1 x(t-i)

这看起来需要时间 O(n*d),但我们可以将其重写为循环:

y(t) = y(t-1) + x(t) - x(t-d)

这说明一维卷积是O(n),与d无关。要执行二维卷积,您只需对每个轴执行一维卷积。之所以可行,是因为可以分解 boxcar 内核:通常,大多数内核无法分解。高斯核是另一个可以分解的核,这也是图像编辑程序中的高斯模糊速度如此之快的原因。

对于您指定的数字类型,这将非常快。 500x500 是一个极小的数据集,你的计算机最多可以在几毫秒内检查 202,500 个区域。您将不得不问自己,是否值得额外花费数小时、数天或数周的时间来进一步优化。

这和justhalf的解法一样,只是由于分解了卷积,所以区域大小不影响算法的速度。

算法 2

假设至少有一个点。不失一般性,将二维空间视为整个平面。令 d 为区域的宽度和高度。设 N 为点数。

引理:存在一个密度最大的区域,其左边缘有一个点。

证明:令 R 为最大密度区域。设 R' 是同一个区域,向右平移 R 的左边缘和 R 中最左边的点之间的距离。R 中的所有点也必须位于 R' 中,因此 R' 也是最大密度的区域。

算法

    将所有点插入到 K-D 树中。这可以在 O(N log2 N) 时间内完成。

    对于每个点,考虑宽度为 d 和高度为 2d 的区域,其中该点位于该区域的左边缘。将此区域称为 R。

    在 KD 树中查询区域 R 中的点。将此集合称为 S。这可以在 O(N1/2+|S|) 时间内完成。

    找到 R 的 dxd 子区域,其中包含 S 中最大数量的点。这可以在 O(|S| log |S|) 时间内完成,方法是按 y 坐标对 S 进行排序,然后执行线性扫描.

生成的算法的时间为 O(N3/2 + N |S​​| log |S|)。

比较

当密度高时,算法#1 优于算法#2。算法#2 仅在粒子密度非常低的情况下具有优势,而算法#2 的优势密度会随着总板尺寸的增加而降低。

请注意,可以认为连续情况的密度为零,此时只有算法 #2 有效。

【讨论】:

我不确定你所说的重叠区域是什么意思...我们只搜索一个区域,有 202,500 种可能的检查。 @MillieSmith:性能和优化很重要,但还为时过早。毕竟,优化的第一条规则是“不要”。 @MillieSmith:我提出的方法不依赖于区域的大小。 O() 中的常数因子是非常小的 常数因子。我至少提出了一个完整正确的解决方案,如果你声称存在一个比无论如何都启发我更有效的解决方案。否则你只是在猜测我错了。 @MillieSmith:我不是在扮演魔鬼的拥护者。我在评论不完整的解决方案。如果它不能解决问题,你的代码有多快也没关系。 @j_random_hacker:分解的框卷积需要每个单元的摊销常数时间。对 d 没有依赖关系,对维数只有线性依赖关系。在一维情况下,给定输入 x(1)..x(n),输出 y(t) 的公式可以由 y(t) = sum i=0..d-1 x(i +t),这确实是 O(n*d)。但是,这等价于递归关系 y(t) = y(t-1) + x(t+d-1) - x(t-1),即 O(n)。这条捷径就是为什么 boxcar 函数的卷积对于离散输入如此有效的原因。【参考方案2】:

我不知道你使用什么蛮力方法,但最蛮力的方法是O(n^2 d^2),通过在O(n^2) 时间内迭代每个区域,然后在@987654325 中计算该区域中的粒子数@time 其中d 是您所在区域的大小。

这个问题和这个问题完全一样:Rat Attack,由于区域面积是固定的,所以密度和计数一样, 解决方案是O(n^2 + k*d^2),其中

    n是整个区域的大小(边长) k 是粒子数 d是每个区域的大小(边长)

通过这个算法:

    对于每个粒子,更新受此粒子影响的O(d^2) 区域的计数 遍历所有O(n^2)可能的区域,找到最大值

如this code所示,我把相关部分复制到这里供大家参考:

using namespace std;

int mat [1024 + 3] [1024 + 3]; // Here n is assumed to be 1024

int main ()

    int testCases; scanf ("%d", &testCases);

    while ( testCases-- ) 

        Set(mat, 0);

        int d; scanf ("%d", &d); // d is the size of the region
        int k; scanf ("%d", &k); // k is the number of particles

        int x, y, cost;

        for ( int i = 0; i < k; i++ ) 
            scanf ("%d %d %d", &x, &y, &cost); // Read each particle position

            // Update the count of the d^2 region affected by this particle
            for ( int j = max (0, x - d); j <= min (x + d, 1024); j++ ) 
                for ( int k = max (0, y - d); k <= min (y + d, 1024); k++ ) mat [j] [k] += cost;
            
        

        int resX, resY, maxi = -1;

        // Find the maximum count over all regions
        for ( int i = 0; i < 1025; i++ ) 
            for ( int j = 0; j < 1025; j++ ) 
                if ( maxi < mat [i] [j] ) 
                    maxi = mat [i] [j];
                    resX = i;
                    resY = j;
                
            
        

        printf ("%d %d %d\n", resX, resY, maxi);

    
    return 0;

我已将我的 cmets 放入代码中向您解释。

【讨论】:

O(n^2 + k) 的运行时间只需稍作修改即可,因为可以通过 O(n^2) 中的分解框卷积一次完成每个 d*d 大小区域的更新) 时间。 哇,你知道的似乎比我多得多。你能描述一下什么是分解框卷积吗?您还可以在答案中提供一些指向 K-D 树的链接吗? 卷积是获取每个单元格并将其内容添加到相邻单元格的过程。 (当您将值添加到相邻单元格时,您也可以按常数缩放值。)您可以同时对所有单元格执行此操作。内核是您影响的一组相邻单元格,以及每个单元格的系数。 box 函数是一个内核,它只是一个矩形,里面到处都有一个常数值。盒子函数可以分解成两个一维卷积(高斯核也有这个性质),这使得计算卷积非常快。 高斯模糊是卷积的另一个例子,与高斯核的卷积。【参考方案3】:

将区域划分为 1000x1000 并计算每个(重叠)2x2 中的粒子数。您可以简单地通过规范化 0..1、缩放 0..999 和转换为整数来对它们进行分区。计数可以很容易地存储为整数的二维数组(ushort、uint 或 ulong...mmmm tea)。这相当于在宽相位碰撞检测中使用的简单二维空间分割。

【讨论】:

以上是关于iOS 二维码有效区域rectOfInterest详解的主要内容,如果未能解决你的问题,请参考以下文章

有关iOS系统中调用相机设备实现二维码扫描功能的注意点(3/3)

在限制任意区域的二维空间中找到有效点

iOS 简单易用的二维码扫描及生成二维码三方控件LFQRCode,可灵活自定义UI

有效查找高密度区域的最佳方法

IOS自定义UIbutton,怎样让点击的有效区域集中在图标内?

将 swift 数组解析为有效的 json