从行人检测到边缘计算:用Python手把手复现HOG+SVM经典算法(附完整代码)

在计算机视觉领域,深度学习虽然占据了当前研究的主流,但传统算法依然有其独特的价值和魅力。HOG(方向梯度直方图)结合SVM(支持向量机)的算法组合,曾经是行人检测领域的黄金标准,即使在今天,它仍然在许多资源受限的场景下展现出惊人的效率。本文将带您从零开始,用Python完整实现这一经典算法,并探讨其在现代边缘计算环境中的应用潜力。

1. HOG算法核心原理与实现准备

HOG特征描述子的核心在于捕捉图像中物体的边缘和形状信息。与深度学习的"黑箱"特性不同,HOG每一步计算都有明确的物理意义和可视化可能,这使得它特别适合教学和理解计算机视觉的基础概念。

1.1 环境配置与依赖安装

在开始编码前,我们需要准备以下Python环境:

pip install opencv-python numpy matplotlib scikit-learn

关键库的作用:

  • OpenCV :图像处理和HOG计算基础操作
  • NumPy :高效的数值计算
  • Matplotlib :结果可视化
  • scikit-learn :SVM分类器实现

提示:建议使用Python 3.8或以上版本,以获得最佳的库兼容性

1.2 基础图像处理流程

HOG特征提取的第一步是读取并预处理图像。虽然原始论文提到可以跳过Gamma校正,但在实际应用中,适当的预处理能提升特征质量:

import cv2
import numpy as np

def load_image(path):
    # 读取图像并转换为灰度
    img = cv2.imread(path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    
    # 可选:直方图均衡化
    gray = cv2.equalizeHist(gray)
    
    return gray.astype(np.float32) / 255.0  # 归一化到[0,1]

2. 梯度计算与方向直方图构建

2.1 像素级梯度计算

梯度是HOG特征的基石,它反映了图像中边缘的强度和方向。我们可以通过Sobel算子或直接差分法计算:

def compute_gradients(image):
    # 使用Sobel算子计算梯度
    grad_x = cv2.Sobel(image, cv2.CV_32F, 1, 0, ksize=3)
    grad_y = cv2.Sobel(image, cv2.CV_32F, 0, 1, ksize=3)
    
    # 计算梯度幅值和方向
    magnitude = np.sqrt(grad_x**2 + grad_y**2)
    angle = np.arctan2(grad_y, grad_x) * (180 / np.pi)  # 转换为角度
    
    # 将角度转换到0-180度范围
    angle[angle < 0] += 180
    
    return magnitude, angle

2.2 Cell级别的方向直方图

将图像划分为8x8像素的cell后,我们需要统计每个cell内的梯度方向分布:

def compute_cell_histogram(magnitude, angle, cell_size=8, bins=9):
    height, width = magnitude.shape
    hist = np.zeros((height // cell_size, width // cell_size, bins))
    
    for i in range(0, height, cell_size):
        for j in range(0, width, cell_size):
            cell_mag = magnitude[i:i+cell_size, j:j+cell_size]
            cell_angle = angle[i:i+cell_size, j:j+cell_size]
            
            # 统计直方图
            for x in range(cell_size):
                for y in range(cell_size):
                    bin_idx = int(cell_angle[x,y] / (180 / bins)) % bins
                    hist[i//cell_size, j//cell_size, bin_idx] += cell_mag[x,y]
    
    return hist

3. Block归一化与特征向量构建

3.1 局部对比度归一化

为了增强特征对光照变化的鲁棒性,我们需要对相邻cell组成的block进行归一化:

def normalize_blocks(hist, block_size=2, method="L2-Hys"):
    h, w, bins = hist.shape
    normalized = np.zeros((h - block_size + 1, w - block_size + 1, block_size * block_size * bins))
    
    for i in range(h - block_size + 1):
        for j in range(w - block_size + 1):
            block = hist[i:i+block_size, j:j+block_size, :].flatten()
            
            if method == "L2":
                norm = np.sqrt(np.sum(block**2) + 1e-5)
                normalized[i,j] = block / norm
            elif method == "L2-Hys":
                norm = np.sqrt(np.sum(block**2) + 1e-5)
                block = block / norm
                block = np.clip(block, 0, 0.2)
                norm = np.sqrt(np.sum(block**2) + 1e-5)
                normalized[i,j] = block / norm
    
    return normalized

3.2 完整HOG特征提取

将上述步骤整合,构建完整的HOG特征提取流程:

def extract_hog_features(image, cell_size=8, block_size=2, bins=9):
    # 1. 计算梯度
    mag, angle = compute_gradients(image)
    
    # 2. 计算cell直方图
    hist = compute_cell_histogram(mag, angle, cell_size, bins)
    
    # 3. Block归一化
    hog_features = normalize_blocks(hist, block_size)
    
    # 4. 展平为特征向量
    return hog_features.flatten()

4. SVM分类器训练与应用

4.1 准备训练数据

我们需要正样本(包含行人)和负样本(不包含行人)来训练分类器:

from sklearn.svm import SVC
from sklearn.model_selection import train_test_split

def prepare_dataset(pos_paths, neg_paths):
    X = []
    y = []
    
    for path in pos_paths:
        img = load_image(path)
        features = extract_hog_features(img)
        X.append(features)
        y.append(1)  # 正样本标签为1
    
    for path in neg_paths:
        img = load_image(path)
        features = extract_hog_features(img)
        X.append(features)
        y.append(0)  # 负样本标签为0
    
    return np.array(X), np.array(y)

# 示例使用
X, y = prepare_dataset(["ped1.jpg", "ped2.jpg"], ["bg1.jpg", "bg2.jpg"])
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

4.2 训练SVM分类器

选择合适的核函数和参数对分类性能至关重要:

def train_svm(X_train, y_train):
    svm = SVC(kernel='linear', C=1.0, probability=True)
    svm.fit(X_train, y_train)
    return svm

# 训练模型
model = train_svm(X_train, y_train)

# 评估模型
accuracy = model.score(X_test, y_test)
print(f"Test accuracy: {accuracy:.2f}")

5. 边缘计算场景下的优化实践

5.1 计算效率优化

在资源受限的边缘设备上,我们可以通过以下方式优化HOG计算:

  • 图像金字塔 :多尺度检测
  • 滑动窗口优化 :减少冗余计算
  • 特征降维 :PCA或特征选择
def optimized_hog_detection(image, model, scale_factor=1.1, window_step=8):
    # 创建图像金字塔
    pyramid = []
    current_scale = 1.0
    while True:
        new_width = int(image.shape[1] / current_scale)
        new_height = int(image.shape[0] / current_scale)
        if new_width < 64 or new_height < 128:  # 最小检测窗口
            break
        resized = cv2.resize(image, (new_width, new_height))
        pyramid.append((resized, current_scale))
        current_scale *= scale_factor
    
    # 滑动窗口检测
    detections = []
    for img, scale in pyramid:
        for y in range(0, img.shape[0] - 128, window_step):
            for x in range(0, img.shape[1] - 64, window_step):
                window = img[y:y+128, x:x+64]
                features = extract_hog_features(window)
                prob = model.predict_proba([features])[0][1]
                if prob > 0.7:  # 置信度阈值
                    detections.append((
                        int(x * scale), 
                        int(y * scale),
                        int(64 * scale),
                        int(128 * scale),
                        prob
                    ))
    
    return detections

5.2 实际部署考量

在边缘设备部署时,还需要考虑:

  • 内存占用 :HOG特征向量通常较大(3780维)
  • 实时性要求 :帧率与精度的权衡
  • 硬件加速 :使用OpenCV的优化实现或专用硬件
# OpenCV的优化HOG实现
def opencv_hog_detection():
    hog = cv2.HOGDescriptor()
    hog.setSVMDetector(cv2.HOGDescriptor_getDefaultPeopleDetector())
    
    # 在视频流中检测
    cap = cv2.VideoCapture(0)
    while True:
        ret, frame = cap.read()
        if not ret:
            break
            
        # 转换为灰度
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        
        # 检测行人
        boxes, weights = hog.detectMultiScale(gray, winStride=(8,8), padding=(16,16), scale=1.05)
        
        # 绘制结果
        for (x,y,w,h), weight in zip(boxes, weights):
            cv2.rectangle(frame, (x,y), (x+w,y+h), (0,255,0), 2)
        
        cv2.imshow('Detection', frame)
        if cv2.waitKey(1) == 27:  # ESC退出
            break
    
    cap.release()
    cv2.destroyAllWindows()

6. 完整代码实现与可视化

将所有组件整合为一个完整的行人检测系统:

import cv2
import numpy as np
import matplotlib.pyplot as plt
from sklearn.svm import SVC

class HOGPedestrianDetector:
    def __init__(self, cell_size=8, block_size=2, bins=9):
        self.cell_size = cell_size
        self.block_size = block_size
        self.bins = bins
        self.model = None
    
    def compute_gradients(self, image):
        grad_x = cv2.Sobel(image, cv2.CV_32F, 1, 0, ksize=3)
        grad_y = cv2.Sobel(image, cv2.CV_32F, 0, 1, ksize=3)
        magnitude = np.sqrt(grad_x**2 + grad_y**2)
        angle = np.arctan2(grad_y, grad_x) * (180 / np.pi)
        angle[angle < 0] += 180
        return magnitude, angle
    
    def compute_cell_histogram(self, magnitude, angle):
        height, width = magnitude.shape
        hist = np.zeros((height // self.cell_size, width // self.cell_size, self.bins))
        
        for i in range(0, height, self.cell_size):
            for j in range(0, width, self.cell_size):
                cell_mag = magnitude[i:i+self.cell_size, j:j+self.cell_size]
                cell_angle = angle[i:i+self.cell_size, j:j+self.cell_size]
                
                for x in range(self.cell_size):
                    for y in range(self.cell_size):
                        bin_idx = int(cell_angle[x,y] / (180 / self.bins)) % self.bins
                        hist[i//self.cell_size, j//self.cell_size, bin_idx] += cell_mag[x,y]
        
        return hist
    
    def normalize_blocks(self, hist):
        h, w, bins = hist.shape
        normalized = np.zeros((h - self.block_size + 1, w - self.block_size + 1, 
                              self.block_size * self.block_size * bins))
        
        for i in range(h - self.block_size + 1):
            for j in range(w - self.block_size + 1):
                block = hist[i:i+self.block_size, j:j+self.block_size, :].flatten()
                norm = np.sqrt(np.sum(block**2) + 1e-5)
                block = block / norm
                block = np.clip(block, 0, 0.2)
                norm = np.sqrt(np.sum(block**2) + 1e-5)
                normalized[i,j] = block / norm
        
        return normalized
    
    def extract_features(self, image):
        mag, angle = self.compute_gradients(image)
        hist = self.compute_cell_histogram(mag, angle)
        hog_features = self.normalize_blocks(hist)
        return hog_features.flatten()
    
    def train(self, X_train, y_train):
        self.model = SVC(kernel='linear', C=1.0, probability=True)
        self.model.fit(X_train, y_train)
    
    def detect(self, image, threshold=0.7):
        if len(image.shape) == 3:
            image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
        image = image.astype(np.float32) / 255.0
        
        # 多尺度检测
        detections = []
        for scale in np.linspace(0.5, 1.5, 5):
            resized = cv2.resize(image, None, fx=scale, fy=scale)
            if resized.shape[0] < 128 or resized.shape[1] < 64:
                continue
                
            for y in range(0, resized.shape[0] - 128, 16):
                for x in range(0, resized.shape[1] - 64, 16):
                    window = resized[y:y+128, x:x+64]
                    features = self.extract_features(window)
                    prob = self.model.predict_proba([features])[0][1]
                    if prob > threshold:
                        detections.append((
                            int(x / scale), 
                            int(y / scale),
                            int(64 / scale),
                            int(128 / scale),
                            prob
                        ))
        
        return detections

# 可视化HOG特征
def visualize_hog(image, cell_size=8, bins=9):
    mag, angle = compute_gradients(image)
    hist = compute_cell_histogram(mag, angle, cell_size, bins)
    
    plt.figure(figsize=(12,6))
    plt.subplot(121)
    plt.imshow(image, cmap='gray')
    plt.title('Original Image')
    
    plt.subplot(122)
    hog_image = np.zeros_like(image)
    for i in range(hist.shape[0]):
        for j in range(hist.shape[1]):
            for k, val in enumerate(hist[i,j]):
                start_angle = k * (180 / bins)
                end_angle = (k+1) * (180 / bins)
                center_x = j * cell_size + cell_size // 2
                center_y = i * cell_size + cell_size // 2
                radius = int(val * cell_size * 0.5)
                
                # 绘制方向线段
                angle_rad = np.radians((start_angle + end_angle) / 2)
                x2 = center_x + radius * np.cos(angle_rad)
                y2 = center_y + radius * np.sin(angle_rad)
                cv2.line(hog_image, 
                        (center_x, center_y), 
                        (int(x2), int(y2)), 
                        255, 1)
    
    plt.imshow(hog_image, cmap='gray')
    plt.title('HOG Visualization')
    plt.show()

更多推荐