使用CLIPSeg进行零样本图像分割
使用CLIPSeg进行零样本图像分割
本指南展示了如何使用CLIPSeg,这是一个零样本图像分割模型,使用🤗 transformers。CLIPSeg创建了粗略的分割掩码,可用于机器人感知、图像修复等许多任务。如果您需要更精确的分割掩码,我们将展示如何在Segments.ai上改进CLIPSeg的结果。
图像分割是计算机视觉领域中一个众所周知的任务。它不仅允许计算机知道图像中有什么(分类),物体在图像中的位置(检测),还允许计算机知道这些物体的轮廓。在机器人和自动驾驶等领域,了解物体的轮廓是至关重要的。例如,机器人必须知道物体的形状才能正确抓取它。分割还可以与图像修复相结合,使用户能够描述他们想要替换的图像的哪个部分。
大多数图像分割模型的一个限制是它们只能使用固定的类别列表。例如,您不能简单地使用训练在橙子上的分割模型来分割苹果。要教导分割模型一个额外的类别,您必须标记新类别的数据并训练一个新模型,这可能既昂贵又耗时。但是,如果有一个模型可以在没有任何进一步训练的情况下分割几乎任何类型的物体,那将会怎么样呢?CLIPSeg正是实现了这一点的零样本分割模型。
- 加速PyTorch Transformers与英特尔Sapphire Rapids – 第1部分
- AI for Game Development 在5天内创建一个农场游戏第二部分
- 使用Hugging Face Datasets和Transformers进行图像相似度比较
目前,CLIPSeg仍然有其局限性。例如,该模型使用352 x 352像素的图像,因此输出的分辨率相当低。这意味着当我们使用现代相机的图像时,我们不能期望像素完美的结果。如果我们想要更精确的分割,我们可以对最先进的分割模型进行微调,就像我们之前的博客文章中所展示的那样。在这种情况下,我们仍然可以使用CLIPSeg生成一些粗略的标签,然后在诸如Segments.ai之类的标注工具中对其进行改进。在描述如何做到这一点之前,让我们先来看看CLIPSeg的工作原理。
CLIP: CLIPSeg背后的神奇模型
CLIP代表对比性语言-图像预训练,是OpenAI于2021年开发的模型。您可以向CLIP提供图像或文本片段,CLIP会输出您输入的抽象表示。这个抽象表示,也称为嵌入,实际上只是一个向量(一系列数字)。您可以将这个向量视为高维空间中的一个点。CLIP被训练得使得相似的图片和文本的表示也是相似的。这意味着如果我们输入与图像匹配的文本描述,图像和文本的表示将是相似的(即高维点将靠近一起)。
起初,这可能看起来并不是很有用,但实际上非常强大。例如,让我们快速看看如何使用CLIP对图像进行分类,而无需对其进行任何训练。要对图像进行分类,我们将图像和我们要选择的不同类别输入到CLIP中(例如,我们将一个图像和“苹果”、“橙子”等词语输入)。然后,CLIP会给我们返回图像和每个类别的嵌入。现在,我们只需检查哪个类别的嵌入与图像的嵌入最接近,就完成了!感觉像魔法一样,不是吗?
使用CLIP进行图像分类的示例(来源)。
更重要的是,CLIP不仅适用于分类,还可用于图像搜索(您能看出这与分类相似吗?)、文本到图像模型(DALL-E 2由CLIP提供支持)、目标检测(OWL-ViT)以及对我们来说最重要的:图像分割。现在您明白为什么CLIP真正是机器学习的突破了。
CLIP之所以如此有效的原因是该模型是在具有文本标注的大型图像数据集上进行训练的。该数据集包含来自互联网的4亿个图像文本对。这些图像包含各种各样的对象和概念,而CLIP对每个对象都能创建出一个表示。
CLIPSeg: 使用CLIP进行图像分割
CLIPSeg 是一种使用 CLIP 表示创建图像分割掩码的模型。它由 Timo Lüddecke 和 Alexander Ecker 发布。他们通过在 CLIP 模型之上训练基于 Transformer 的解码器实现了零样本图像分割。解码器接收图像的 CLIP 表示和要分割的目标的 CLIP 表示作为输入。利用这两个输入,CLIPSeg 解码器创建一个二进制分割掩码。更准确地说,解码器不仅仅使用我们想要分割的图像的最终 CLIP 表示,还使用了 CLIP 的一些层的输出。
来源
解码器在 PhraseCut 数据集上进行了训练,该数据集包含超过 340,000 个短语和相应的图像分割掩码。作者还尝试了各种数据增强方法来扩大数据集的规模。这里的目标不仅是能够分割数据集中存在的类别,还可以分割未见类别。实验证明,解码器确实可以推广到未见类别。
CLIPSeg 的一个有趣特性是查询(我们想要分割的图像)和提示(我们想要在图像中分割的物体)都作为 CLIP 嵌入输入。提示的 CLIP 嵌入可以来自文本(类别名称)或另一张图像。这意味着您可以通过给 CLIPSeg 提供一个橙子的示例图像来分割照片中的橙子。
当您想要分割的物体很难描述时,这种被称为 “视觉提示” 的技术非常有帮助。例如,如果您想要在一张 T 恤的图片中分割一个徽标,描述徽标的形状并不容易,但是 CLIPSeg 可以让您简单地使用徽标的图像作为提示。
CLIPSeg 论文中提供了一些改进视觉提示效果的提示。他们发现裁剪查询图像(使其只包含要分割的对象)对提高效果很有帮助。模糊和变暗查询图像的背景也有一点帮助。在下一节中,我们将展示如何使用 Hugging Face Transformers 自己尝试视觉提示。
使用 Hugging Face Transformers 进行 CLIPSeg
使用 Hugging Face Transformers,您可以轻松下载和运行预训练的 CLIPSeg 模型来处理您的图像。让我们从安装 transformers 开始。
!pip install -q transformers
要下载模型,只需实例化它。
from transformers import CLIPSegProcessor, CLIPSegForImageSegmentation
processor = CLIPSegProcessor.from_pretrained("CIDAS/clipseg-rd64-refined")
model = CLIPSegForImageSegmentation.from_pretrained("CIDAS/clipseg-rd64-refined")
现在我们可以加载一张图像来尝试分割。我们选择了 Calum Lewis 拍摄的一张美味早餐图片。
from PIL import Image
import requests
url = "https://unsplash.com/photos/8Nc_oQsc2qQ/download?ixid=MnwxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNjcxMjAwNzI0&force=true&w=640"
image = Image.open(requests.get(url, stream=True).raw)
image
文本提示
首先,让我们定义一些要分割的文本类别。
prompts = ["餐具", "煎饼", "蓝莓", "橙汁"]
现在我们有了输入,我们可以对它们进行处理并输入到模型中。
import torch
inputs = processor(text=prompts, images=[image] * len(prompts), padding="max_length", return_tensors="pt")
# 预测
with torch.no_grad():
outputs = model(**inputs)
preds = outputs.logits.unsqueeze(1)
最后,让我们可视化输出。
import matplotlib.pyplot as plt
_, ax = plt.subplots(1, len(prompts) + 1, figsize=(3*(len(prompts) + 1), 4))
[a.axis('off') for a in ax.flatten()]
ax[0].imshow(image)
[ax[i+1].imshow(torch.sigmoid(preds[i][0])) for i in range(len(prompts))];
[ax[i+1].text(0, -15, prompt) for i, prompt in enumerate(prompts)];
视觉提示
如前所述,我们还可以使用图像作为输入提示(即代替类别名称)。如果很难描述您想要分割的事物,这将特别有用。对于这个示例,我们将使用由Daniel Hooper拍摄的咖啡杯的图片。
url = "https://unsplash.com/photos/Ki7sAc8gOGE/download?ixid=MnwxMjA3fDB8MXxzZWFyY2h8MTJ8fGNvZmZlJTIwdG8lMjBnb3xlbnwwfHx8fDE2NzExOTgzNDQ&force=true&w=640"
prompt = Image.open(requests.get(url, stream=True).raw)
prompt
现在我们可以处理输入图像和提示图像,并将它们输入模型。
encoded_image = processor(images=[image], return_tensors="pt")
encoded_prompt = processor(images=[prompt], return_tensors="pt")
# 预测
with torch.no_grad():
outputs = model(**encoded_image, conditional_pixel_values=encoded_prompt.pixel_values)
preds = outputs.logits.unsqueeze(1)
preds = torch.transpose(preds, 0, 1)
然后,我们可以像以前一样可视化结果。
_, ax = plt.subplots(1, 2, figsize=(6, 4))
[a.axis('off') for a in ax.flatten()]
ax[0].imshow(image)
ax[1].imshow(torch.sigmoid(preds[0]))
让我们最后尝试一次,使用论文中描述的视觉提示技巧,即裁剪图像并加深背景。
url = "https://i.imgur.com/mRSORqz.jpg"
alternative_prompt = Image.open(requests.get(url, stream=True).raw)
alternative_prompt
encoded_alternative_prompt = processor(images=[alternative_prompt], return_tensors="pt")
# 预测
with torch.no_grad():
outputs = model(**encoded_image, conditional_pixel_values=encoded_alternative_prompt.pixel_values)
preds = outputs.logits.unsqueeze(1)
preds = torch.transpose(preds, 0, 1)
_, ax = plt.subplots(1, 2, figsize=(6, 4))
[a.axis('off') for a in ax.flatten()]
ax[0].imshow(image)
ax[1].imshow(torch.sigmoid(preds[0]))
在这种情况下,结果几乎相同。这可能是因为咖啡杯在原始图像中已经很好地与背景分离。
使用CLIPSeg在Segments.ai上预标记图像
如您所见,CLIPSeg的结果有点模糊且分辨率很低。如果我们想要获得更好的结果,您可以微调最先进的分割模型,如我们之前的博客文章中所解释的那样。为了微调模型,我们需要标记数据。在本节中,我们将向您展示如何使用CLIPSeg创建一些粗糙的分割蒙版,然后在Segments.ai上进行改进,Segments.ai是一个具有智能标注工具用于图像分割的标注平台。
首先,在https://segments.ai/join上创建一个帐户并安装Segments Python SDK。然后,您可以使用API密钥来初始化Segments.ai Python客户端。这个密钥可以在帐户页面上找到。
!pip install -q segments-ai
from segments import SegmentsClient
from getpass import getpass
api_key = getpass('请输入您的API密钥:')
segments_client = SegmentsClient(api_key)
接下来,让我们使用Segments客户端从数据集中加载一张图像。我们将使用a2d2自动驾驶数据集。您也可以按照这些说明创建自己的数据集。
samples = segments_client.get_samples("admin-tobias/clipseg")
# 使用最后一张图像作为示例
sample = samples[1]
image = Image.open(requests.get(sample.attributes.image.url, stream=True).raw)
image
我们还需要从数据集属性中获取类别名称。
dataset = segments_client.get_dataset("admin-tobias/clipseg")
category_names = [category.name for category in dataset.task_attributes.categories]
现在,我们可以像以前一样在图像上使用CLIPSeg。这次,我们还将放大输出,使其与输入图像的大小匹配。
from torch import nn
inputs = processor(text=category_names, images=[image] * len(category_names), padding="max_length", return_tensors="pt")
# 预测
with torch.no_grad():
outputs = model(**inputs)
# 调整输出大小
preds = nn.functional.interpolate(
outputs.logits.unsqueeze(1),
size=(image.size[1], image.size[0]),
mode="bilinear"
)
然后我们可以再次可视化结果。
len_cats = len(category_names)
_, ax = plt.subplots(1, len_cats + 1, figsize=(3*(len_cats + 1), 4))
[a.axis('off') for a in ax.flatten()]
ax[0].imshow(image)
[ax[i+1].imshow(torch.sigmoid(preds[i][0])) for i in range(len_cats)];
[ax[i+1].text(0, -15, category_name) for i, category_name in enumerate(category_names)];
现在我们需要将预测结果合并为单个分割图像。我们将通过选择每个图像块中具有最大sigmoid值的类别来实现这一点。我们还将确保在某个阈值以下的所有值都不计入。
threshold = 0.1
flat_preds = torch.sigmoid(preds.squeeze()).reshape((preds.shape[0], -1))
# 使用阈值初始化一个虚拟的“未标记”掩码
flat_preds_with_treshold = torch.full((preds.shape[0] + 1, flat_preds.shape[-1]), threshold)
flat_preds_with_treshold[1:preds.shape[0]+1,:] = flat_preds
# 获取每个像素的顶部掩码索引
inds = torch.topk(flat_preds_with_treshold, 1, dim=0).indices.reshape((preds.shape[-2], preds.shape[-1]))
让我们快速可视化结果。
plt.imshow(inds)
最后,我们可以将预测上传到Segments.ai。为此,我们首先将位图转换为png文件,然后将此文件上传到Segments,最后将标签添加到样本中。
from segments.utils import bitmap2file
import numpy as np
inds_np = inds.numpy().astype(np.uint32)
unique_inds = np.unique(inds_np).tolist()
f = bitmap2file(inds_np, is_segmentation_bitmap=True)
asset = segments_client.upload_asset(f, "clipseg_prediction.png")
attributes = {
'format_version': '0.1',
'annotations': [{"id": i, "category_id": i} for i in unique_inds if i != 0],
'segmentation_bitmap': { 'url': asset.url },
}
segments_client.add_label(sample.uuid, 'ground-truth', attributes)
如果您查看在Segments.ai上上传的预测结果,您会发现它并不完美。但是,您可以手动纠正最大的错误,然后可以使用纠正后的数据集来训练比CLIPSeg更好的模型。
结论
CLIPSeg是一个零样本分割模型,可同时使用文本和图像提示。该模型在CLIP的基础上添加了解码器,可以对几乎任何物体进行分割。然而,目前输出的分割掩码分辨率仍然非常低,因此如果准确性很重要,您可能仍然希望对不同的分割模型进行微调。
请注意,目前正在进行更多关于零样本分割的研究,因此您可以期待在不久的将来添加更多的模型。一个例子是GroupViT,它已经在🤗 Transformers中可用。要了解分割研究的最新动态,请关注我们的Twitter:@TobiasCornille,@NielsRogge和@huggingface。
如果您有兴趣了解如何微调最先进的分割模型,请查看我们之前的博客文章:https://huggingface.co/blog/fine-tune-segformer。