代码示例 / 计算机视觉 / 随时分割模型与🤗Transformers

随时分割模型与🤗Transformers

作者: Merve Noyan & Sayak Paul
创建日期: 2023/07/11
最后修改: 2023/07/11
描述: 使用 Keras 和 🤗 Transformers 对随时分割模型进行微调。

在 Colab 中查看 GitHub 源代码


介绍

大型语言模型(LLMs)通过“提示”使最终用户可以轻松将其应用于各种应用程序。例如,如果我们想让 LLM 预测以下句子的情感——“那部电影太棒了,我彻底享受了它”——我们会用类似这样的提示来提示 LLM:

“以下句子的情感是什么?”:“那部电影太棒了,我彻底享受了它”?

作为回应,LLM 会返回情感标记。

但是在视觉识别任务中,我们如何来设计“视觉”提示来提示基础视觉模型呢?例如,我们可以有一张输入图像并在该图像上用边界框提示模型,要求其执行分割。边界框将在这里作为我们的视觉提示。

随时分割模型(简称 SAM)中,Meta 的研究人员将语言提示的范围扩展到了视觉提示。SAM 能够通过提示输入进行零-shot 分割,灵感来自大型语言模型。这里的提示可以是一组前景/背景点、自由文本、框或掩码。有许多下游分割任务,包括语义分割和边缘检测。SAM 的目标是通过提示来实现所有这些下游分割任务。

在这个示例中,我们将学习如何使用来自🤗 Transformers 的 SAM 模型进行推理和微调。


安装

!!pip install -q git+https://github.com/huggingface/transformers
[]

让我们导入这个示例所需的所有内容。

from tensorflow import keras
from transformers import TFSamModel, SamProcessor
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
from tensorflow.python.ops.numpy_ops import np_config
from PIL import Image
import requests
import glob
import os
/Users/mervenoyan/miniforge3/envs/py310/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. 请更新 jupyter 和 ipywidgets。请参阅 https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm

SAM 简述

SAM 具有以下组件:

| |:–:| | 图片来自官方 SAM 博客文章 | |

图像编码器负责计算图像嵌入。当与 SAM 交互时,我们只需一次计算图像嵌入(因为图像编码器开销较大),然后使用上述不同的提示(点、边界框、掩码)重复使用它。

点和框(所谓的稀疏提示)通过轻量级提示编码器,而掩码(密集提示)则通过卷积层。我们将从图像编码器提取的图像嵌入与提示嵌入结合,二者一起输入轻量级掩码解码器。解码器负责预测掩码。

图像取自 SAM 论文

SAM 被预训练为预测对任何有效提示的 有效 掩码。这个要求使得 SAM 即使在提示模糊不清时也能输出有效的掩码——这使得 SAM 具有模糊理解能力。此外,SAM 为单个提示预测多个掩码。

我们强烈建议您查看 SAM 论文博客文章 以了解更多关于 SAM 和用于预训练的数据集的附加细节。


使用 SAM 进行推理

SAM 有三个检查点:

我们在TFSamModel中加载 sam-vit-base。我们还需要 SamProcessor 来配合相应的检查点。

model = TFSamModel.from_pretrained("facebook/sam-vit-base")
processor = SamProcessor.from_pretrained("facebook/sam-vit-base")
所有模型检查点层在初始化 TFSamModel 时都被使用。
TFSamModel 的所有层都是从 facebook/sam-vit-base 模型检查点初始化的。
如果你的任务类似于检查点模型训练的任务,你可以在不进行进一步训练的情况下直接使用 TFSamModel 进行预测。

接下来,我们编写一些可视化的实用函数。大多数函数来自 这个笔记本

np_config.enable_numpy_behavior()


def show_mask(mask, ax, random_color=False):
    if random_color:
        color = np.concatenate([np.random.random(3), np.array([0.6])], axis=0)
    else:
        color = np.array([30 / 255, 144 / 255, 255 / 255, 0.6])
    h, w = mask.shape[-2:]
    mask_image = mask.reshape(h, w, 1) * color.reshape(1, 1, -1)
    ax.imshow(mask_image)


def show_box(box, ax):
    x0, y0 = box[0], box[1]
    w, h = box[2] - box[0], box[3] - box[1]
    ax.add_patch(
        plt.Rectangle((x0, y0), w, h, edgecolor="green", facecolor=(0, 0, 0, 0), lw=2)
    )


def show_boxes_on_image(raw_image, boxes):
    plt.figure(figsize=(10, 10))
    plt.imshow(raw_image)
    for box in boxes:
        show_box(box, plt.gca())
    plt.axis("on")
    plt.show()


def show_points_on_image(raw_image, input_points, input_labels=None):
    plt.figure(figsize=(10, 10))
    plt.imshow(raw_image)
    input_points = np.array(input_points)
    if input_labels is None:
        labels = np.ones_like(input_points[:, 0])
    else:
        labels = np.array(input_labels)
    show_points(input_points, labels, plt.gca())
    plt.axis("on")
    plt.show()


def show_points_and_boxes_on_image(raw_image, boxes, input_points, input_labels=None):
    plt.figure(figsize=(10, 10))
    plt.imshow(raw_image)
    input_points = np.array(input_points)
    if input_labels is None:
        labels = np.ones_like(input_points[:, 0])
    else:
        labels = np.array(input_labels)
    show_points(input_points, labels, plt.gca())
    for box in boxes:
        show_box(box, plt.gca())
    plt.axis("on")
    plt.show()


def show_points_and_boxes_on_image(raw_image, boxes, input_points, input_labels=None):
    plt.figure(figsize=(10, 10))
    plt.imshow(raw_image)
    input_points = np.array(input_points)
    if input_labels is None:
        labels = np.ones_like(input_points[:, 0])
    else:
        labels = np.array(input_labels)
    show_points(input_points, labels, plt.gca())
    for box in boxes:
        show_box(box, plt.gca())
    plt.axis("on")
    plt.show()


def show_points(coords, labels, ax, marker_size=375):
    pos_points = coords[labels == 1]
    neg_points = coords[labels == 0]
    ax.scatter(
        pos_points[:, 0],
        pos_points[:, 1],
        color="green",
        marker="*",
        s=marker_size,
        edgecolor="white",
        linewidth=1.25,
    )
    ax.scatter(
        neg_points[:, 0],
        neg_points[:, 1],
        color="red",
        marker="*",
        s=marker_size,
        edgecolor="white",
        linewidth=1.25,
    )


def show_masks_on_image(raw_image, masks, scores):
    if len(masks[0].shape) == 4:
        final_masks = tf.squeeze(masks[0])
    if scores.shape[0] == 1:
        final_scores = tf.squeeze(scores)

    nb_predictions = scores.shape[-1]
    fig, axes = plt.subplots(1, nb_predictions, figsize=(15, 15))

    for i, (mask, score) in enumerate(zip(final_masks, final_scores)):
        mask = tf.stop_gradient(mask)
        axes[i].imshow(np.array(raw_image))
        show_mask(mask, axes[i])
        axes[i].title.set_text(f"Mask {i+1}, Score: {score.numpy().item():.3f}")
        axes[i].axis("off")
    plt.show()

我们将使用点提示对汽车图像进行分割。确保在调用处理器时将 return_tensors 设置为 tf

让我们加载一张汽车的图像并进行分割。

img_url = "https://huggingface.co/ybelkada/segment-anything/resolve/main/assets/car.png"
raw_image = Image.open(requests.get(img_url, stream=True).raw).convert("RGB")
plt.imshow(raw_image)
plt.show()

png

现在让我们定义一组用于提示的点。

input_points = [[[450, 600]]]

# 可视化一个点。
show_points_on_image(raw_image, input_points[0])

png

并进行分割:

# 预处理输入图像。
inputs = processor(raw_image, input_points=input_points, return_tensors="tf")

# 使用提示进行分割预测。
outputs = model(**inputs)

outputs 有两个我们感兴趣的属性:

  • outputs.pred_masks:表示预测的掩码。
  • outputs.iou_scores:表示与掩码相关的 IoU 分数。

让我们对掩码进行后处理,并用它们的 IoU 分数进行可视化:

masks = processor.image_processor.post_process_masks(
    outputs.pred_masks,
    inputs["original_sizes"],
    inputs["reshaped_input_sizes"],
    return_tensors="tf",
)

show_masks_on_image(raw_image, masks, outputs.iou_scores)

png

这就完成了!

可以注意到,所有的掩膜都是我们提供的点提示的_有效_掩膜。

SAM 灵活到足以支持不同的视觉提示,我们鼓励你查看 这个笔记本以了解更多信息!


微调

我们将使用这个数据集,它由乳腺癌扫描图像组成。在医学成像领域,能够分割出含有恶性肿瘤的细胞是一项重要任务。

数据准备

让我们先获取数据集。

remote_path = "https://hf.co/datasets/sayakpaul/sample-datasets/resolve/main/breast-cancer-dataset.tar.gz"
dataset_path = keras.utils.get_file(
    "breast-cancer-dataset.tar.gz", remote_path, untar=True
)

现在让我们从数据集中可视化一个样本。

(show_mask()工具取自 这个笔记本)

def show_mask(mask, ax, random_color=False):
    if random_color:
        color = np.concatenate([np.random.random(3), np.array([0.6])], axis=0)
    else:
        color = np.array([30 / 255, 144 / 255, 255 / 255, 0.6])
    h, w = mask.shape[-2:]
    mask_image = mask.reshape(h, w, 1) * color.reshape(1, 1, -1)
    ax.imshow(mask_image)


# 加载所有的图像和标签路径。
image_paths = sorted(glob.glob(os.path.join(dataset_path, "images/*.png")))
label_paths = sorted(glob.glob(os.path.join(dataset_path, "labels/*.png")))

# 加载图像和标签。
idx = 15
image = Image.open(image_paths[idx])
label = Image.open(label_paths[idx])
image = np.array(image)
ground_truth_seg = np.array(label)

# 显示。
fig, axes = plt.subplots()
axes.imshow(image)
show_mask(ground_truth_seg, axes)
axes.title.set_text(f"真实掩膜")
axes.axis("off")
plt.show()

tf.shape(ground_truth_seg)

png

<tf.Tensor: shape=(2,), dtype=int32, numpy=array([256, 256], dtype=int32)>

准备 tf.data.Dataset

我们现在编写一个生成器类,用于使用上面使用的processor准备图像和分割掩膜。我们将利用这个生成器类通过使用tf.data.Dataset.from_generator()为我们的训练集创建一个tf.data.Dataset对象。此类的实用工具已从 这个笔记本进行了适应。

生成器负责生成预处理的图像和分割掩膜,以及 SAM 模型所需的一些其他元数据。

class Generator:
    """处理图像和掩膜以进行SAM微调的生成器类。"""

    def __init__(self, dataset_path, processor):
        self.dataset_path = dataset_path
        self.image_paths = sorted(
            glob.glob(os.path.join(self.dataset_path, "images/*.png"))
        )
        self.label_paths = sorted(
            glob.glob(os.path.join(self.dataset_path, "labels/*.png"))
        )
        self.processor = processor

    def __call__(self):
        for image_path, label_path in zip(self.image_paths, self.label_paths):
            image = np.array(Image.open(image_path))
            ground_truth_mask = np.array(Image.open(label_path))

            # 获取边界框提示
            prompt = self.get_bounding_box(ground_truth_mask)

            # 准备图像和提示以供模型使用
            inputs = self.processor(image, input_boxes=[[prompt]], return_tensors="np")

            # 去掉处理器默认添加的批量维度
            inputs = {k: v.squeeze(0) for k, v in inputs.items()}

            # 添加真实的分割
            inputs["ground_truth_mask"] = ground_truth_mask

            yield inputs

    def get_bounding_box(self, ground_truth_map):
        # 从掩膜获取边界框
        y_indices, x_indices = np.where(ground_truth_map > 0)
        x_min, x_max = np.min(x_indices), np.max(x_indices)
        y_min, y_max = np.min(y_indices), np.max(y_indices)

        # 向边界框坐标添加扰动
        H, W = ground_truth_map.shape
        x_min = max(0, x_min - np.random.randint(0, 20))
        x_max = min(W, x_max + np.random.randint(0, 20))
        y_min = max(0, y_min - np.random.randint(0, 20))
        y_max = min(H, y_max + np.random.randint(0, 20))
        bbox = [x_min, y_min, x_max, y_max]

        return bbox

get_bounding_box() 负责将真实分割图转化为边界框。这些边界框在微调过程中与原始图像一起作为提示输入给 SAM,随后 SAM 被训练以预测有效的掩码。

首先创建生成器,然后使用它来创建 tf.data.Dataset 的优点在于灵活性。有时,我们可能需要使用其他库中的工具(例如 albumentations),而这些工具可能不在原生的 TensorFlow 实现中。通过这种工作流程,我们可以轻松地适应这样的使用场景。

不过,非 TF 的对应工具可能会引入性能瓶颈。然而,对于我们的例子,它应该工作正常。

现在,我们从训练集中准备 tf.data.Dataset

# 定义生成器类的输出签名。
output_signature = {
    "pixel_values": tf.TensorSpec(shape=(3, None, None), dtype=tf.float32),
    "original_sizes": tf.TensorSpec(shape=(None,), dtype=tf.int64),
    "reshaped_input_sizes": tf.TensorSpec(shape=(None,), dtype=tf.int64),
    "input_boxes": tf.TensorSpec(shape=(None, None), dtype=tf.float64),
    "ground_truth_mask": tf.TensorSpec(shape=(None, None), dtype=tf.int32),
}

# 准备数据集对象。
train_dataset_gen = Generator(dataset_path, processor)
train_ds = tf.data.Dataset.from_generator(
    train_dataset_gen, output_signature=output_signature
)

接下来,我们配置数据集以提高性能。

auto = tf.data.AUTOTUNE
batch_size = 2
shuffle_buffer = 4

train_ds = (
    train_ds.cache()
    .shuffle(shuffle_buffer)
    .batch(batch_size)
    .prefetch(buffer_size=auto)
)

获取一批数据并检查其中元素的形状。

sample = next(iter(train_ds))
for k in sample:
    print(k, sample[k].shape, sample[k].dtype, isinstance(sample[k], tf.Tensor))
pixel_values (2, 3, 1024, 1024) <dtype: 'float32'> True
original_sizes (2, 2) <dtype: 'int64'> True
reshaped_input_sizes (2, 2) <dtype: 'int64'> True
input_boxes (2, 1, 4) <dtype: 'float64'> True
ground_truth_mask (2, 256, 256) <dtype: 'int32'> True

训练

我们现在来编写 DICE 损失。这个实现基于 MONAI DICE loss

def dice_loss(y_true, y_pred, smooth=1e-5):
    y_pred = tf.sigmoid(y_pred)
    reduce_axis = list(range(2, len(y_pred.shape)))
    if batch_size > 1:
        # 减少空间维度和批次
        reduce_axis = [0] + reduce_axis
    intersection = tf.reduce_sum(y_true * y_pred, axis=reduce_axis)
    y_true_sq = tf.math.pow(y_true, 2)
    y_pred_sq = tf.math.pow(y_pred, 2)

    ground_o = tf.reduce_sum(y_true_sq, axis=reduce_axis)
    pred_o = tf.reduce_sum(y_pred_sq, axis=reduce_axis)
    denominator = ground_o + pred_o
    # 计算 DICE 系数
    loss = 1.0 - (2.0 * intersection + 1e-5) / (denominator + 1e-5)
    loss = tf.reduce_mean(loss)

    return loss

微调 SAM

现在我们将微调 SAM 的解码器部分。我们将冻结视觉编码器和提示编码器层。

# 初始化 SAM 模型和优化器
sam = TFSamModel.from_pretrained("facebook/sam-vit-base")
optimizer = keras.optimizers.Adam(1e-5)

for layer in sam.layers:
    if layer.name in ["vision_encoder", "prompt_encoder"]:
        layer.trainable = False


@tf.function
def train_step(inputs):
    with tf.GradientTape() as tape:
        # 将输入传递给 SAM 模型
        outputs = sam(
            pixel_values=inputs["pixel_values"],
            input_boxes=inputs["input_boxes"],
            multimask_output=False,
            training=True,
        )

        predicted_masks = tf.squeeze(outputs.pred_masks, 1)
        ground_truth_masks = tf.cast(inputs["ground_truth_mask"], tf.float32)

        # 计算预测掩码和真实掩码之间的损失
        loss = dice_loss(tf.expand_dims(ground_truth_masks, 1), predicted_masks)
        # 更新可训练变量
        trainable_vars = sam.trainable_variables
        grads = tape.gradient(loss, trainable_vars)
        optimizer.apply_gradients(zip(grads, trainable_vars))

        return loss
所有模型检查点层在初始化 TFSamModel 时均已使用。
TFSamModel 的所有层均已从 facebook/sam-vit-base 的模型检查点进行初始化。如果您的任务与检查点训练时的任务相似,您可以在不进一步训练的情况下直接使用 TFSamModel 进行预测。
警告:absl:目前,v2.11+ 优化器 [`tf.keras.optimizers.Adam`](/api/optimizers/adam#adam-class) 在 M1/M2 Mac 上运行缓慢,请改用位于 [`tf.keras.optimizers.legacy.Adam`](https://www.tensorflow.org/api_docs/python/tf/keras/optimizers/legacy/Adam) 的传统 Keras 优化器。

我们现在可以运行三轮训练。我们可能会收到关于掩码解码器的IoU预测头不存在梯度的警告,我们可以安全地忽略它。

# 运行训练
for epoch in range(3):
    for inputs in train_ds:
        loss = train_step(inputs)
    print(f"Epoch {epoch + 1}: Loss = {loss}")
WARNING:tensorflow:在最小化损失时,变量['tf_sam_model_1/mask_decoder/iou_prediction_head/proj_in/kernel:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/proj_in/bias:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/proj_out/kernel:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/proj_out/bias:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/layers_._0/kernel:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/layers_._0/bias:0']的梯度不存在。如果你正在使用`model.compile()`,你是否忘记提供`loss`参数? WARNING:tensorflow:在最小化损失时,变量['tf_sam_model_1/mask_decoder/iou_prediction_head/proj_in/kernel:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/proj_in/bias:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/proj_out/kernel:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/proj_out/bias:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/layers_._0/kernel:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/layers_._0/bias:0']的梯度不存在。如果你正在使用`model.compile()`,你是否忘记提供`loss`参数? WARNING:tensorflow:在最小化损失时,变量['tf_sam_model_1/mask_decoder/iou_prediction_head/proj_in/kernel:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/proj_in/bias:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/proj_out/kernel:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/proj_out/bias:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/layers_._0/kernel:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/layers_._0/bias:0']的梯度不存在。如果你正在使用`model.compile()`,你是否忘记提供`loss`参数? WARNING:tensorflow:在最小化损失时,变量['tf_sam_model_1/mask_decoder/iou_prediction_head/proj_in/kernel:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/proj_in/bias:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/proj_out/kernel:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/proj_out/bias:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/layers_._0/kernel:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/layers_._0/bias:0']的梯度不存在。如果你正在使用`model.compile()`,你是否忘记提供`loss`参数? WARNING:tensorflow:在最小化损失时,变量['tf_sam_model_1/mask_decoder/iou_prediction_head/proj_in/kernel:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/proj_in/bias:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/proj_out/kernel:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/proj_out/bias:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/layers_._0/kernel:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/layers_._0/bias:0']的梯度不存在。如果你正在使用`model.compile()`,你是否忘记提供`loss`参数? WARNING:tensorflow:在最小化损失时,变量['tf_sam_model_1/mask_decoder/iou_prediction_head/proj_in/kernel:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/proj_in/bias:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/proj_out/kernel:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/proj_out/bias:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/layers_._0/kernel:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/layers_._0/bias:0']的梯度不存在。如果你正在使用`model.compile()`,你是否忘记提供`loss`参数? WARNING:tensorflow:在最小化损失时,变量['tf_sam_model_1/mask_decoder/iou_prediction_head/proj_in/kernel:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/proj_in/bias:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/proj_out/kernel:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/proj_out/bias:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/layers_._0/kernel:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/layers_._0/bias:0']的梯度不存在。如果你正在使用`model.compile()`,你是否忘记提供`loss`参数? WARNING:tensorflow:在最小化损失时,变量['tf_sam_model_1/mask_decoder/iou_prediction_head/proj_in/kernel:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/proj_in/bias:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/proj_out/kernel:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/proj_out/bias:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/layers_._0/kernel:0', 'tf_sam_model_1/mask_decoder/iou_prediction_head/layers_._0/bias:0']的梯度不存在。如果你正在使用`model.compile()`,你是否忘记提供`loss`参数? Epoch 1: Loss = 0.08322787284851074 Epoch 2: Loss = 0.05677264928817749 Epoch 3: Loss = 0.07764029502868652

序列化模型

我们序列化了模型并在下面进行了推送。 push_to_hub 方法序列化模型,生成模型卡并将其推送到 Hugging Face Hub,以便其他人可以使用 from_pretrained 方法加载模型进行推断或进一步微调。我们还需要在库中推送相同的预处理器。可以在 这里 找到模型和预处理器。

# sam.push_to_hub("merve/sam-finetuned")
# processor.push_to_hub("merve/sam-finetuned")

我们现在可以使用模型进行推断。

# 加载另一张用于推断的图像。
idx = 20
raw_image_inference = Image.open(image_paths[idx])

# 处理图像并进行推断
preprocessed_img = processor(raw_image_inference)
outputs = sam(preprocessed_img)

最后,我们可以可视化结果。

infer_masks = outputs["pred_masks"]
iou_scores = outputs["iou_scores"]
show_masks_on_image(raw_image_inference, masks=infer_masks, scores=iou_scores)
WARNING:matplotlib.image:将输入数据裁剪到 imshow 有效范围,RGB 数据的范围是 ([0..1] 的浮点数或 [0..255] 的整数)。
WARNING:matplotlib.image:将输入数据裁剪到 imshow 有效范围,RGB 数据的范围是 ([0..1] 的浮点数或 [0..255] 的整数)。
WARNING:matplotlib.image:将输入数据裁剪到 imshow 有效范围,RGB 数据的范围是 ([0..1] 的浮点数或 [0..255] 的整数)。

png