计算机视觉就像给 AI 装上了 “眼睛”,让机器能看懂图像和视频里的世界。从手机相册的自动分类,到自动驾驶的行人检测,再到监控里的目标追踪,背后都离不开四个最基础也最核心的任务:目标分类、目标检测、目标分割、目标跟踪

一、目标分类:给整张图片 “贴个标签”

核心任务:只回答 “图里有什么”

目标分类是计算机视觉最基础的任务,它的工作特别简单:输入一张完整的图片,输出这张图片属于哪个类别,以及对应的概率。

完全不关心物体在图片里的位置,也不关心有几个物体,只给整张图打一个(或多个)标签。比如输入一张猫咪照片,它只会告诉你 “这是猫,概率 98%”,不会说猫在图片左上角还是右下角。

任务特点

  • 最基础的视觉任务,分为单标签分类(一张图只有一个物体)和多标签分类(一张图有多个物体)
  • 不管目标的数量、大小和坐标,只关注整体类别
  • 经典应用:手写数字识别、图片分类检索、垃圾邮件图片过滤

PyTorch 极简实现:LeNet5 手写数字分类

# PyTorch核心库
import torch
# 构建神经网络需要的模块
import torch.nn as nn
# 优化器(更新模型参数)
import torch.optim as optim
# 数据集、图像预处理工具
from torchvision import datasets, transforms
# 数据加载器:批量读取数据
from torch.utils.data import DataLoader
# 画图工具,用于显示测试图片
import matplotlib.pyplot as plt


# ------------------- 定义LeNet5卷积神经网络 -------------------
# 继承nn.Module,所有PyTorch模型都要继承这个类
class LeNet5(nn.Module):
    # 初始化函数:定义网络的每一层
    # num_classes=10 表示手写数字10分类(0-9)
    def __init__(self, num_classes=10):
        # 调用父类初始化方法,固定写法
        super(LeNet5, self).__init__()

        # 特征提取层:卷积+池化,用来提取图像特征
        self.features = nn.Sequential(
            # 第一层卷积:输入通道1(灰度图),输出通道6,卷积核5×5,步长1
            nn.Conv2d(1, 6, kernel_size=5, stride=1),
            # 激活函数:增加非线性
            nn.Tanh(),
            # 平均池化:缩小图片尺寸,减少计算量
            nn.AvgPool2d(kernel_size=2, stride=2),

            # 第二层卷积:输入通道6,输出通道16,卷积核5×5
            nn.Conv2d(6, 16, kernel_size=5, stride=1),
            nn.Tanh(),
            nn.AvgPool2d(kernel_size=2, stride=2),
        )

        # 分类器:全连接层,把特征映射成类别概率
        self.classifier = nn.Sequential(
            # 第一层全连接:输入是16个通道×4×4特征图
            nn.Linear(16 * 4 * 4, 120),
            nn.Tanh(),
            # 第二层全连接
            nn.Linear(120, 84),
            nn.Tanh(),
            # 最后一层:输出10个分类(0-9)
            nn.Linear(84, num_classes),
        )

    # 前向传播函数:数据在网络中流动的路径
    def forward(self, x):
        # 图像 -> 特征提取层
        x = self.features(x)
        # 把多维特征展平成一维(全连接层需要一维输入)
        x = torch.flatten(x, 1)
        # 展平后的特征 -> 分类层 -> 输出结果
        x = self.classifier(x)
        return x


# ------------------- 设置超参数 -------------------
# 每次训练喂入64张图片
batch_size = 64
# 学习率:控制参数更新的步长
lr = 0.001
# 训练5轮(整个数据集过5遍)
epochs = 5
# 自动选择设备:有GPU用GPU,没有用CPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# ------------------- 图像预处理(标准化) -------------------
transform = transforms.Compose([
    # 把图片转成PyTorch张量
    transforms.ToTensor(),
    # 归一化:让数据分布更稳定,训练更快
    transforms.Normalize((0.1307,), (0.3081,))
])

# ------------------- 加载MNIST数据集 -------------------
# 训练集:train=True
train_dataset = datasets.MNIST(
    root='./data',  # 数据集路径
    train=True,  # 训练集
    download=False,  # 不下载,使用本地已有数据
    transform=transform  # 应用预处理
)
# 测试集:train=False
test_dataset = datasets.MNIST(
    root='./data',
    train=False,
    download=False,
    transform=transform
)

# 数据加载器:批量、乱序加载数据
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)

# ------------------- 模型、损失函数、优化器 -------------------
# 创建模型,并搬到GPU/CPU
model = LeNet5().to(device)
# 损失函数:分类任务用交叉熵
criterion = nn.CrossEntropyLoss()
# 优化器:Adam,自动更新网络参数
optimizer = optim.Adam(model.parameters(), lr=lr)


# ------------------- 6. 训练函数 -------------------
def train():
    # 开启训练模式(启用Dropout/BatchNorm等)
    model.train()

    # 循环训练多轮
    for epoch in range(epochs):
        total_loss = 0  # 累计损失
        correct = 0  # 预测正确的数量
        total = 0  # 总样本数

        # 遍历训练集的每一批数据
        for data, target in train_loader:
            # 数据搬到设备上
            data, target = data.to(device), target.to(device)

            optimizer.zero_grad()  # 清空上一轮梯度
            output = model(data)  # 前向传播,得到预测结果
            loss = criterion(output, target)  # 计算损失
            loss.backward()  # 反向传播,计算梯度
            optimizer.step()  # 更新参数

            # 统计
            total_loss += loss.item()
            pred = output.argmax(dim=1)  # 取概率最大的类别
            correct += pred.eq(target).sum().item()  # 正确数
            total += target.size(0)  # 总数

        # 计算这一轮的平均损失和准确率
        avg_loss = total_loss / len(train_loader)
        acc = 100 * correct / total
        print(f"Epoch [{epoch + 1}/{epochs}] | Loss: {avg_loss:.4f} | Acc: {acc:.2f}%")


# ------------------- 测试函数:计算测试集准确率 -------------------
def test():
    # 开启评估模式,不计算梯度
    model.eval()
    correct = 0
    total = 0

    # 无梯度上下文,节省内存
    with torch.no_grad():
        for data, target in test_loader:
            data, target = data.to(device), target.to(device)
            output = model(data)
            pred = output.argmax(dim=1)
            correct += pred.eq(target).sum().item()
            total += target.size(0)

    acc = 100 * correct / total
    print(f"\n测试集准确率: {acc:.2f}%")


# ------------------- 显示测试样本与预测结果 -------------------
def show_test_sample(n=8):
    model.eval()
    # 从测试集取一批图片
    test_iter = iter(test_loader)
    imgs, labels = next(test_iter)
    imgs = imgs.to(device)

    # 不计算梯度,进行预测
    with torch.no_grad():
        preds = model(imgs).argmax(dim=1)

    # 反归一化:把图片恢复成正常灰度图
    mean = torch.tensor(0.1307)
    std = torch.tensor(0.3081)
    imgs_restore = imgs.cpu() * std + mean

    # 画图
    plt.figure(figsize=(12, 5))
    for idx in range(n):
        plt.subplot(2, n // 2, idx + 1)
        # 显示图片,去掉通道维度
        plt.imshow(imgs_restore[idx].squeeze(), cmap='gray')
        # 标题显示真实标签和预测标签
        plt.title(f"True:{labels[idx].item()}, Pred:{preds[idx].item()}")
        plt.axis('off')  # 关闭坐标轴
    plt.tight_layout()  # 自动调整布局
    plt.show()


# ------------------- 9. 主程序入口 -------------------
if __name__ == "__main__":
    print("开始训练...")
    train()  # 训练模型
    print("\n训练完成,开始测试...")
    test()  # 测试准确率
    print("\n输出测试样本图片...")
    show_test_sample(n=8)  # 显示8张测试图

输出结果:

二、目标检测:不仅认得出,还能 “框得住”

核心任务:回答 “有什么、在哪里、有几个”

目标分类只能告诉我们图里有什么,而目标检测在分类的基础上,多了定位能力。它会用矩形框(包围盒 bbox)把每个物体圈出来,同时标注物体的类别和置信度。

输入还是一张图片,输出是多个(类别, 包围框坐标[x1,y1,x2,y2], 置信度)的组合。比如一张有 2 只猫 1 只狗的图片,它会输出 3 个矩形框,分别标注 “猫”“猫”“狗”,并给出每个框的准确位置。

任务特点

  • 解决 “在哪 + 是什么 + 有几个” 三大问题
  • 用矩形框近似物体轮廓,框内所有像素都算该目标
  • 工业界最常用的算法:YOLO 系列(速度快)、Faster R-CNN(精度高)
  • 经典应用:自动驾驶的车辆 / 行人检测、安防监控的异常检测、电商的商品识别

PyTorch 极简实现:MNIST 数字检测

import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt
import matplotlib.patches as patches  # 用来画检测框


# ==================== 定义目标检测网络 ====================
# 继承nn.Module,这是Pytorch模型的固定写法
class MNISTDetNet(nn.Module):
    # 初始化函数:搭建网络结构
    def __init__(self, num_classes=10):
        super(MNISTDetNet, self).__init__()

        # 主干网络 Backbone:负责提取图像特征
        self.backbone = nn.Sequential(
            # 第一层卷积:输入1通道(灰度图) → 输出16通道,3x3卷积核
            nn.Conv2d(1, 16, kernel_size=3, padding=1),
            nn.ReLU(),  # 激活函数,增加非线性
            nn.MaxPool2d(2),  # 最大池化,图片尺寸缩小一半

            # 第二层卷积:16通道 → 32通道
            nn.Conv2d(16, 32, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2),
        )

        # 检测任务有两个输出头:分类头 + 回归头

        # 1. 分类头:输出数字类别 0-9
        # 输入是32个通道,每个通道7x7,展平后输入全连接层
        self.cls_head = nn.Linear(32 * 7 * 7, num_classes)

        # 2. 回归头:输出4个值 → 检测框坐标 (x1, y1, x2, y2)
        self.reg_head = nn.Linear(32 * 7 * 7, 4)

    # 前向传播:数据在网络中怎么走
    def forward(self, x):
        # 第一步:图像经过卷积层提取特征
        x = self.backbone(x)

        # 展平:把多维特征变成一维向量,才能输入全连接层
        x = torch.flatten(x, 1)

        # 第二步:两个头分别输出结果
        cls_out = self.cls_head(x)  # 分类结果
        reg_out = self.reg_head(x)  # 检测框结果

        return cls_out, reg_out  # 返回:类别 + 框坐标


# ==================== 超参数 & 设备设置 ====================
batch_size = 64  # 每次训练喂入64张图片
lr = 0.001  # 学习率:控制参数更新步长
epochs = 5  # 训练5轮
# 自动选择设备:有GPU用GPU,没有用CPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# ==================== 加载本地MNIST数据集 ====================
# 图像预处理:转张量 + 归一化
transform = transforms.Compose([
    transforms.ToTensor(),  # 图片 → 张量
    transforms.Normalize((0.1307,), (0.3081,))  # 归一化,训练更稳定
])

# 训练集
train_dataset = datasets.MNIST(
    root='./data',
    train=True,
    download=False,
    transform=transform
)

# 测试集
test_dataset = datasets.MNIST(
    root='./data',
    train=False,
    download=False,
    transform=transform
)

# 数据加载器:批量、乱序加载数据
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)

# ==================== 模型、损失函数、优化器 ====================
model = MNISTDetNet().to(device)  # 创建模型并搬到设备上

# 分类损失:判断数字预测是否正确
cls_loss_fn = nn.CrossEntropyLoss()

# 回归损失:判断检测框坐标是否准确
reg_loss_fn = nn.SmoothL1Loss()

# 优化器:Adam,自动更新网络参数
optimizer = optim.Adam(model.parameters(), lr=lr)


# ==================== 训练函数 ====================
def train():
    model.train()  # 开启训练模式

    # 循环训练多轮
    for epoch in range(epochs):
        total_cls_loss = 0  # 累计分类损失
        total_reg_loss = 0  # 累计框损失
        correct = 0  # 预测正确数量
        total = 0  # 总样本数

        # 遍历每一批数据
        for data, target in train_loader:
            data, target = data.to(device), target.to(device)

            # 目标检测标签:
            # 分类标签:target(真实数字)
            # 框标签:MNIST数字占满整个图片,固定标签 [0,0,1,1]
            box_target = torch.tensor([[0.0, 0.0, 1.0, 1.0]] * data.size(0)).to(device)

            optimizer.zero_grad()  # 清空上一轮梯度
            cls_out, reg_out = model(data)  # 前向传播,得到两个输出

            # 计算两个损失
            cls_loss = cls_loss_fn(cls_out, target)
            reg_loss = reg_loss_fn(reg_out, box_target)

            # 总损失 = 分类损失 + 回归损失
            loss = cls_loss + reg_loss

            loss.backward()  # 反向传播,计算梯度
            optimizer.step()  # 更新模型参数

            # 统计训练信息
            total_cls_loss += cls_loss.item()
            total_reg_loss += reg_loss.item()
            pred = cls_out.argmax(dim=1)  # 取概率最大的类别
            correct += pred.eq(target).sum().item()  # 正确数
            total += target.size(0)

        # 计算这一轮的准确率
        cls_acc = 100 * correct / total

        # 打印日志
        print(f"Epoch {epoch + 1} | 分类准确率: {cls_acc:.2f}% | "
              f"分类损失: {total_cls_loss:.4f} | 框损失: {total_reg_loss:.4f}")


# ==================== 测试 + 可视化检测结果 ====================
def visualize_detection():
    model.eval()  # 开启评估模式

    # 从测试集取一批数据
    data, target = next(iter(test_loader))
    data = data.to(device)

    # 不计算梯度,节省内存
    with torch.no_grad():
        cls_out, reg_out = model(data)

    preds = cls_out.argmax(dim=1)  # 预测数字
    boxes = reg_out.cpu().numpy()  # 预测框坐标

    # 绘制前8张测试图片 + 检测框
    plt.figure(figsize=(12, 5))
    for i in range(8):
        img = data[i].cpu().squeeze()  # 去掉通道维度,显示图片
        pred_num = preds[i].item()  # 预测数字
        true_num = target[i].item()  # 真实数字

        # 取出模型预测的框
        x1, y1, x2, y2 = boxes[i]

        # 把归一化坐标(0~1)转为图片像素坐标(0~28)
        x1 *= 28
        y1 *= 28
        x2 *= 28
        y2 *= 28

        plt.subplot(2, 4, i + 1)
        plt.imshow(img, cmap='gray')

        # 获取当前绘图对象,用于画框
        ax = plt.gca()

        # 画红色检测框
        rect = patches.Rectangle(
            (x1, y1),  # 框左上角
            x2 - x1,  # 宽度
            y2 - y1,  # 高度
            linewidth=2,  # 线宽
            edgecolor='r',  # 红色
            facecolor='none'
        )
        ax.add_patch(rect)

        plt.title(f"True:{true_num}, Pred:{pred_num}")
        plt.axis('off')  # 关闭坐标轴

    plt.tight_layout()  # 自动调整布局
    plt.show()


# ==================== 主程序入口 ====================
if __name__ == "__main__":
    train()
    visualize_detection()

输出结果:

三、目标分割:像素级精准 “描轮廓”

目标检测用矩形框圈物体,但很多时候我们需要更精准的边界 —— 比如医疗影像里分割肿瘤、自动驾驶里分割车道线,这时候就需要目标分割。它不再用方框近似,而是逐像素判断每个像素属于哪个物体,精准勾勒出物体的轮廓。

目标分割主要分为两类,适用场景完全不同:

(1)语义分割:按类别分像素,不区分个体

语义分割会给图片里的每个像素分配一个类别标签,但同类物体不会区分不同个体。比如一张有两只猫的图片,语义分割会把所有猫的像素都标成同一种颜色,但分不清哪只是猫 A,哪只是猫 B。

(2)实例分割:检测 + 分割,区分每个个体

实例分割是目前目标分割的主流,它相当于目标检测 + 像素掩码。它不仅能区分不同类别的物体,还能区分同一类别的不同个体,给每个独立目标生成一张不规则的蒙版,精准贴合物体的边缘(比如毛发、身体曲线)。

代表算法:Mask R-CNN、YOLO-Seg 经典应用:人像抠图、医疗影像分割、工业零件缺陷检测

PyTorch 极简实现:Mini U-Net 数字分割

# PyTorch核心库
import torch
# 构建神经网络的模块
import torch.nn as nn
# 优化器,用于更新网络参数
import torch.optim as optim
# 导入数据集(MNIST)和图像预处理工具
from torchvision import datasets, transforms
# 数据加载器,用于批量读取数据
from torch.utils.data import DataLoader
# 绘图库,用于显示图片
import matplotlib.pyplot as plt
# 数值计算库,用于处理图像数组
import numpy as np


# ==================== 定义经典分割网络:Mini U-Net ====================
# 任务:输入一张手写数字图,输出一张同样大小的掩码图
# 掩码图中:数字区域=1,背景区域=0
class MiniUNet(nn.Module):
    # 初始化函数,定义网络的每一层结构
    def __init__(self):
        super(MiniUNet, self).__init__()  # 固定写法,调用父类初始化

        # ------------------- 编码器 (Encoder):下采样 -------------------
        # 作用:通过卷积和池化,把图片变小,提取图像特征
        # 输入: 28x28 -> 输出: 14x14
        self.enc1 = nn.Sequential(
            # 卷积层:输入1通道(灰度图),输出16通道,卷积核3x3
            nn.Conv2d(1, 16, kernel_size=3, padding=1),
            # 激活函数:增加网络非线性表达能力
            nn.ReLU(),
            # 池化层:尺寸减半 (28x28 -> 14x14)
            nn.MaxPool2d(2)
        )

        # 输入: 14x14 -> 输出: 7x7
        self.enc2 = nn.Sequential(
            # 卷积层:输入16通道,输出32通道
            nn.Conv2d(16, 32, kernel_size=3, padding=1),
            nn.ReLU(),
            # 池化层:尺寸减半 (14x14 -> 7x7)
            nn.MaxPool2d(2)
        )

        # ------------------- 解码器 (Decoder):上采样 -------------------
        # 作用:通过反卷积,把小图放大,恢复成原图尺寸
        # 输入: 7x7 -> 输出: 14x14
        self.dec1 = nn.Sequential(
            # 反卷积(转置卷积):专门用于放大图像
            # 输入32通道 -> 输出16通道,尺寸扩大2倍
            nn.ConvTranspose2d(32, 16, kernel_size=2, stride=2),
            nn.ReLU()
        )

        # 输入: 14x14 -> 输出: 28x28
        self.dec2 = nn.Sequential(
            # 反卷积:输入16通道,输出16通道,尺寸再扩大2倍 (回到28x28)
            nn.ConvTranspose2d(16, 16, kernel_size=2, stride=2),
            nn.ReLU()
        )

        # ------------------- 输出层 -------------------
        # 最后输出1个通道,代表分割结果(前景/背景)
        self.out = nn.Conv2d(16, 1, kernel_size=3, padding=1)
        # Sigmoid函数:把输出值压缩到 [0, 1] 之间,表示是数字的概率
        self.sigmoid = nn.Sigmoid()

    # 前向传播函数:定义数据在网络中的流动路径
    def forward(self, x):
        # 1. 编码器提取特征(图像变小)
        x1 = self.enc1(x)
        x2 = self.enc2(x1)

        # 2. 解码器恢复尺寸(图像变大)
        x = self.dec1(x2)
        x = self.dec2(x)

        # 3. 输出分割掩码
        out = self.out(x)
        # 经过sigmoid,每个像素值都在0~1之间
        out = self.sigmoid(out)
        return out


# ==================== 超参数与设备设置 ====================
batch_size = 64      # 批次大小:训练时一次性喂入64张图片
lr = 0.001          # 学习率:控制参数更新的速度
epochs = 5          # 训练轮数:把整个数据集完整过5遍
# 自动检测设备:有GPU用GPU,没有就用CPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# ==================== 4. 加载本地MNIST数据集 ====================
# 图像预处理操作
transform = transforms.Compose([
    transforms.ToTensor(),  # 将PIL图片转换为Tensor张量 (值范围0~1)
])

# 加载训练集(使用本地data文件夹下的数据,不重新下载)
train_dataset = datasets.MNIST(root='./data', train=True, download=False, transform=transform)
# 加载测试集
test_dataset = datasets.MNIST(root='./data', train=False, download=False, transform=transform)

# 创建数据加载器
# 训练集:打乱数据(shuffle=True),保证训练随机性
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
# 测试集:batch_size=8只为了方便画图,不打乱顺序
test_loader = DataLoader(test_dataset, batch_size=8, shuffle=False)

# ==================== 5. 模型、损失函数、优化器初始化 ====================
# 创建模型实例,并将模型搬到GPU/CPU上
model = MiniUNet().to(device)

# 损失函数:BCELoss (二值交叉熵损失)
# 用于比较:预测的掩码 和 真实的掩码 之间的差距
criterion = nn.BCELoss()

# 优化器:Adam
# 作用:根据计算出的梯度,自动更新网络的参数
optimizer = optim.Adam(model.parameters(), lr=lr)


# ==================== 训练函数 ====================
def train():
    model.train()  # 开启训练模式
    for epoch in range(epochs):
        total_loss = 0  # 累计每一轮的总损失

        # 遍历训练集中的每一批数据
        for data, _ in train_loader:
            data = data.to(device)  # 将数据搬到指定设备

            # -------- 构造分割标签(真值掩码)--------
            # 原理:原图中像素值 > 0.5 的是数字(白色),标记为1
            #       像素值 < 0.5 的是背景(黑色),标记为0
            mask_target = (data > 0.5).float()

            optimizer.zero_grad()   # 清空上一步的梯度
            pred_mask = model(data) # 前向传播,得到模型预测的掩码

            # 计算损失:预测值 和 真实值 差多少
            loss = criterion(pred_mask, mask_target)

            loss.backward()   # 反向传播,计算梯度
            optimizer.step()  # 更新网络参数

            total_loss += loss.item()  # 累加损失

        # 打印训练日志
        print(f"Epoch {epoch + 1}/{epochs} | 分割损失: {total_loss:.4f}")


# ==================== 可视化分割结果 ====================
def show_segmentation():
    model.eval()  # 开启评估/测试模式
    # 从测试集中取一批数据(8张图片)
    data, _ = next(iter(test_loader))
    data = data.to(device)

    # with torch.no_grad():测试时不计算梯度,节省内存和算力
    with torch.no_grad():
        pred_mask = model(data)  # 模型预测分割掩码

    # 绘图
    plt.figure(figsize=(12, 5))
    for i in range(8):
        # 上排:显示原始输入图片
        plt.subplot(2, 8, i + 1)
        # .squeeze() 去掉多余的维度,从 (1,28,28) 变成 (28,28)
        plt.imshow(data[i].cpu().squeeze(), cmap='gray')
        plt.title("Input")
        plt.axis('off')  # 不显示坐标轴

        # 下排:显示模型输出的分割掩码
        plt.subplot(2, 8, i + 9)
        # 将预测结果从Tensor转为numpy数组
        mask = pred_mask[i].cpu().squeeze().numpy()

        # -------- 将灰度掩码转为黄色掩码 --------
        yellow_mask = np.zeros((28, 28, 3)) # 创建一个黑色的RGB图
        yellow_mask[..., 0] = mask          # 红色通道 = 掩码值
        yellow_mask[..., 1] = mask          # 绿色通道 = 掩码值
        # 红色 + 绿色 = 黄色,背景保持黑色

        plt.imshow(yellow_mask)
        plt.title("Mask")
        plt.axis('off')

    plt.tight_layout()
    plt.show()


# ==================== 主程序入口 ====================
if __name__ == "__main__":
    
    train()
    
show_segmentation()

输出结果:

关键知识点:

反卷积:即转置卷积,在线性代数层面等价于卷积权重矩阵转置后的矩阵运算,区别于固定规则的插值上采样,它依靠自主学习的卷积参数实现特征图尺寸扩充,普通卷积依靠池化、大步长压缩图像完成下采样,转置卷积则通过步长插零、边缘补零的预处理再卷积实现上采样放大,虽无法精准还原卷积前原始像素,但能自适应恢复图像细节,是语义分割、图像生成网络解码器的核心上采样模块,代码中两层转置卷积依次把 7×7→14×14、14×14→28×28,最终复原原图尺寸用于像素分割预测。

构造分割标签:MNIST 里数字是白色(像素值高),背景是黑色(像素值低),所以我们直接把像素值大于 0.5 的标记为 1(数字),小于 0.5 的标记为 0(背景)

四、目标跟踪:视频里 “追着跑” 的技术

核心任务:跨帧锁定同一个目标

前面三个任务都是处理单张图片,而目标跟踪是视频时序任务。它基于前一帧的检测 / 分割结果,在后续的视频帧里持续锁定同一个目标,给每个目标分配唯一的 ID,不管目标怎么移动、遮挡,都能保持 ID 不变。

输入是连续的视频帧,输出是每个目标的唯一 ID、实时位置(或掩码)。它的前置依赖是目标检测或分割:第一帧先检出所有目标,后续帧不用重复检测全图,只需要追踪这些 ID 对应的目标。

任务分类

  • 检测跟踪(一阶段):一帧同时完成检测和跟踪 ID 分配,速度快、工业界最常用,代表算法:JDE、ByteTrack
  • 单目标跟踪(SOT):手动框选一个目标,全程只跟踪这一个物体,适用于特定目标追踪场景

经典应用:监控视频的行人追踪、体育赛事的运动员追踪、无人机的目标跟随

PyTorch 极简实现:SiamFC 单目标跟踪

import torch
import torch.nn as nn
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation  # 用于动态画图(视频效果)
from torchvision import datasets, transforms  # 加载MNIST数据集


# ==================== 1. 定义经典目标跟踪网络:SiamFC(孪生全卷积网络) ====================
# 核心思想:输入两个图
#   template(模板):要跟踪的目标
#   search(搜索图):在这张图里找目标
# 输出:响应图(越亮表示越像目标)
class SiamFC(nn.Module):
    def __init__(self):
        super().__init__()
        # 主干特征提取网络(两个输入共享这一套权重!)
        self.backbone = nn.Sequential(
            nn.Conv2d(1, 16, 3, 1, 1), nn.ReLU(),  # 卷积+激活
            nn.Conv2d(16, 32, 3, 1, 1), nn.ReLU()   # 卷积+激活
        )

    # 前向传播:同时处理模板和搜索图
    def forward(self, template, search):
        ft = self.backbone(template)       # 提取目标模板特征
        fs = self.backbone(search)         # 提取搜索区域特征
        # 互相关操作:用目标特征在搜索图上“滑动匹配”
        return nn.functional.conv2d(fs, ft)

# ==================== 2. 加载MNIST数据集,随机取一个数字作为跟踪目标 ====================
transform = transforms.ToTensor()  # 图片转张量

# 加载MNIST
dataset = datasets.MNIST(root='./data', train=True, download=False, transform=transform)

# 随机取一张数字图作为“模板”
template_img, _ = dataset[np.random.randint(len(dataset))]

# 增加batch维度,变成 [1, 1, 28, 28]
template_img = template_img.unsqueeze(0)

# ==================== 3. 配置设备、模型、超参数 ====================
DEVICE = torch.device("cpu")  # 使用CPU运行
model = SiamFC().to(DEVICE)   # 实例化跟踪模型
template = template_img.to(DEVICE)  # 模板送到设备上

SEARCH_SIZE = 64  # 搜索区域(大图)大小 64x64
OBJ_SIZE = 28      # 数字目标大小 28x28

# ==================== 4. 生成“数字随机移动”的一帧图像 ====================
# 输入:上一帧的坐标 (prev_x, prev_y)
# 输出:新的一帧 + 新坐标
def create_moving_frame(prev_x, prev_y):
    # 随机生成小位移(-3,-2,-1,0,1,2,3)
    dx = np.random.randint(-3, 4)
    dy = np.random.randint(-3, 4)

    # 更新位置,并确保不会跑出画面(clip)
    x = np.clip(prev_x + dx, 0, SEARCH_SIZE - OBJ_SIZE)
    y = np.clip(prev_y + dy, 0, SEARCH_SIZE - OBJ_SIZE)

    # 创建全黑的64x64大图
    frame = torch.zeros(1, 1, SEARCH_SIZE, SEARCH_SIZE)

    # 将数字贴到新位置
    frame[:, :, y:y+OBJ_SIZE, x:x+OBJ_SIZE] = template_img

    return frame, x, y

# ==================== 5. 跟踪函数:输入搜索图,输出目标坐标 ====================
def track(search_img):
    # 不计算梯度,推理更快
    with torch.no_grad():
        response = model(template, search_img.to(DEVICE))  # 得到响应图

    # 把响应图转成numpy,找到最大值位置(最像目标的地方)
    res = response[0,0].cpu().numpy()
    y, x = np.unravel_index(np.argmax(res), res.shape)

    # 将响应图坐标映射回64x64大图的坐标
    scale = SEARCH_SIZE - OBJ_SIZE + 1
    x = int(x * (SEARCH_SIZE - OBJ_SIZE) / (scale - 1))
    y = int(y * (SEARCH_SIZE - OBJ_SIZE) / (scale - 1))

    return x, y

# ==================== 6. 动态显示:左边模板,右边跟踪画面(带绿色框) ====================
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))  # 1行2列图

# 初始化数字起始位置
x, y = 10, 10
frame, _, _ = create_moving_frame(x, y)

# 左图:显示要跟踪的目标(模板)
ax1.imshow(template_img[0,0], cmap="gray")
ax1.set_title("Template")  # 标题
ax1.axis("off")           # 不显示坐标轴

# 右图:显示跟踪画面(数字+跟踪框)
im = ax2.imshow(frame[0,0], cmap="gray", vmin=0, vmax=1)

# 创建绿色矩形框,表示跟踪到的目标
rect = plt.Rectangle((x, y), 28, 28, fill=False, edgecolor="green", linewidth=2)
ax2.add_patch(rect)

ax2.set_title("Tracking")
ax2.axis("off")

# ==================== 动画更新函数:每一帧都会调用 ====================
def update(frame_idx):
    global x, y  # 使用全局坐标
    frame, x, y = create_moving_frame(x, y)  # 生成移动后的新帧
    tx, ty = track(frame)                    # 用模型跟踪目标位置

    im.set_data(frame[0,0])        # 更新图像
    rect.set_xy((tx, ty))         # 更新绿色框位置
    return im, rect

# ==================== 启动动画 ====================
ani = animation.FuncAnimation(
    fig, update, frames=100, interval=80, blit=True
)

plt.tight_layout()
plt.show()

输出结果:

关键知识点:

这段代码不需要训练,是因为它仅在纯黑背景、单一白色数字的极简场景下,依靠随机初始化卷积核天然具备的边缘、灰度对比检测能力,就能通过孪生网络的互相关运算完成简单的模板匹配,目标与背景差异极大、无干扰、无遮挡、无形态变化,仅需简单的特征相似度计算即可准确定位,无需通过数据学习优化权重,因此可以直接实现跟踪效果。

互相关运算以目标模板特征为卷积核,在搜索区域特征图上进行滑动卷积计算,通过逐窗口匹配得到相似度响应图,响应图中的最大值坐标对应搜索图中与目标最相似的位置,从而实现目标定位,是孪生跟踪网络 SiamFC 的核心匹配机制。

五、总结

  • 目标分类:认图,只说 “这是什么”
  • 目标检测:画框,说清 “有什么、在哪里、有几个”
  • 目标分割:描边,像素级精准勾勒物体轮廓
  • 目标跟踪:追人,在视频里跨帧锁定同一个目标

 

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐