Skip to content

Latest commit

 

History

History
232 lines (134 loc) · 13.6 KB

find-picture-image-without-marking.md

File metadata and controls

232 lines (134 loc) · 13.6 KB

在图像中找到未标记的图片

原文:www.kdnuggets.com/2022/09/find-picture-image-without-marking.html

在图像中找到未标记的图片

作者提供的图片 | 编辑者编辑

我们经常看到图像中的图片:例如,漫画将几张图片合并成一张。如果你有一个娱乐应用程序,用户在其中发布迷因,就像我们的 iFunny,你会经常遇到这种情况。神经网络已经能够找到动物、人或其他物体,但如果我们需要在图像中找到另一张图片怎么办?让我们更详细地了解我们的算法,以便你可以在 Google Colaboratory 中使用一个笔记本进行测试,甚至在你的项目中实现它。


我们的前三大课程推荐

1. Google Cybersecurity Certificate - 快速进入网络安全领域

2. Google Data Analytics Professional Certificate - 提升你的数据分析技能

3. Google IT Support Professional Certificate - 支持你的组织 IT 需求


所以,迷因。许多迷因由图片及其标题组成,前者作为语言笑话的背景:

在图像中找到未标记的图片

一张迷因可能包含多个带有文字的图片:

在图像中找到未标记的图片

一种流行的迷因类型是社交网络帖子截图,例如来自 Twitter。图片在其中占据了相对较小的区域。

在图像中找到未标记的图片

最初,寻找图片的任务出现在我们发送推送通知的时候。在 Android 全面页面推送消息出现之前,这些消息中的所有图片都非常小。文字难以阅读(那用户需要它干嘛?),通知图片中的所有对象几乎没有信息量。它们也不够吸引人。

我们决定押注于迷因中的图片。为了增强视觉效果,我们制作了一个算法,可以裁剪图片周围的所有框架,无论上面显示了什么。

让我们通过几个例子来看看算法的实际效果

以下是算法如何处理黑色背景和应用程序水印的方式:

在图像中查找图片而不标记 在图像中查找图片而不标记

在以下示例中,尽管它们之间有一条大线,但算法并未将这些图像分开。

在图像中查找图片而不标记 在图像中查找图片而不标记

该方法也适用于占据整个图像约 50%面积的大型竖直文本。

在图像中查找图片而不标记 在图像中查找图片而不标记

然而,算法未能识别应用水印(仍需改进)。

算法在处理以下示例时表现出色,即使目标图像在整个表情包中所占比例相对较小。算法还修剪了侧边的白色边框。

在图像中查找图片而不标记 在图像中查找图片而不标记

在下一个示例中,图像中同时有两个背景(白色和黑色),但算法成功处理了这一情况。然而,它保留了侧边的白色边框。

在图像中查找图片而不标记 在图像中查找图片而不标记

最后一个例子特别有趣,因为图像本身包含许多线条。但即便如此,也未能困扰我们的算法。上边界比我们期望的稍大,但对于如此困难的情况来说,这是一个很好的结果!

在图像中查找图片而不标记 在图像中查找图片而不标记

请回忆一下,这些结果是在没有标记的情况下取得的。

算法

跳过代码中无关紧要的部分。你可以在算法的完整实现中找到并运行这些部分。在这篇文章中,我们将重点关注主要思想。

以这张小狗的图片为例。

在图像中查找图片而不标记

它在顶部有文本和一条白色背景的小条纹,以及底部的应用水印。

为了仅获取小狗的图片,我们需要去除上下两个水平矩形。基本思路是使用霍夫变换识别图像中的所有直线,然后选择那些与白色或黑色单色区域相邻的直线,我们将这些区域视为背景。

第 1 步。将图像转换为灰度图像

图像处理的第一步是将其转换为单色格式。有很多方法可以实现这一点,但我更喜欢获取Lab颜色空间中的 L 通道(亮度)。这种方法在大多数情况下被证明是最好的。不过,你可以尝试其他方法,比如HSV颜色空间中的 V 通道(值)。

为了消除图像中产生不必要线条的强对比度差异,我们对生成的黑白图像应用高斯滤波器。

img_gray = cv2.cvtColor(img, cv2.COLOR_RGB2LAB)[:, :, 0]

img_blur = cv2.GaussianBlur(img_gray, (9, 9), 0)

这里我们使用了 OpenCV 库中的函数。除了图像外,cv2.GaussianBlur 函数接受高斯内核大小(宽度,高度)和 X 轴上的标准差(默认情况下,Y 轴为 0)。当使用 0 作为标准差时,它会根据内核的大小进行计算。我们使用的参数基于主观经验。

在图像中查找图片而不标记

这是我们目前得到的结果。

你可以从scikit-image库中找到并使用类似的方法,但要小心,因为它们在结果和输入参数上有所不同。skimage.color.rgb2lab函数的亮度通道结果在[0,100]范围内,高斯滤波器skimage.filters.gaussian对内核参数更为敏感,这会影响最终结果。

第 2 步。检测边缘

霍夫变换是我们算法的基础,只能处理由背景和边缘组成、值为 0 和 1 的二值图像。

我们使用边缘检测算法来创建二值图像。简而言之,该算法计算像素坐标的强度函数的梯度,并找到其局部最大值,这些区域被定义为所需的边缘。

在图像中查找图片而不标记

变换结果

我们使用来自scikit-image库的 Feature 模块中的 Canny 函数来执行变换。你可以在 OpenCV 库中找到类似的函数,但它的实现包括一个 5x5 的高斯滤波器平滑,这使得控制情况有点困难。

img_canny = feature.canny(img_blur) * 1

第 3 步:Hough 变换

现在我们需要在找到的所有线条中检测直线。Hough 的直线变换在这方面表现出色。算法在原点的给定距离 ? 处和 X 轴的某个角度 ? 绘制一条直线,如下所示。

在图像中找到未标记的图片

对于每条这样的线,它计算位于绘制直线上的二值图像的亮像素数量。这个过程会对选定范围内的所有 ? 和 ? 值重复进行。结果是 (?, ?) 坐标系中的强度图。因此,当绘制的直线与图像中的线条重合时,结果图中的极大值将会出现。在图像中找到未标记的图片

右图中是对左侧图中两条黑线的 Hough 变换的示意图。

在我们的算法中,我们使用这个来自 scikit-image 的实现,因为它更易于配置。此外,这个库提供了一个方便的方法来选择最类似于直线的局部极大值。OpenCV 的实现没有类似的功能。

tested_angles = np.linspace(-1.6, 1.6, 410)
h, theta, d = transform.hough_line(img_canny, theta=tested_angles)
_, angles, dists = transform.hough_line_peaks(h, theta, d)

变换输出了找到的线条的角度和距离。绘制这些线条比我们习惯的直线要复杂一些,因此这里有一个额外的代码片段。

plt.figure(figsize=(10, 10))
plt.imshow(img_gray, cmap="gray")

x = np.array((0, img_gray.shape[1]))
ys = []
for angle, dist in zip(angles, dists):    
    y0, y1 = (dist - x * np.cos(angle)) / np.sin(angle)
    y = int(np.mean([y0, y1]))
    ys += [y]
    plt.plot(x, [y, y])

ys = np.array(ys)
plt.axis("off");

计算得到的距离是斜边,而直线在 y 轴上的位置是直角边,因此我们将距离值除以所得角度的正弦值。

在图像中找到未标记的图片

运行此代码后,你将看到以下内容:

我们的算法找到了所有必要的线条!应用水印的边界也被检测到。

第 4 步:直线分类

最后,我们需要确定哪一条是底部线,哪一条是顶部线,同时去除不必要的线条,例如黑条上方的那一条。

在图像中找到未标记的图片

最终结果

为了解决这个问题,我们添加了对找到的线条上下区域的检查。由于我们处理的是表情包及其字幕,我们期望图像外的背景是白色的,而文字是黑色的。或者反过来:黑色背景和白色文字。

因此,我们的算法基于检查分离区域中白色和黑色像素的百分比。

props = {
    "white_limit": 230,
    "black_limit": 35,
    "percent_limit": 0.8
}

areas_h = []
for y in ys:
  percent_up = np.sum((img[:y] > props['white_limit']) + (img[:y] < props['black_limit'])) / (y * img.shape[1] * 3)
  percent_down = np.sum((img[y:] > props['white_limit']) + (img[y:] < props['black_limit'])) / ((img.shape[0] - y) * img.shape[1] * 3)
  areas_h += [-1 * (percent_up > props['percent_limit']) + (percent_down > props['percent_limit']) * 1]

在这种方法中,我们使用了 3 个变量:

  • white_limit 是定义白色的下限。所有值在(230; 256)范围内的像素将被视为白色。

  • black_limit 是定义黑色的上限。所有值在(0; 35)范围内的像素将被视为黑色。

  • percent_limit 是将区域视为带有文本的背景时白色/黑色像素的最小百分比。

此外,比较在每个颜色通道中独立进行,这是一种通用的条件,允许你正确处理稀有的例外情况。

接下来的几行代码选择了上限中的最低线和下限中的最高线。我们还添加了一个关于所选区域大小的条件。如果结果的垂直线裁剪掉了图像高度的超过 60%,我们将其视为错误,并返回原始图像的顶部或底部边框。这个参数是基于对表情包图像占用区域的假设来选择的。

cut_threshold = 0.6
y0 = np.max(ys[areas_h == -1])
if y0 > cut_threshold * img.shape[0]:
    y0 = 0
y1 = np.min(ys[areas_h == 1])
if y1 < (1 - cut_threshold) * img.shape[0]:
    y1 = img.shape[0]

最终算法,你可以在这里的 Functions 部分找到,还包括了寻找和调整垂直边框的功能,让你能够去除图像两侧的边框。

接下来是什么?

为了改善结果,你需要学习如何测量质量,而这需要标记。如果你想创建一个训练数据集,我们的算法将使你的手动标记更容易,从而使你能更快地收集到所需数量的样本。

为了使我们的方法适用于更多情况,你可以将算法中的背景色检查替换为单色检查。这样,无论背景色是什么,方法都可以使用。

亚罗斯拉夫·穆尔扎耶夫是 Funcorp 的数据科学家。

更多相关内容