如何在大图片中定位二维码提高解码性能?

How to locate QR code in large image to improve decoding performance?

背景

我需要在 Raspberry Pi 上的大图像 (2500x2000) 中检测和解码相对较小的 QR 码(110x110 像素)。 QR 码可以在框架中的任何位置,但方向应该是正常的,即顶部向上。我们使用的是高质量的工业相机和镜头,因此图像通常质量好且对焦清晰。

目前,当我使用大约 600x500 的 window 裁剪 QR 码周围的图像时,我能够使用 pyzbar 可靠地检测和解码图像。如果我尝试解码完整图像,则符号不是 detected/decoded。

我试过的

我编写了一个循环,在图像上滑动裁剪 window,并尝试分别解码每个裁剪的帧。我每次迭代将 window 移动 50%,以确保我不会错过 window.

边缘的任何符号

我也尝试过使用 OpenCV detection/decoding 但性能并不比 pyzbar

我的解决方案有问题

影响我当前项目的问题:

滑动 window 方法难以调优、低效且缓慢 b/c:

  1. 导致整个区域被分析近4次;将 window 移动 50% 的副作用,
  2. 最可靠的 window 尺寸往往很小并且需要多次迭代,
  3. 由于 closer/further 来自相机,符号大小可能会有所不同。

可能会影响我将使用此方法的其他项目的问题:

  1. 滑动 window 可能会多次捕获一个符号,因此很难确定该符号是否多次出现。

问题

如何找到 QR 码的大致位置以便相应地裁剪图像?

我对提高 detection/decoding 性能的任何解决方案感兴趣,但更喜欢 (a) 使用机器学习技术(我是 ML 新手但愿意学习),(b) 使用 OpenCV图像预处理或 (c) 改进我的基本裁剪算法。

示例图片

这是我用于测试的示例图像之一。故意采用较差的照明质量来近似最坏的情况,但是在裁剪时各个代码仍然可以正确检测和解码。

每个二维码的角上有3个square/rectangles。如果你能用openCV找到那些部分,就可以裁剪二维码区域。

我也找到了this

我想我找到了一种简单而可靠的方法来检测二维码的角点。但是,我的方法假设 QR 与其周围区域之间存在一些对比(越多越好)。另外,我们必须记住,pyzbaropencv.QRCodeDetector 都不是 100% 可靠的。

所以,这是我的方法:

  1. 调整图像大小。 经过一些实验,我得出的结论是 pyzbar 不完全是比例不变的。虽然我没有可以支持这一说法的参考资料,但作为经验法则,我仍然使用中小型图像进行条形码检测。您可以跳过这一步,因为它看起来完全是随意的。
image = cv2.imread("image.jpg")
scale = 0.3
width = int(image.shape[1] * scale)
height = int(image.shape[0] * scale)
image = cv2.resize(image, (width, height))
  1. 阈值。 我们可以利用条形码通常在白色表面上呈黑色这一事实。对比度越大越好。
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
_, thresh = cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)

3. Dilation + contours. 这一步有点棘手,如果我的英语在这里不是很清楚,我深表歉意。从上图我们可以看出,二维码内部的白色中间有黑色space。如果我们只是找到轮廓,那么 opencv 将假设这些 space 是独立的实体而不是整体的一部分。如果我们想转换二维码,让它看起来只是一个白色的方块,我们必须做一些morphological operations。即,我们必须扩大图像。

# The bigger the kernel, the more the white region increases.
# If the resizing step was ignored, then the kernel will have to be bigger
# than the one given here.
kernel = np.ones((3, 3), np.uint8)
thresh = cv2.dilate(thresh, kernel, iterations=1)
contours, _ = cv2.findContours(thresh, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)

4. 过滤和获取边界框。大多数找到的轮廓太小而不能包含条形码,所以我们必须过滤它们以使我们的搜索space更小。过滤掉弱候选后,我们可以获取强候选的边界框。

编辑: 在这种情况下,我们按区域过滤(小区域=弱候选),但我们也可以按检测的 extent 进行过滤。基本上,范围测量的是对象的矩形度,我们可以使用该信息,因为我们知道 QR 码是正方形。我选择的范围大于pi / 4,因为那是一个完美的圆的范围,意味着我们也过滤掉了圆形的物体。

bboxes = []
for cnt in contours:
  area = cv2.contourArea(cnt)
  xmin, ymin, width, height = cv2.boundingRect(cnt)
  extent = area / (width * height)
  
  # filter non-rectangular objects and small objects
  if (extent > np.pi / 4) and (area > 100):
    bboxes.append((xmin, ymin, xmin + width, ymin + height))

5. 检测条形码。我们已将搜索 space 减少到只搜索实际的 QR 码!现在我们终于可以使用pyzbar而不用担心条码检测时间太长了。

qrs = []
info = set()
for xmin, ymin, xmax, ymax in bboxes:
  roi = image[ymin:ymax, xmin:xmax]
  detections = pyzbar.decode(roi, symbols=[pyzbar.ZBarSymbol.QRCODE])
  for barcode in detections:
     info.add(barcode.data)
     # bounding box coordinates
     x, y, w, h = barcode.rect
     qrs.append((xmin + x, ymin + y, xmin + x + w, ymin + y + height))

不幸的是,pyzbar 只能解码最大 QR 码 (b'3280406-001') 的信息,即使两个条形码都在搜索中 space。关于了解检测到特定代码的次数,您可以使用 collections 标准模块中的 Counter 对象。如果您不介意拥有这些信息,那么您可以像我在这里那样使用一套。

希望这对您有所帮助:)。

这个解决方案非常低效,因为它更偏向于蛮力,但应该可以工作:图像大小为 2000x2000,检测阈值大约为 500x500,条形码大小为 110x110。

理论上它比滑动 window 方法更好,但可能不会好很多,因为它更多的是变体。

我们的想法是,如果我们不能在图像中可靠地找到条形码,但我们可以在图像的一个子部分中找到它,那么我们可以尝试将图像分成多个子部分,其中一个将包含图像.由于我们无法确定在将条码拆分为多个部分时条形码不会被打散,因此我们必须通过并确保它在可能的部分之一内。

首先,将图像分割成 500x500 的网格 (4x4) 并在每个网格上 运行 pyzbar。理论:条形码要么在这 16 个网格之一内,要么被这些网格之一分开。

如果找不到条形码,则在 x 轴上将网格偏移 250。 运行 再次尝试,然后尝试在 y 轴上将网格偏移 250,再次 运行,然后在 x 和 y 轴上尝试 250,然后再次尝试 运行。理论上,这应该确保条形码存在于 some 500x500 或更小的网格内。如果这不起作用,建议改用 250x250 网格(当然这将花费 4 倍的时间 运行),因为 250 仍然是条码大小的两倍多。

此处提供的其他建议可用于缩小搜索范围并丢弃绝对不能包含条形码的部分或重点关注可能包含条形码的部分。塞巴斯蒂安的回答可能会很好地确定要关注的网格方块。

另一种选择可能是考虑条形码最有可能在哪里,我希望条形码更靠近屏幕中心。因此,由内而外的螺旋式搜索模式可能会有所帮助。