第11章:计算机视觉(Computer Vision)
一、导读
1.1 本章学习目标
第11章介绍计算机视觉——让计算机理解和处理图像信息的技术。视觉是人类感知世界的主要方式,让机器"看懂"图像是AI长期以来的挑战。近年来,深度学习方法在计算机视觉领域取得了突破性进展。
本章的核心学习目标包括:
- 理解图像的基本表示:像素、颜色空间、图像特征。
- 掌握图像分类:CNN 的基本原理和经典架构(LeNet、AlexNet、VGG、ResNet)。
- 理解目标检测:从传统方法到 YOLO、Faster R-CNN。
- 掌握图像分割:语义分割、实例分割、全景分割。
- 了解3D视觉:深度估计、点云处理、NeRF。
- 了解视觉大模型:CLIP、DALL-E、Stable Diffusion 等生成模型。
1.2 为什么第十一章如此重要
计算机视觉是AI最成功的应用领域之一。从人脸识别到自动驾驶,从医学影像分析到工业检测,视觉技术已经深入到各行各业。特别是深度学习的突破,使得计算机在许多视觉任务上已经达到甚至超过人类水平。
理解计算机视觉的基本原理,不仅有助于开发视觉应用,也有助于理解现代AI系统如何处理视觉信息。本章将从基础的图像处理开始,逐步深入到深度学习方法。
1.3 核心问题引导
在阅读本章时,请带着以下问题思考:
图像的表示:一张数字图像在计算机中是如何表示的?颜色空间有什么影响?
CNN 为什么适合图像? 卷积操作有什么特殊性质使得它特别适合处理图像?
目标检测的挑战:图像中可能有多个不同大小的物体,如何同时检测它们?
语义分割 vs 实例分割:两者的区别是什么?各自适用于什么场景?
视觉生成模型:DALL-E、Stable Diffusion 是如何从文本生成图像的?
二、核心概念详解
2.1 图像基础
2.1.1 图像的数字化表示
import numpy as np
class Image:
def __init__(self, height, width, channels=3):
self.height = height
self.width = width
self.channels = channels
self.data = np.zeros((height, width, channels), dtype=np.uint8)
def get_pixel(self, x, y):
return self.data[y, x]
def set_pixel(self, x, y, value):
self.data[y, x] = value
def to_grayscale(self):
gray = np.dot(self.data[..., :3], [0.2989, 0.5870, 0.1140])
return gray.astype(np.uint8)
def resize(self, new_height, new_width):
resized = np.zeros((new_height, new_width, self.channels), dtype=np.uint8)
y_ratio = self.height / new_height
x_ratio = self.width / new_width
for i in range(new_height):
for j in range(new_width):
src_y = int(i * y_ratio)
src_x = int(j * x_ratio)
resized[i, j] = self.data[src_y, src_x]
return resized
# 创建测试图像
img = Image(100, 100)
img.data[:50, :50] = 255
print(f"图像形状: {img.data.shape}")
gray = img.to_grayscale()
print(f"灰度图像形状: {gray.shape}")2.1.2 颜色空间
class ColorSpace:
@staticmethod
def rgb_to_gray(rgb):
return np.dot(rgb[..., :3], [0.2989, 0.5870, 0.1140])
@staticmethod
def rgb_to_hsv(rgb):
rgb = rgb.astype(float) / 255.0
r, g, b = rgb[..., 0], rgb[..., 1], rgb[..., 2]
max_val = np.max(rgb, axis=-1)
min_val = np.min(rgb, axis=-1)
diff = max_val - min_val
h = np.zeros_like(max_val)
mask = diff > 0
h[mask & (max_val == r)] = (60 * ((g[mask & (max_val == r)] - b[mask & (max_val == r)]) / diff[mask & (max_val == r)]) + 360) % 360
h[mask & (max_val == g)] = (60 * ((b[mask & (max_val == g)] - r[mask & (max_val == g)]) / diff[mask & (max_val == g)]) + 120)
h[mask & (max_val == b)] = (60 * ((r[mask & (max_val == b)] - g[mask & (max_val == b)]) / diff[mask & (max_val == b)]) + 240)
s = np.where(max_val > 0, diff / max_val, 0)
v = max_val
return np.stack([h, s, v], axis=-1)
# 测试颜色转换
rgb = np.array([[[255, 0, 0], [0, 255, 0], [0, 0, 255]]])
gray = ColorSpace.rgb_to_gray(rgb)
print(f"灰度值: {gray}")2.1.3 图像滤波
class ImageFilter:
@staticmethod
def convolve(image, kernel):
kh, kw = kernel.shape
ph, pw = kh // 2, kw // 2
padded = np.pad(image, ((ph, ph), (pw, pw)), mode='constant')
output = np.zeros_like(image)
for i in range(image.shape[0]):
for j in range(image.shape[1]):
region = padded[i:i+kh, j:j+kw]
output[i, j] = np.sum(region * kernel)
return output
@staticmethod
def gaussian_kernel(size=3, sigma=1.0):
kernel = np.zeros((size, size))
center = size // 2
for i in range(size):
for j in range(size):
x, y = i - center, j - center
kernel[i, j] = np.exp(-(x**2 + y**2) / (2 * sigma**2))
return kernel / np.sum(kernel)
@staticmethod
def edge_detection(image):
sobel_x = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]])
sobel_y = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]])
gx = ImageFilter.convolve(image, sobel_x)
gy = ImageFilter.convolve(image, sobel_y)
magnitude = np.sqrt(gx**2 + gy**2)
return magnitude
# 测试滤波
image = np.random.randint(0, 256, (10, 10))
gaussian = ImageFilter.gaussian_kernel(3, 1.0)
smoothed = ImageFilter.convolve(image, gaussian)
edges = ImageFilter.edge_detection(image)
print(f"边缘检测完成")2.2 卷积神经网络
2.2.1 卷积层
class ConvLayer:
def __init__(self, in_channels, out_channels, kernel_size, stride=1, padding=0):
self.stride = stride
self.padding = padding
self.kernel_size = kernel_size
self.weights = np.random.randn(out_channels, in_channels, kernel_size, kernel_size) * 0.01
self.bias = np.zeros(out_channels)
def forward(self, X):
batch, c, h, w = X.shape
if self.padding > 0:
X = np.pad(X, ((0,0), (0,0), (self.padding, self.padding), (self.padding, self.padding)))
out_h = (h + 2 * self.padding - self.kernel_size) // self.stride + 1
out_w = (w + 2 * self.padding - self.kernel_size) // self.stride + 1
output = np.zeros((batch, len(self.weights), out_h, out_w))
for i in range(out_h):
for j in range(out_w):
h_start = i * self.stride
h_end = h_start + self.kernel_size
w_start = j * self.stride
w_end = w_start + self.kernel_size
patch = X[:, :, h_start:h_end, w_start:w_end]
for k in range(len(self.weights)):
output[:, k, i, j] = np.sum(patch * self.weights[k], axis=(1,2,3)) + self.bias[k]
return output
# 测试卷积层
conv = ConvLayer(3, 16, 3, padding=1)
X = np.random.randn(1, 3, 32, 32)
output = conv.forward(X)
print(f"卷积输出形状: {output.shape}")2.2.2 池化层
class PoolLayer:
def __init__(self, pool_size=2, stride=2, mode='max'):
self.pool_size = pool_size
self.stride = stride
self.mode = mode
def forward(self, X):
batch, c, h, w = X.shape
out_h = (h - self.pool_size) // self.stride + 1
out_w = (w - self.pool_size) // self.stride + 1
output = np.zeros((batch, c, out_h, out_w))
for i in range(out_h):
for j in range(out_w):
h_start = i * self.stride
h_end = h_start + self.pool_size
w_start = j * self.stride
w_end = w_start + self.pool_size
patch = X[:, :, h_start:h_end, w_start:w_end]
if self.mode == 'max':
output[:, :, i, j] = np.max(patch, axis=(2, 3))
elif self.mode == 'avg':
output[:, :, i, j] = np.mean(patch, axis=(2, 3))
return output
# 测试池化层
pool = PoolLayer(pool_size=2, stride=2, mode='max')
X = np.random.randn(1, 16, 32, 32)
output = pool.forward(X)
print(f"池化输出形状: {output.shape}")2.2.3 完整的 CNN
class SimpleCNN:
def __init__(self, num_classes=10):
self.conv1 = ConvLayer(1, 8, 3, padding=1)
self.pool1 = PoolLayer(2, 2)
self.conv2 = ConvLayer(8, 16, 3, padding=1)
self.pool2 = PoolLayer(2, 2)
self.num_classes = num_classes
def forward(self, X):
x = self.conv1.forward(X)
x = np.maximum(0, x)
x = self.pool1.forward(x)
x = self.conv2.forward(x)
x = np.maximum(0, x)
x = self.pool2.forward(x)
return x
# 测试 CNN
cnn = SimpleCNN(num_classes=10)
X = np.random.randn(4, 1, 28, 28)
features = cnn.forward(X)
print(f"CNN 特征形状: {features.shape}")2.3 目标检测
2.3.1 滑动窗口检测
class SlidingWindowDetector:
def __init__(self, window_sizes, scales):
self.window_sizes = window_sizes
self.scales = scales
def generate_proposals(self, image_shape):
h, w = image_shape[:2]
proposals = []
for size in self.window_sizes:
for scale in self.scales:
scaled_size = int(size * scale)
for y in range(0, h - scaled_size, scaled_size // 2):
for x in range(0, w - scaled_size, scaled_size // 2):
proposals.append((x, y, scaled_size, scaled_size))
return proposals
# 生成候选框
detector = SlidingWindowDetector([32, 64], [1.0, 1.5])
proposals = detector.generate_proposals((100, 100))
print(f"候选框数量: {len(proposals)}")2.3.2 IoU 计算
def compute_iou(box1, box2):
x1, y1, w1, h1 = box1
x2, y2, w2, h2 = box2
xi1 = max(x1, x2)
yi1 = max(y1, y2)
xi2 = min(x1 + w1, x2 + w2)
yi2 = min(y1 + h1, y2 + h2)
inter_area = max(0, xi2 - xi1) * max(0, yi2 - yi1)
box1_area = w1 * h1
box2_area = w2 * h2
union_area = box1_area + box2_area - inter_area
return inter_area / union_area if union_area > 0 else 0
# 测试 IoU
box1 = (10, 10, 50, 50)
box2 = (30, 30, 50, 50)
iou = compute_iou(box1, box2)
print(f"IoU: {iou:.4f}")2.4 图像分割
2.4.1 语义分割
class SimpleSegmentation:
def __init__(self, num_classes):
self.num_classes = num_classes
def predict(self, features):
batch, c, h, w = features.shape
segmentation = np.zeros((batch, h, w), dtype=np.int32)
for b in range(batch):
for i in range(h):
for j in range(w):
pixel_features = features[b, :, i, j]
segmentation[b, i, j] = np.argmax(pixel_features[:self.num_classes])
return segmentation
# 测试分割
seg = SimpleSegmentation(num_classes=3)
features = np.random.randn(1, 3, 32, 32)
mask = seg.predict(features)
print(f"分割掩码形状: {mask.shape}")2.5 视觉生成模型
2.5.1 简单的图像生成
class SimpleGenerator:
def __init__(self, latent_dim=100, image_shape=(1, 28, 28)):
self.latent_dim = latent_dim
self.image_shape = image_shape
self.W1 = np.random.randn(latent_dim, 256) * 0.01
self.W2 = np.random.randn(256, 512) * 0.01
self.W3 = np.random.randn(512, np.prod(image_shape)) * 0.01
def generate(self, z):
h1 = np.maximum(0, z @ self.W1)
h2 = np.maximum(0, h1 @ self.W2)
output = h2 @ self.W3
output = output.reshape(-1, *self.image_shape)
output = 1 / (1 + np.exp(-output))
return output
# 测试生成器
gen = SimpleGenerator(latent_dim=100, image_shape=(1, 28, 28))
z = np.random.randn(4, 100)
images = gen.generate(z)
print(f"生成图像形状: {images.shape}")三、实战案例
3.1 案例一:简单的图像分类流程
class ImageClassificationPipeline:
def __init__(self):
self.cnn = SimpleCNN(num_classes=10)
def preprocess(self, image):
if len(image.shape) == 2:
image = image[np.newaxis, np.newaxis, ...]
elif len(image.shape) == 3:
image = image[np.newaxis, ...]
return image / 255.0
def predict(self, image):
processed = self.preprocess(image)
features = self.cnn.forward(processed)
return features
def evaluate(self, images, labels):
correct = 0
for img, label in zip(images, labels):
features = self.predict(img)
pred = np.argmax(features)
if pred == label:
correct += 1
return correct / len(labels)
# 测试流程
pipeline = ImageClassificationPipeline()
test_images = [np.random.randint(0, 256, (28, 28)) for _ in range(5)]
test_labels = [0, 1, 2, 3, 4]
accuracy = pipeline.evaluate(test_images, test_labels)
print(f"准确率: {accuracy:.2f}")四、本章小结
本章介绍了计算机视觉的核心概念和方法。从基础的图像表示和处理,到卷积神经网络的原理,再到目标检测和图像分割,我们逐步构建了计算机视觉的知识体系。
核心知识点回顾:
- 图像是像素的二维数组,颜色空间影响处理效果
- CNN 通过卷积和池化提取图像特征
- 目标检测需要定位和分类多个物体
- 图像分割为每个像素分配类别标签
- 视觉生成模型可以从噪声或文本生成图像
五、思考与练习
思考题
CNN 的平移不变性:为什么 CNN 对图像的平移具有不变性?这种性质有什么优势?
目标检测的挑战:小目标检测为什么困难?如何改进?
编程练习
实现简单的 CNN:用 NumPy 实现一个完整的 CNN 进行 MNIST 分类。
实现边缘检测:用 Sobel 算子实现边缘检测,比较不同阈值的效果。
实现图像增强:实现旋转、翻转、裁剪等数据增强方法。