5.1 计算机视觉核心任务

内容讲解

计算机视觉 (Computer Vision, CV) 是一门致力于让机器“看懂”并解释视觉世界的科学。它结合了计算机科学、物理学、数学和神经科学,旨在赋予计算机类似于人类的视觉能力,使其能够从图像和视频中识别、跟踪、理解和重构三维世界。

核心任务
计算机视觉是一个广阔的领域,其核心任务可以被看作是回答关于一张图片的不同层次的问题:

  1. 图像分类 (Image Classification) - “这张图片里有什么?”

    • 目标:为整张图像分配一个单一的类别标签。
    • 示例:输入一张图片,系统输出“猫”、“狗”或“汽车”。这是CV领域最基础、最核心的任务,是许多其他复杂任务的基石。
  2. 目标检测 (Object Detection) - “这些东西在哪里?”

    • 目标:不仅要识别出图像中的物体,还要用边界框 (Bounding Box) 精确地标出它们的位置。
    • 示例:在自动驾驶场景中,系统需要实时框出道路上的所有“行人”、“车辆”、“自行车”和“交通信号灯”。
  3. 图像分割 (Image Segmentation) - “每个像素属于哪个物体?”

    • 目标:达到像素级别的理解,将图像中的每个像素分配给一个特定的类别。它比目标检测更精细。
    • 类型
      • 语义分割 (Semantic Segmentation):区分不同物体的像素区域,但不区分同类物体的不同实例。例如,将图中所有的人都标记为“人类”这个类别。
      • 实例分割 (Instance Segmentation):既区分不同类别,也区分同一类别的不同实例。例如,将图中的每个人分别标记为“人类1”、“人类2”、“人类3”。
  4. 图像生成 (Image Generation) - “根据描述创造一幅画。”

    • 目标:基于某些输入(如文本描述、草图或其他图像)创建全新的、逼真的图像。
    • 示例:使用生成对抗网络(GAN)或扩散模型(Diffusion Model)根据文本提示“一只穿着宇航服的猫在火星上弹吉他”来生成对应的、前所未见的图片。

图像数据表示
对于计算机而言,图像并非我们所见的连续画面,而是一个离散的数字矩阵。一张灰度图可以表示为一个二维矩阵,其中每个元素(像素)的值代表其亮度(通常在0-255之间,0代表黑色,255代表白色)。一张彩色图像则通常由三个独立的二维矩阵叠加而成,分别代表**红色®、绿色(G)和蓝色(B)**三个颜色通道。因此,一张 1920x1080 分辨率的彩色图像,在计算机眼中是一个 1920 x 1080 x 3 的三维张量(Tensor)。

计算机视觉任务示意图

graph TD
    subgraph 输入
        A[<img src='https://www.petmd.com/sites/default/files/styles/article_image/public/what-are-cat-trills-and-chirps.jpg' width='200' />];
    end

    subgraph 任务
        B(图像分类: "这是一只猫");
        C(目标检测: 框出猫的位置并标注 "猫");
        D(图像分割: 将属于猫的像素区域高亮);
    end

    A --> B;
    A --> C;
    A --> D;

    style B fill:#aaffaa,stroke:#333,stroke-width:2px;
    style C fill:#aaaaff,stroke:#333,stroke-width:2px;
    style D fill:#ffaaaa,stroke:#333,stroke-width:2px;

5.2 卷积神经网络 (CNN)

内容讲解

卷积神经网络 (Convolutional Neural Network, CNN) 是深度学习在计算机视觉领域取得革命性突破的核心技术。它是一种特殊设计的前馈神经网络,其架构灵感来源于生物的视觉皮层,特别擅长处理网格状数据(如图像)。CNN的核心思想是通过一系列可学习的滤波器(卷积核)自动、分层地学习图像中的空间层次结构特征。

CNN的核心组件:

  1. 卷积层 (Convolutional Layer)

    • 作用:特征提取的核心。可以想象成用一系列不同功能的“放大镜”去扫描图像。
    • 工作原理:使用一个称为卷积核 (Kernel)滤波器 (Filter) 的小型权重矩阵,在输入图像上按指定的步幅 (Stride) 进行滑动。在每个位置,卷积核与其覆盖的图像区域进行逐元素相乘再求和(点积运算),得到一个输出值。这个过程会生成一个特征图 (Feature Map),它代表了某种特定模式(如垂直边缘、水平边缘、特定颜色斑块)在原始图像中的激活程度。
    • 关键概念
      • 局部连接 (Local Connectivity):每个输出神经元只与输入的一个局部区域(感受野)相连,这符合视觉信息处理的局部性原理,并极大减少了参数数量。
      • 参数共享 (Parameter Sharing):同一个卷积核在整个图像上共享同一套权重。这意味着,如果一个卷积核学会了识别“猫耳朵”,它就能在图像的任何位置识别出“猫耳朵”,赋予了模型平移不变性
      • 填充 (Padding):在输入图像的边界周围添加额外的像素(通常是0),以控制输出特征图的空间尺寸,并确保边界像素能被卷积核充分处理。
  2. 激活函数 (Activation Function)

    • 在卷积操作之后,通常会应用一个非线性激活函数,最常用的是ReLU (Rectified Linear Unit)。它为网络引入了非线性,使得CNN能够学习更复杂的特征组合。
  3. 池化层 (Pooling Layer)

    • 作用:进行下采样(Downsampling),逐步减小特征图的空间维度。
    • 工作原理:在一个小窗口内(如2x2),取该窗口内所有值的**最大值(Max Pooling)平均值(Average Pooling)**作为输出。Max Pooling更常用,因为它能更好地保留最显著的特征。
    • 目的
      1. 减少计算量和参数数量,有效控制过拟合。
      2. 增大感受野,让后续的卷积层能看到更广阔的原始图像区域。
      3. 提供一定程度的平移、旋转和小尺度形变的不变性
  4. 全连接层 (Fully Connected Layer)

    • 作用:在经过多层卷积和池化提取到高级、抽象的特征后,进行最终的分类或回归任务。
    • 工作原理:将前面层输出的三维特征图“展平”(Flatten)成一个一维向量,然后将其输入到一个或多个传统的全连接神经网络层。最后,通常使用Softmax激活函数输出每个类别的概率分布。

经典CNN架构图 (LeNet-5)

输入图像 32x32
C1: 6个5x5卷积核, Stride=1
P1: 2x2最大池化, Stride=2
C2: 16个5x5卷积核, Stride=1
P2: 2x2最大池化, Stride=2
展平 Flatten
FC1: 全连接层, 120个神经元
FC2: 全连接层, 84个神经元
输出层: 10个类别, Softmax

5.3 经典CNN模型介绍

内容讲解

自2012年起,ImageNet大规模视觉识别挑战赛(ILSVRC)见证了一系列里程碑式的CNN模型的诞生,它们不仅在比赛中取得了压倒性胜利,更深刻地塑造了现代计算机视觉的发展轨迹。

  1. AlexNet (2012)

    • 贡献:作为深度学习在CV领域的“开山之作”,AlexNet首次证明了深度CNN在复杂大规模图像分类任务上的巨大潜力,其成功直接引爆了全球范围内的深度学习热潮。
    • 关键技术
      • 首次成功应用ReLU作为激活函数,有效缓解了梯度消失问题。
      • 使用Dropout在全连接层随机失活神经元,成为一种强大的正则化手段,有效防止了过拟合。
      • 广泛使用数据增强(如图像平移、翻转、裁剪)来扩充训练集。
      • 开创性地使用双GPU进行并行训练,解决了当时单个GPU显存不足的问题。
  2. VGGNet (2014)

    • 贡献:VGGNet的核心思想是“深度至关重要”。它系统地探索了网络深度对性能的影响,证明了通过反复堆叠非常小的(3x3)卷积核,可以构建出非常深(如16层、19层)且性能卓越的网络。
    • 特点:其结构极其简洁、规整、统一。整个网络只使用3x3的卷积和2x2的池化,这种模块化的设计思想深刻影响了后续模型的构建。两个3x3的卷积层串联的感受野等效于一个5x5的卷积层,但参数更少,非线性变换更多。
  3. GoogLeNet (Inception) (2014)

    • 贡献:在追求更深、更宽网络的同时,巧妙地控制了计算资源的消耗。它引入了创新的Inception模块,旨在构建一种“稀疏连接”的网络结构。
    • Inception模块:该模块的核心思想是“并行计算,多尺度融合”。在一个模块内部,它并行地使用不同尺寸的卷积核(1x1, 3x3, 5x5)和池化操作,然后将所有分支的输出特征图在深度上拼接(Concatenate)起来。这使得网络能够自适应地学习在不同空间尺度上最有用的特征组合。1x1卷积被大量用于降维和升维,极大地减少了计算量。
  4. ResNet (Residual Network) (2015)

    • 贡献:ResNet是深度学习历史上的一个重大突破,它优雅地解决了困扰学界已久的深度网络退化问题(即网络越深,训练误差反而越高的现象),使得训练数百甚至上千层的超深网络成为可能。
    • 核心思想:引入残差学习 (Residual Learning) 框架。其关键是残差连接 (Residual Connection) 或称快捷连接 (Shortcut Connection)。它允许输入信号(x)直接“跳过”一层或多层,与这些层的输出(F(x))相加,形成最终输出(H(x) = F(x) + x)。这种设计使得网络学习的目标从直接拟合一个复杂的函数(H(x))转变为学习一个更容易的残差函数(F(x) = H(x) - x)。如果恒等映射是最优的,网络只需将(F(x))的权重置为0即可,这比拟合恒等映射本身要容易得多。这种结构极大地促进了梯度在深层网络中的传播。
    graph TD
        subgraph 残差块 (Residual Block)
            A_input[输入 x] --> B(权重层1);
            B --> C(激活函数 ReLU);
            C --> D(权重层2);
            D -- "F(x)" --> E(加法操作);
            A_input -- "快捷连接 (Shortcut)" --> E;
            E --> F_output[输出 H(x) = F(x) + x];
        end
        style D fill:#f9f, stroke:#333, stroke-width:2px
        style B fill:#f9f, stroke:#333, stroke-width:2px
    
  5. DenseNet (2017)

    • 贡献:将ResNet的“跳跃连接”思想推向了极致,提出了密集连接 (Dense Connection) 的概念。
    • 核心思想:在DenseNet中,每个层都会接收其前面所有层的输出作为输入,并将其自身的输出特征图传递给后续所有层。这种“密集”的连接方式极大地促进了特征的复用,缓解了梯度消失问题,并且在参数效率上通常优于ResNet。
  6. Vision Transformer (ViT) (2020)

    • 贡献:ViT是另一个里程碑,它成功地将原本在自然语言处理领域大放异彩的Transformer架构应用于计算机视觉任务,并取得了与顶级CNN相媲美甚至超越的性能。
    • 核心思想:ViT完全抛弃了CNN的卷积和池化操作。它首先将输入图像分割成一系列固定大小的图像块 (Patches),将每个图像块线性嵌入成一个向量(类似于NLP中的词嵌入),然后将这些向量序列连同一个可学习的“分类令牌”一起输入到标准的Transformer编码器中。通过自注意力机制,ViT能够捕捉图像中长距离的依赖关系,展现出强大的全局建模能力。它的成功挑战了CNN在视觉领域的统治地位,开启了CV架构设计的新篇章。

5.4 代码实战:使用Keras构建CNN进行图像分类

下面是一个完整的示例,展示了如何使用TensorFlow/Keras库构建一个简单的CNN模型,并在经典的CIFAR-10数据集上进行训练和评估。

import tensorflow as tf
from tensorflow.keras import layers, models, datasets
import matplotlib.pyplot as plt

# 1. 加载并预处理CIFAR-10数据集
(train_images, train_labels), (test_images, test_labels) = datasets.cifar10.load_data()

# 将像素值归一化到0-1之间
train_images, test_images = train_images / 255.0, test_images / 255.0

# CIFAR-10的类别名称
class_names = ['airplane', 'automobile', 'bird', 'cat', 'deer',
               'dog', 'frog', 'horse', 'ship', 'truck']

# 可视化部分数据
plt.figure(figsize=(10,10))
for i in range(25):
    plt.subplot(5,5,i+1)
    plt.xticks([])
    plt.yticks([])
    plt.grid(False)
    plt.imshow(train_images[i])
    plt.xlabel(class_names[train_labels[i][0]])
plt.show()

# 2. 构建卷积神经网络模型
model = models.Sequential()
# 第一组卷积-池化层
model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(32, 32, 3)))
model.add(layers.MaxPooling2D((2, 2)))
# 第二组卷积-池化层
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
# 第三组卷积层
model.add(layers.Conv2D(64, (3, 3), activation='relu'))

# 在卷积层之上添加全连接层
model.add(layers.Flatten())
model.add(layers.Dense(64, activation='relu'))
model.add(layers.Dense(10)) # 输出层,10个类别

# 打印模型结构
model.summary()

# 3. 编译并训练模型
model.compile(optimizer='adam',
              loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
              metrics=['accuracy'])

history = model.fit(train_images, train_labels, epochs=10, 
                    validation_data=(test_images, test_labels))

# 4. 评估模型
plt.plot(history.history['accuracy'], label='accuracy')
plt.plot(history.history['val_accuracy'], label = 'val_accuracy')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.ylim([0.5, 1])
plt.legend(loc='lower right')
plt.show()

test_loss, test_acc = model.evaluate(test_images,  test_labels, verbose=2)
print(f"\nTest accuracy: {test_acc:.4f}")

8.5 图像生成与风格迁移

8.5.1 生成对抗网络 (GANs)

  • 基本原理:由一个生成器(Generator)和一个判别器(Discriminator)组成,通过相互博弈的方式进行训练。生成器努力生成逼真的图像来“欺骗”判别器,而判别器则努力区分真实图像和生成图像。
  • 经典模型:DCGAN, StyleGAN, CycleGAN

8.5.2 变分自编码器 (VAEs)

  • 基本原理:一种生成模型,通过学习数据的潜在表示(latent representation)来生成新的数据样本。

8.5.3 神经风格迁移 (Neural Style Transfer)

  • 核心思想:将一张内容图片(Content Image)的内容和一张风格图片(Style Image)的风格结合起来,生成一张新的、具有特定艺术风格的图像。

8.5.4 代码实战:使用PyTorch实现神经风格迁移

import torch
import torch.nn as nn
import torch.optim as optim
from PIL import Image
import torchvision.transforms as transforms
import torchvision.models as models
import copy

# --- 设备配置 ---
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# --- 图像加载与预处理 ---
# 定义图像尺寸
imsize = 512 if torch.cuda.is_available() else 128  # 如果没有GPU,使用小尺寸

loader = transforms.Compose([
    transforms.Resize(imsize),  # 缩放图像
    transforms.ToTensor()])  # 转换为Tensor

def image_loader(image_name):
    image = Image.open(image_name)
    # 伪造一个batch维度以适应网络输入
    image = loader(image).unsqueeze(0)
    return image.to(device, torch.float)

# --- 加载内容与风格图像 ---
# 请将 "content.jpg" 和 "style.jpg" 替换为您自己的图像文件路径
# 为了演示,我们先创建虚拟的图像文件
Image.new('RGB', (200, 200), color = 'red').save("content.jpg")
Image.new('RGB', (200, 200), color = 'blue').save("style.jpg")

style_img = image_loader("style.jpg")
content_img = image_loader("content.jpg")

assert style_img.size() == content_img.size(), \
    "我们需要输入相同尺寸的风格和内容图像"

# --- 定义损失函数 ---
class ContentLoss(nn.Module):
    def __init__(self, target,):
        super(ContentLoss, self).__init__()
        self.target = target.detach()

    def forward(self, input):
        self.loss = nn.functional.mse_loss(input, self.target)
        return input

def gram_matrix(input):
    a, b, c, d = input.size()  # a=batch size(=1)
    # b=number of feature maps
    # (c,d)=dimensions of a f. map (N=c*d)
    features = input.view(a * b, c * d)
    G = torch.mm(features, features.t())  # 计算gram product
    return G.div(a * b * c * d)

class StyleLoss(nn.Module):
    def __init__(self, target_feature):
        super(StyleLoss, self).__init__()
        self.target = gram_matrix(target_feature).detach()

    def forward(self, input):
        G = gram_matrix(input)
        self.loss = nn.functional.mse_loss(G, self.target)
        return input

# --- 加载预训练模型 (VGG19) ---
cnn = models.vgg19(pretrained=True).features.to(device).eval()

cnn_normalization_mean = torch.tensor([0.485, 0.456, 0.406]).to(device)
cnn_normalization_std = torch.tensor([0.229, 0.224, 0.225]).to(device)

class Normalization(nn.Module):
    def __init__(self, mean, std):
        super(Normalization, self).__init__()
        self.mean = torch.tensor(mean).view(-1, 1, 1)
        self.std = torch.tensor(std).view(-1, 1, 1)

    def forward(self, img):
        return (img - self.mean) / self.std

# --- 构建风格迁移模型 ---
content_layers_default = ['conv_4']
style_layers_default = ['conv_1', 'conv_2', 'conv_3', 'conv_4', 'conv_5']

def get_style_model_and_losses(cnn, normalization_mean, normalization_std,
                               style_img, content_img,
                               content_layers=content_layers_default,
                               style_layers=style_layers_default):
    cnn = copy.deepcopy(cnn)
    normalization = Normalization(normalization_mean, normalization_std).to(device)
    content_losses = []
    style_losses = []

    model = nn.Sequential(normalization)

    i = 0  # increment every time we see a conv
    for layer in cnn.children():
        if isinstance(layer, nn.Conv2d):
            i += 1
            name = 'conv_{}'.format(i)
        elif isinstance(layer, nn.ReLU):
            name = 'relu_{}'.format(i)
            layer = nn.ReLU(inplace=False)
        elif isinstance(layer, nn.MaxPool2d):
            name = 'pool_{}'.format(i)
        elif isinstance(layer, nn.BatchNorm2d):
            name = 'bn_{}'.format(i)
        else:
            raise RuntimeError('Unrecognized layer: {}'.format(layer.__class__.__name__))

        model.add_module(name, layer)

        if name in content_layers:
            target = model(content_img).detach()
            content_loss = ContentLoss(target)
            model.add_module("content_loss_{}".format(i), content_loss)
            content_losses.append(content_loss)

        if name in style_layers:
            target_feature = model(style_img).detach()
            style_loss = StyleLoss(target_feature)
            model.add_module("style_loss_{}".format(i), style_loss)
            style_losses.append(style_loss)

    for i in range(len(model) - 1, -1, -1):
        if isinstance(model[i], ContentLoss) or isinstance(model[i], StyleLoss):
            break

    model = model[:(i + 1)]

    return model, style_losses, content_losses

# --- 运行模型 ---
def get_input_optimizer(input_img):
    optimizer = optim.LBFGS([input_img.requires_grad_()])
    return optimizer

def run_style_transfer(cnn, normalization_mean, normalization_std,
                       content_img, style_img, input_img,
                       num_steps=300, style_weight=1000000, content_weight=1):
    print('Building the style transfer model..')
    model, style_losses, content_losses = get_style_model_and_losses(
        cnn, normalization_mean, normalization_std, style_img, content_img)
    optimizer = get_input_optimizer(input_img)

    print('Optimizing..')
    run = [0]
    while run[0] <= num_steps:

        def closure():
            input_img.data.clamp_(0, 1)
            optimizer.zero_grad()
            model(input_img)
            style_score = 0
            content_score = 0

            for sl in style_losses:
                style_score += sl.loss
            for cl in content_losses:
                content_score += cl.loss

            style_score *= style_weight
            content_score *= content_weight

            loss = style_score + content_score
            loss.backward()

            run[0] += 1
            if run[0] % 50 == 0:
                print("run {}:".format(run))
                print('Style Loss : {:4f} Content Loss: {:4f}'.format(
                    style_score.item(), content_score.item()))
                print()

            return style_score + content_score

        optimizer.step(closure)

    input_img.data.clamp_(0, 1)
    return input_img

总结

本章带领我们进入了计算机视觉的奇妙世界。我们首先系统地了解了CV的四大核心任务:图像分类、目标检测、图像分割和图像生成,它们共同构成了机器“视觉智能”的骨架。接着,我们深入解构了驱动这一切的引擎——卷积神经网络 (CNN)。通过生动的比喻,我们理解了其核心组件的运作机制:卷积层通过局部连接参数共享高效提取特征,池化层进行降维并增强不变性,而全连接层则负责最后的决策。我们还学习了填充步幅等关键概念。最后,我们回顾了CV发展史上的里程碑模型:点燃革命之火的AlexNet,以深度和规整著称的VGGNet,以高效和多尺度融合为特点的GoogLeNet (Inception),通过残差连接彻底改变深度网络训练格局的ResNet,以及将连接思想推向极致的DenseNet和颠覆传统范式的Vision Transformer (ViT)。为了将理论付诸实践,我们还提供了一个完整的Keras代码示例,手把手教你构建并训练一个CNN模型来解决真实的图像分类问题。这些经典与现代的模型共同构成了当今无数视觉应用得以实现的坚实地基。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐