My machine is a living life. I’ll prove it.

引言

hCaptcha 在今天又喜提一次更新,出现了新的标签,要求选中 在天上向左飞飞机

不过幸运的是,所有的样例图片里面,都包含一个飞机,所以这个标签相当于少了一个限制,只有 在天上向左飞 这两个限制了。

正文方法

首先,还是先来观察图片,依然是从收集的 数据集 来看。除了上面说到的,每个样例图片中比如会有一个飞机以外,如果飞机是在天空中,那么他的背景一定很“干净”,如果不在天空,基本可以判断在地面,在地面的图片中,也会有多个区域组成,例如草坪、机场跑道、背景森林、背景天空之类的。

那么区分第一个问题的关键就是背景的复杂度。咋做呢?

最开始的想法就是继承之前的思路,色块过滤,但是划分好色块以后,很难去区分是否是飞机色块还是背景色块,遂被 pass 掉。

然后,看了一下大部分去除天空背景的方法基本都是基于 HSV 颜色空间的阈值过滤,最后再加上腐蚀和膨胀的计算图形学操作来降噪,我自己测了几下,但是天空的颜色范围略大,有蓝、有白、有黄,而最关键的,其实和飞机颜色非常像,因为飞机也基本都是淡蓝或者白的浅色系。也被 pass。

又想到,飞机底部的阴影会有黑色,那么根据黑色来画一个超像素的智能选区也是可以的,但是我不会怎么实现(x,遂被 pass。

最后采取的方案是使用轮廓线方法 Canny,来找到所有轮廓线,然后设置阈值,根据轮廓线的数量来判断是否是在天空中,如果在天空,其实轮廓线会很简单,而不在天空则会添加一大片杂乱无章的线。当然这个阈值是我随便试了几张图片得出来的,毕竟两者的差距过大了。经过这样处理的判断准确率接近 100%。

好了,解决了一个问题,那么剩下的问题就是,怎么判断飞机朝向是在左?

这可真是难到我了,不用 Deep Learning 来做这个确实有难度,但是还是有取巧的办法的。首先,大部分飞机都是运输机或者战斗机、客机一类的,很少有那种前面是螺旋桨的飞机,也没见过直升机。甚至还有下面这样的 WTF Airplane(这 tm 什么玩意儿?)

既然这样,这类的飞机也是有特点的,就是“头轻脚重”。除了本身尾翼会比较重、头部会呈尖状较轻以外,还有就是机翼会向后,那么整个图片的“重心”应该是在偏向机尾的,我可以根据 4 个点,极左、极右、中点、重心来判断飞机朝向。

听起来很科学对不对,然而实际效果却不是很好,最重要的就是飞机存在透视关系导致从前面看去,可能重心就在前面了。比如机头朝你,大范围的线都画到了机头的上面,尾部的线很少,这样整个图片的重心就在前面了。后面想能不能把图片填充上,然后得到的轮廓线大多都不是闭合的,所以很难填充出整个飞机(如果能,那岂不是图像分割就简单了)。

后面我突发奇想,另辟蹊径,还是从上面画出来的轮廓线看出来的,机头因为没有复杂的东西,画出来的轮廓线会比较“简单”,而机尾由于存在“尾翼”之类的元件,画出来会比较“复杂”。那么如何衡量这个“简单”和“复杂”呢?求和就完了…没错,我最后就是把左边在 x_minx_min + left_threshold 的非零像素数出来(就是轮廓线的像素点)和 x_max - left_thresholdx_max 的像素点数出来进行比较,谁大那么谁就是机尾,右边大的话,那就是机头在左。没想到最后结果还不错,基本 1-2 轮就能通过验证。

附一下测试版本的完整 Code

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
from itertools import count
import cv2
import numpy as np
import matplotlib.pyplot as plt
from scipy import ndimage as ndi
from skimage.util import random_noise
from skimage import feature


class SkyLeftAirplaneChallenger:
    """A fast solution for identifying vertical rivers"""
    def __init__(self):
        self.flag = "skyleftairplane_model"
        self.sky_threshold = 1800
        self.left_threshold = 30
        self.debug = True

    @staticmethod
    def _remove_border(img):
        img[:, 1] = 0
        img[:, -2] = 0
        img[1, :] = 0
        img[-2, :] = 0
        return img

    def solution(self, img_stream, **kwargs) -> bool:  # noqa
        """Implementation process of solution"""
        img_arr = np.frombuffer(img_stream, np.uint8)
        img = cv2.imdecode(img_arr, flags=1)
        img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

        # cv2.imshow("img", img)
        # cv2.waitKey(0)

        edges1 = feature.canny(img)
        edges1 = self._remove_border(edges1)
        edges2 = feature.canny(img, sigma=3)
        edges2 = self._remove_border(edges2)

        # display results
        # fig, ax = plt.subplots(nrows=1, ncols=3, figsize=(8, 3))

        # ax[0].imshow(img, cmap='gray')
        # ax[0].set_title('noisy image', fontsize=20)

        # ax[1].imshow(edges1, cmap='gray')
        # ax[1].set_title(r'Canny filter, $\sigma=1$', fontsize=20)

        # ax[2].imshow(edges2, cmap='gray')
        # ax[2].set_title(r'Canny filter, $\sigma=3$', fontsize=20)

        # for a in ax:
        #     a.axis('off')

        # fig.tight_layout()
        # plt.show()

        # fill_plane = ndi.binary_fill_holes(edges1)

        # fig, ax = plt.subplots(figsize=(4, 3))
        # ax.imshow(fill_plane, cmap=plt.cm.gray)
        # ax.set_title('filling the holes')
        # ax.axis('off')
        # plt.show()
        # print(np.count_nonzero(edges1))
        # print(np.count_nonzero(edges2))

        if np.count_nonzero(edges1) > self.sky_threshold:
            if self.debug:
                print('[not in sky] ', end='')
            return False

        # get avg coordinate of edges where edges are not zero
        # avg_point = np.average(np.nonzero(edges1), axis=1)
        # print(avg_point)

        min_x = np.min(np.nonzero(edges1), axis=1)[1]
        max_x = np.max(np.nonzero(edges1), axis=1)[1]

        left_nonzero = np.count_nonzero(edges1[:, min_x:min(max_x, min_x + self.left_threshold)])
        right_nonzero = np.count_nonzero(edges1[:, max(min_x, max_x - self.left_threshold):max_x])

        # print(left_nonzero, right_nonzero)

        if left_nonzero > right_nonzero:
            if self.debug:
                print('[not turn left] ', end='')
            return False

        # mid_x = (min_x + max_x) / 2

        # print(min_x, max_x, mid_x, avg_point[0] < mid_x)

        # if avg_point[0] >= mid_x:
        #     return False

        # plt.show()
        return True


if __name__ == '__main__':
    import os
    import sys
    sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

    result_path = 'result.txt'
    if os.path.exists(result_path):
        os.remove(result_path)

    # result_file = open(result_path, 'w')
    result_file = sys.stdout

    base_path = os.path.join('..', 'database', 'airplane_in_the_sky_flying_left')
    image_list = os.listdir(base_path)
    # image_list.sort()
    for image_name in image_list:
        image_path = os.path.join(base_path, image_name)
        with open(image_path, "rb") as file:
            data = file.read()
        solution = SkyLeftAirplaneChallenger().solution(data)
        result_file.write(f'{image_name}: {solution}\n')
        result_file.flush()

    result_file.close()

结语

说实话每天做一道 hCaptcha 出的高质量图片处理题还是感觉很爽的 hhhh。

不过通过网友分享,已经看到了更多生成模型来做的问题了,毕竟人家带薪搞这东西,后面有些题已经不是只通过图像处理就能解决的了,比如在某油猴插件反馈中出现的黑白条纹的猫之类的。

见招拆招的取巧方法罢了。