WikiWiki
首页
Java开发
Java面试
Linux手册
  • AI相关
  • Python Flask
  • Pytorch
  • youlo8
SEO
uniapp小程序
Vue前端
work
数据库
软件设计师
入门指南
首页
Java开发
Java面试
Linux手册
  • AI相关
  • Python Flask
  • Pytorch
  • youlo8
SEO
uniapp小程序
Vue前端
work
数据库
软件设计师
入门指南

计算机视觉--YOLO+Transfomer多场景目标检测实战*

第1章 计算机视觉--YOLO+Transfomer多场景目标检测--课程导学

image-20250327133302227

**第2章 深度学习环境的搭建 - **

2-1 深度学习开发环境搭建-导学

申请免费的云主机资源

  • 申请阿里的GPU和CPU资源
  • 申请Kaggle的GPU与CPU资源
  • 申请Goole的GPU与CPU资源

GPU的选择、主板的选择、内存/CPU、硬盘的选择、电源的选择

  • Navidia驱动/CUDA
  • pip/Conda/apt/brew
  • Jupyter Notebook
  • VsCode
  • Pycharm

通过Docker打造深度学习开发环境

  • 使用Docker的好处
    • 简单方便,开箱即用
    • 移植性好
  • 使用Docker的坏处
    • 利用更多的空间
    • 性能略差

2-2 申请阿里云的免费GPU和CPU资源

阿里云免费试用 - 阿里云

2-3 申请Kaggle的免费GPU和CPU资源

kaggle(白嫖免费GPU,新手必看!!!)_kaggle免费gpu服务器-CSDN博客

2-4 申请Google的免费GPU和CPU资源

https://colab.research.google.com/

2-5 打造自己的深度学习开发环境-硬件部分

为什么要使用GPU?

image-20250327140245318

硬件配置

image-20250327140432803

image-20250327142514093

2-6 打造自己的深度学习开发环境-Linux

image-20250327142843025

  • 安装操作系统
  • 安装Nvidia Driver
  • MiniConda
  • 创建Python隔离环境
  • 安装Jupyter Lab

Ubuntu下安装Nvidia驱动

  • 卸载Unbuntu下的默认显卡驱动

    编辑黑名单配置文件

    打开 /etc/modprobe.d/blacklist.conf 文件,添加以下两行内容:

    blacklist nouveau
    options nouveau modeset=0
    

    更新initramfs

    执行以下命令使更改生效:

    sudo update-initramfs -u
    

    重启系统

    更改完成后需要重启系统:

    sudo reboot
    
  • 安装Nvidia显卡驱动

更新系统并查找可用驱动版本

sudo apt update
apt search nvidia-driver  # 查看可用的驱动版本

输出示例(选择最新的稳定版或推荐版本):

nvidia-driver-535-server/bionic-updates 535.161.07-0ubuntu0.22.04.1
nvidia-driver-535/bionic-updates 535.161.07-0ubuntu0.22.04.1
nvidia-driver-550/bionic-updates 550.67-0ubuntu0.22.04.1
...

安装驱动(以 nvidia-driver-535 为例)

sudo apt install nvidia-driver-535  # 替换为你的目标版本
sudo reboot  # 安装完成后重启

验证安装

nvidia-smi  # 查看 GPU 状态

正常输出示例:

+-----------------------------------------------------------------------------+
| NVIDIA-SMI 535.161.07   Driver Version: 535.161.07   CUDA Version: 12.2     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  NVIDIA GeForce ...  Off  | 00000000:01:00.0  On |                  N/A |
| 30%   45C    P8    10W / 120W |    500MiB /  8192MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+

2-7 打造自己的深度学习开发环境-Windows

下载 NVIDIA 官方驱动 | NVIDIA

image-20250327144050978

默认安装即可。

nvidia-smi 

Driver与CUDA兼容性问题

  • 新Driver一般会兼容老版本CUDA
  • 但Driver与CUDA的版本不应跨距太大
  • 驱动与CUDA对应表

1. CUDA 12.8 Update 1 Release Notes — Release Notes 12.8 documentation

2-8 打造自己的深度学习开发环境miniconda与jupyter

miniconda:Index of /anaconda/miniconda/ | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror

https://mirrors.tuna.tsinghua.edu.cn/anaconda/miniconda/Miniconda3-3.3.0-Windows-x86_64.exe

详细教程参考 Anaconda 与 jupyter 安装教程

2-9 打造自己的深度学习开发环境-Docker

  • 安装WSL2
  • 安装Docker Desktop
  • 拉取镜像
  • 启动Docker

docker的运行架构

image-20250327145905469

Windows 10/11 安装 WSL2

1. 启用 WSL 相关功能

以 管理员身份 运行 PowerShell 或 CMD,执行以下命令:

(1)启用「Linux 子系统」功能

dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart

(2)启用「虚拟机平台」功能(WSL2 必需)

dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart

(3)重启计算机

shutdown /r /t 0

2. 设置 WSL2 为默认版本

重启后,以管理员身份运行:

wsl --install
wsl --set-default-version 2

如果提示 WSL 2 requires an update to its kernel component,需下载并安装 WSL2 内核更新包。


3. 安装 Linux 发行版

(1)从 Microsoft Store 安装

  • 打开 Microsoft Store,搜索并安装喜欢的发行版(如 Ubuntu 22.04)。

(2)通过命令行安装(可选)

wsl --install -d Ubuntu-22.04  # 自动安装默认发行版(Ubuntu)

4. 验证安装

wsl -l -v  # 查看已安装的发行版及 WSL 版本

正常输出示例:

 NAME            STATE           VERSION
* Ubuntu-22.04    Stopped         2

安装Docker

从docker.com下载Docker Desktop

默认安装就好,注意:安装时,选择WSL2作用默认运行环境!

image-20250327150528769

拉取Tensorflow镜像

docker run \
  --name my_tensorflow_container \  # 容器名称
  --gpus all \                     # 启用所有GPU
  -it \                            # 交互式终端
  -p 8888:8888 \                   # 端口映射(主机:容器)
  tensorflow/tensorflow:latest-gpu-jupyter  # 镜像名称

关键参数说明

参数作用
--name指定容器名称(方便后续管理)
--gpus all允许容器使用所有可用GPU(需已安装NVIDIA驱动和Docker GPU支持)
-it以交互模式运行容器(可输入命令)
-p 8888:8888将容器的8888端口(Jupyter Notebook默认端口)映射到主机的8888端口

使用步骤

  1. 启动容器 执行上述命令后,容器会自动启动并输出 Jupyter Notebook 的访问链接(含 token),例如:

    plaintext

    复制

    http://127.0.0.1:8888/?token=abcdef123456...
    
  2. 访问 Jupyter Notebook 在浏览器中打开输出的链接即可使用 TensorFlow GPU 环境。

  3. 停止/删除容器

    bash

    复制

    # 停止容器
    docker stop my_tensorflow_container
    # 删除容器(需先停止)
    docker rm my_tensorflow_container
    

2-11 使用VSCode作为深度学习开发编辑器

  • 安装VSCode

    • 安装Docker扩展包

    • 安装远程连接扩展包

      image-20250327152024049

    • 安装jupyter扩展包

      image-20250327152140337

第3章 AI工具

3-8 大语言模型之提示词(一)

基本原则 每个人都会用提示词,只要把大模型当做人并与之交流保持会话的单一性

  • 任务指导型
  • 优化信息检索型
  • 示例驱动型
  • 角色扮演型
  • 思考过程型
  • 自我修正型
  • 多模态交互型
  • 任务拆分型
  • 交互对话型

第5章 经典计算机视觉核心技术与算法 - 重温经典,扎实AI根基

5-1 经典计算机视觉核心技术与算法

  • 传统计算机视觉
  • 基于深度学习的计算机视觉

计算机视觉的定义与任务 传统计算机视觉解决问题的步骤与方法 OpenCV环境的搭建

特征点及边缘检测 形态学原理与实战

计算机视觉的定义

  • 让计算机能够从图像和视频中理解、分析和解释里边的内容

5-2 视觉的基本处理流程

传统计算机视觉方法

对图像、视频进行预处理,增强对比度,灰度化,变形等

特征提取,边缘、角点、纹理等

分割,通过阈值进行分割,分别处理

形态学处理,通过膨胀、腐蚀改变图像结构,清除噪点

目标检测与识别,给合特征描述子和分类器来实现

传统计算机视觉擅长的事儿

  • 采集图像 和视频
  • 对图像、视频进行预处理
    • 灰度化、背景分离、去噪...
    • 图像的旋转、缩放、裁剪...
  • 从图像、视频中提取特征
    • 角点检测、边缘检测...
    • 获取轮廓...

传统计算机视觉的缺点

  • 受光照、角度、纹理影响大
  • 鲁棒性差
  • 难以处理复杂的视觉任务

5-3 OpenCV环境的搭建

在Python环境下安装OpenCV

pip install opencv-python
引入OpenCV库:import cv2
调用OpenCV API

使用OpenCV打开图片并显示

import cv2
image = cv2.imread("screensho123t.jpg")
cv2.imshow("image",image)
cv2.waitKey(0)
if cv2.waitKey(1) & 0xFF == ord("q"):
    cv2.destroyAllWindows()

5-4 通过OpenCV采集图像与视频

import cv2
import time
import os

def capture_image_from_camera():
    """
    从摄像头捕获单张图像并保存
    """
    # 创建保存图像的目录
    if not os.path.exists('captured_images'):
        os.makedirs('captured_images')
    
    # 初始化摄像头 (0 表示默认摄像头)
    cap = cv2.VideoCapture(0)
    
    if not cap.isOpened():
        print("无法打开摄像头")
        return
    
    print("按 's' 键保存图像,按 'q' 键退出")
    
    while True:
        # 读取一帧
        ret, frame = cap.read()
        
        if not ret:
            print("无法获取图像帧")
            break
        
        # 显示图像
        cv2.imshow('Camera - Press s to save, q to quit', frame)
        
        # 等待键盘输入
        key = cv2.waitKey(1) & 0xFF
        
        if key == ord('s'):  # 保存图像
            timestamp = time.strftime("%Y%m%d_%H%M%S")
            filename = f"captured_images/image_{timestamp}.jpg"
            cv2.imwrite(filename, frame)
            print(f"图像已保存为 {filename}")
        
        elif key == ord('q'):  # 退出
            break
    
    # 释放资源
    cap.release()
    cv2.destroyAllWindows()

def record_video_from_camera():
    """
    从摄像头录制视频并保存
    """
    # 创建保存视频的目录
    if not os.path.exists('recorded_videos'):
        os.makedirs('recorded_videos')
    
    # 初始化摄像头
    cap = cv2.VideoCapture(0)
    
    if not cap.isOpened():
        print("无法打开摄像头")
        return
    
    # 获取摄像头分辨率
    frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
    frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
    fps = 30  # 帧率
    
    # 定义视频编码器和创建 VideoWriter 对象
    timestamp = time.strftime("%Y%m%d_%H%M%S")
    video_filename = f"recorded_videos/video_{timestamp}.mp4"
    fourcc = cv2.VideoWriter_fourcc(*'mp4v')  # MP4 编码器
    out = cv2.VideoWriter(video_filename, fourcc, fps, (frame_width, frame_height))
    
    print(f"开始录制视频,按 'q' 键停止并保存到 {video_filename}")
    print("录制中...")
    
    recording = True
    while recording:
        ret, frame = cap.read()
        
        if not ret:
            print("无法获取图像帧")
            break
        
        # 写入视频文件
        out.write(frame)
        
        # 显示录制画面
        cv2.imshow('Recording - Press q to stop', frame)
        
        # 检查是否按下 q 键
        if cv2.waitKey(1) & 0xFF == ord('q'):
            recording = False
    
    print("视频录制完成")
    
    # 释放资源
    cap.release()
    out.release()
    cv2.destroyAllWindows()

def display_live_camera_feed():
    """
    显示实时摄像头画面
    """
    # 初始化摄像头
    cap = cv2.VideoCapture(0)
    
    if not cap.isOpened():
        print("无法打开摄像头")
        return
    
    print("显示实时摄像头画面,按 'q' 键退出")
    
    while True:
        ret, frame = cap.read()
        
        if not ret:
            print("无法获取图像帧")
            break
        
        # 显示图像
        cv2.imshow('Live Camera Feed - Press q to quit', frame)
        
        # 检查是否按下 q 键
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
    
    # 释放资源
    cap.release()
    cv2.destroyAllWindows()

def main():
    print("OpenCV 图像与视频采集 Demo")
    print("请选择操作:")
    print("1. 显示实时摄像头画面")
    print("2. 捕获单张图像")
    print("3. 录制视频")
    print("4. 退出")
    
    while True:
        choice = input("请输入选项 (1-4): ")
        
        if choice == '1':
            display_live_camera_feed()
        elif choice == '2':
            capture_image_from_camera()
        elif choice == '3':
            record_video_from_camera()
        elif choice == '4':
            print("程序退出")
            break
        else:
            print("无效输入,请重新选择")

if __name__ == "__main__":
    main()

5-5 二值化

二值化

  • 图像进行二值化的作用是简化图像处理的难度
  • 它可以将图像以某个阈值为分界线
  • 小于阈值的为0,大于阈值的为设定的值
threshold(src,阈值,MaxVal,类型)
- 状态
- dst
import cv2
import numpy as np

def image_binarization():
    """
    图像二值化演示
    """
    # 读取图像(转换为灰度图)
    img = cv2.imread('image.jpg', cv2.IMREAD_GRAYSCALE)
    
    if img is None:
        print("无法加载图像,请检查文件路径")
        return
    
    # 定义阈值和最大值
    threshold_value = 127
    max_val = 255
    
    # 1. 基本阈值处理(二进制阈值化)
    _, binary = cv2.threshold(img, threshold_value, max_val, cv2.THRESH_BINARY)
    
    # 2. 反二进制阈值化
    _, binary_inv = cv2.threshold(img, threshold_value, max_val, cv2.THRESH_BINARY_INV)
    
    # 3. 截断阈值化
    _, truncated = cv2.threshold(img, threshold_value, max_val, cv2.THRESH_TRUNC)
    
    # 4. 阈值化为0
    _, to_zero = cv2.threshold(img, threshold_value, max_val, cv2.THRESH_TOZERO)
    
    # 5. 反阈值化为0
    _, to_zero_inv = cv2.threshold(img, threshold_value, max_val, cv2.THRESH_TOZERO_INV)
    
    # 6. 大津算法自动确定阈值
    _, otsu = cv2.threshold(img, 0, max_val, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    
    # 显示所有结果
    cv2.imshow('Original', img)
    cv2.imshow('Binary', binary)
    cv2.imshow('Binary Inv', binary_inv)
    cv2.imshow('Truncated', truncated)
    cv2.imshow('To Zero', to_zero)
    cv2.imshow('To Zero Inv', to_zero_inv)
    cv2.imshow('Otsu', otsu)
    
    print("按任意键退出...")
    cv2.waitKey(0)
    cv2.destroyAllWindows()

def live_camera_binarization():
    """
    实时摄像头二值化演示
    """
    cap = cv2.VideoCapture(0)
    
    if not cap.isOpened():
        print("无法打开摄像头")
        return
    
    print("实时摄像头二值化演示")
    print("按键说明:")
    print("1-5: 切换不同阈值方法")

5-6 二值化背后的原理

RGB到Gray图的过程

image-20250328105211403

二值化

image-20250328105329786

5-7 Blur

Blur的作用是对图像做模糊处理比如美颜中对肤色的平滑处理在计算机视觉中可以消除噪音

import cv2
import numpy as np

def image_blur_demo():
    """
    图像模糊处理演示(静态图像)
    """
    # 读取图像
    img = cv2.imread('image.jpg')
    
    if img is None:
        print("无法加载图像,请检查文件路径")
        return
    
    # 1. 均值模糊 (Averaging)
    blur_avg = cv2.blur(img, (5, 5))
    
    # 2. 高斯模糊 (Gaussian Blur)
    blur_gaussian = cv2.GaussianBlur(img, (5, 5), 0)
    
    # 3. 中值模糊 (Median Blur) - 对椒盐噪声特别有效
    blur_median = cv2.medianBlur(img, 5)
    
    # 4. 双边滤波 (Bilateral Filter) - 能保留边缘
    blur_bilateral = cv2.bilateralFilter(img, 9, 75, 75)
    
    # 显示结果
    cv2.imshow('Original', img)
    cv2.imshow('Average Blur', blur_avg)
    cv2.imshow('Gaussian Blur', blur_gaussian)
    cv2.imshow('Median Blur', blur_median)
    cv2.imshow('Bilateral Filter', blur_bilateral)
    
    print("按任意键退出...")
    cv2.waitKey(0)
    cv2.destroyAllWindows()

def live_camera_blur_demo():
    """
    实时摄像头模糊处理演示
    """
    cap = cv2.VideoCapture(0)
    
    if not cap.isOpened():
        print("无法打开摄像头")
        return
    
    print("实时摄像头模糊处理演示")
    print("按键说明:")
    print("1: 均值模糊")
    print("2: 高斯模糊")
    print("3: 中值模糊")
    print("4: 双边滤波")
    print("0: 原始图像")
    print("q: 退出")
    
    # 初始模糊类型
    blur_type = 0  # 0:原始, 1:均值, 2:高斯, 3:中值, 4:双边
    
    while True:
        ret, frame = cap.read()
        if not ret:
            print("无法获取图像帧")
            break
        
        # 根据选择应用不同的模糊方法
        if blur_type == 1:
            processed = cv2.blur(frame, (15, 15))
            title = "Average Blur (1)"
        elif blur_type == 2:
            processed = cv2.GaussianBlur(frame, (15, 15), 0)
            title = "Gaussian Blur (2)"
        elif blur_type == 3:
            processed = cv2.medianBlur(frame, 15)
            title = "Median Blur (3)"
        elif blur_type == 4:
            processed = cv2.bilateralFilter(frame, 15, 75, 75)
            title = "Bilateral Filter (4)"
        else:
            processed = frame.copy()
            title = "Original (0)"
        
        # 显示结果
        cv2.imshow('Blur Demo - ' + title, processed)
        
        # 处理键盘输入
        key = cv2.waitKey(1) & 0xFF
        if key == ord('q'):
            break
        elif key == ord('0'):
            blur_type = 0
        elif key == ord('1'):
            blur_type = 1
        elif key == ord('2'):
            blur_type = 2
        elif key == ord('3'):
            blur_type = 3
        elif key == ord('4'):
            blur_type = 4
    
    cap.release()
    cv2.destroyAllWindows()

def add_noise_and_denoise_demo():
    """
    添加噪声并去噪演示
    """
    img = cv2.imread('image.jpg')
    
    if img is None:
        print("无法加载图像,请检查文件路径")
        return
    
    # 添加高斯噪声
    mean = 0
    var = 100
    sigma = var ** 0.5
    gaussian = np.random.normal(mean, sigma, img.shape)
    noisy_img = np.clip(img + gaussian, 0, 255).astype(np.uint8)
    
    # 添加椒盐噪声
    def add_salt_pepper(image, amount=0.05):
        out = image.copy()
        # 盐噪声 (白色)
        num_salt = np.ceil(amount * image.size * 0.5)
        coords = [np.random.randint(0, i-1, int(num_salt)) for i in image.shape]
        out[coords[0], coords[1], coords[2]] = 255
        
        # 椒噪声 (黑色)
        num_pepper = np.ceil(amount * image.size * 0.5)
        coords = [np.random.randint(0, i-1, int(num_pepper)) for i in image.shape]
        out[coords[0], coords[1], coords[2]] = 0
        return out
    
    salt_pepper_img = add_salt_pepper(img)
    
    # 去噪处理
    denoised_gaussian = cv2.GaussianBlur(noisy_img, (5, 5), 0)
    denoised_median = cv2.medianBlur(salt_pepper_img, 5)
    
    # 显示结果
    cv2.imshow('Original', img)
    cv2.imshow('Gaussian Noise', noisy_img)
    cv2.imshow('Denoised (Gaussian Blur)', denoised_gaussian)
    cv2.imshow('Salt & Pepper Noise', salt_pepper_img)
    cv2.imshow('Denoised (Median Blur)', denoised_median)
    
    print("按任意键退出...")
    cv2.waitKey(0)
    cv2.destroyAllWindows()

def main():
    print("OpenCV 图像模糊处理演示")
    print("请选择演示模式:")
    print("1. 图像文件模糊处理")
    print("2. 实时摄像头模糊处理")
    print("3. 噪声添加与去噪演示")
    
    choice = input("请输入选项 (1-3): ")
    
    if choice == '1':
        image_blur_demo()
    elif choice == '2':
        live_camera_blur_demo()
    elif choice == '3':
        add_noise_and_denoise_demo()
    else:
        print("无效输入")

if __name__ == "__main__":
    main()

模糊技术详解

  1. 均值模糊 (Averaging Blur)
cv2.blur(src, ksize)
  • 原理:用核内所有像素的平均值替换中心像素
  • 参数:
    • ksize: 模糊核大小 (宽度, 高度)
  • 特点:简单快速,但会产生均匀的模糊效果

2. 高斯模糊 (Gaussian Blur)

cv2.GaussianBlur(src, ksize, sigmaX)
  • 原理:使用高斯函数生成的权重核进行加权平均
  • 参数:
    • ksize: 核大小(必须是正奇数)
    • sigmaX: X方向的高斯核标准差
  • 特点:更自然的模糊效果,保留更多边缘信息

3. 中值模糊 (Median Blur)

cv2.medianBlur(src, ksize)
  • 原理:用核内所有像素的中值替换中心像素
  • 参数:
    • ksize: 核大小(必须是大于1的奇数)
  • 特点:对椒盐噪声特别有效,能保留边缘

4. 双边滤波 (Bilateral Filter)

cv2.bilateralFilter(src, d, sigmaColor, sigmaSpace)
  • 原理:同时考虑空间距离和像素值相似性的非线性滤波
  • 参数:
    • d: 像素邻域直径
    • sigmaColor: 颜色空间的标准差
    • sigmaSpace: 坐标空间的标准差
  • 特点:能在平滑的同时保持边缘锐利,常用于美颜

5-8 Blur后面的原理

均值Blur

image-20250328110501414

Padding与Kernel

image-20250328110612994

Kernel算子大小

算子一般是 3x3的矩阵

算子元素的个数要用奇数,要保证中心对齐

5x5,7x7 会增加运算量

好处是受视野大,可以捕获更多的信息

步长

image-20250328110759811

中值Blur

image-20250328110953173

高斯正态分布

image-20250328111143191

高斯Blur

image-20250328111220164

双边Blur

双边Blur相较于其它Blur要复杂的多

它没有固定的滤波矩阵,需要动态计算

要计算两种滤波矩阵,空间滤波矩阵和相似滤波矩阵

空间滤波矩阵计算的是相邻像素到中心像素的距离权重

相似滤波矩阵计算的是相邻像素与中间像素的差值的权重

双边滤波

距离矩阵与相似矩阵都使用的是高斯算法

最终将计算出的两个矩阵相乘

再乘以Filter覆盖的像素,最终得到结果

5-9 腐蚀操作

腐蚀的算法

  • 让kernel在原图像上滑动
  • 输出像素被设置为kernel区域内最小值
import cv2
import numpy as np


def image_erosion_demo():
    """
    图像腐蚀操作演示(静态图像)
    """
    # 读取图像(转换为灰度图)
    img = cv2.imread('image.jpg', cv2.IMREAD_GRAYSCALE)

    if img is None:
        print("无法加载图像,请检查文件路径")
        return

    # 二值化处理(便于演示腐蚀效果)
    _, binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)

    # 定义不同形状的结构元素(kernel)
    kernel_rect = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
    kernel_cross = cv2.getStructuringElement(cv2.MORPH_CROSS, (5, 5))
    kernel_ellipse = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))

    # 应用不同kernel的腐蚀操作
    erosion_rect = cv2.erode(binary, kernel_rect, iterations=1)
    erosion_cross = cv2.erode(binary, kernel_cross, iterations=1)
    erosion_ellipse = cv2.erode(binary, kernel_ellipse, iterations=1)

    # 显示结果
    cv2.imshow('Original', img)
    cv2.imshow('Binary', binary)
    cv2.imshow('Erosion (Rectangle Kernel)', erosion_rect)
    cv2.imshow('Erosion (Cross Kernel)', erosion_cross)
    cv2.imshow('Erosion (Ellipse Kernel)', erosion_ellipse)

    print("按任意键退出...")
    cv2.waitKey(0)
    cv2.destroyAllWindows()


def live_camera_erosion_demo():
    """
    实时摄像头腐蚀操作演示
    """
    cap = cv2.VideoCapture(0)

    if not cap.isOpened():
        print("无法打开摄像头")
        return

    print("实时摄像头腐蚀操作演示")
    print("按键说明:")
    print("1: 矩形结构元素")
    print("2: 十字形结构元素")
    print("3: 椭圆形结构元素")
    print("0: 原始图像")
    print("+/-: 增加/减少腐蚀程度")
    print("q: 退出")

    # 初始参数
    kernel_type = 1  # 1:矩形, 2:十字形, 3:椭圆形
    iterations = 1  # 腐蚀次数
    kernel_size = 5  # 结构元素大小

    while True:
        ret, frame = cap.read()
        if not ret:
            print("无法获取图像帧")
            break

        # 转换为灰度图并二值化
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)

        # 根据选择创建不同的结构元素
        if kernel_type == 1:
            kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernel_size, kernel_size))
            title = f"Rect Kernel (Size:{kernel_size}, Iter:{iterations})"
        elif kernel_type == 2:
            kernel = cv2.getStructuringElement(cv2.MORPH_CROSS, (kernel_size, kernel_size))
            title = f"Cross Kernel (Size:{kernel_size}, Iter:{iterations})"
        elif kernel_type == 3:
            kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size))
            title = f"Ellipse Kernel (Size:{kernel_size}, Iter:{iterations})"
        else:
            kernel = None

        # 应用腐蚀操作
        if kernel is not None:
            processed = cv2.erode(binary, kernel, iterations=iterations)
        else:
            processed = binary.copy()
            title = "Original"

        # 显示结果
        cv2.imshow('Erosion Demo - ' + title, processed)

        # 处理键盘输入
        key = cv2.waitKey(1) & 0xFF
        if key == ord('q'):
            break
        elif key == ord('0'):
            kernel_type = 0
        elif key == ord('1'):
            kernel_type = 1
        elif key == ord('2'):
            kernel_type = 2
        elif key == ord('3'):
            kernel_type = 3
        elif key == ord('+'):
            iterations = min(iterations + 1, 10)
        elif key == ord('-'):
            iterations = max(iterations - 1, 1)
        elif key == ord('['):
            kernel_size = max(kernel_size - 2, 3)
        elif key == ord(']'):
            kernel_size = min(kernel_size + 2, 21)

    cap.release()
    cv2.destroyAllWindows()


def erosion_visualization():
    """
    腐蚀操作可视化演示(使用自定义图像)
    """
    # 创建测试图像(白色背景,黑色文字)
    img = np.ones((300, 400), dtype=np.uint8) * 255
    cv2.putText(img, 'OpenCV', (50, 150), cv2.FONT_HERSHEY_SIMPLEX, 3, 0, 10)

    # 定义结构元素
    kernel = np.ones((15, 15), np.uint8)

    # 应用腐蚀
    erosion = cv2.erode(img, kernel, iterations=1)

    # 显示过程动画
    print("腐蚀过程可视化(按任意键继续)...")
    cv2.imshow('Original', img)
    cv2.waitKey(500)

    # 显示kernel
    kernel_display = cv2.resize(kernel * 255, (200, 200), interpolation=cv2.INTER_NEAREST)
    cv2.imshow('Kernel (15x15)', kernel_display)
    cv2.waitKey(500)

    # 显示腐蚀结果
    cv2.imshow('After Erosion', erosion)
    cv2.waitKey(0)
    cv2.destroyAllWindows()


def main():
    print("OpenCV 图像腐蚀操作演示")
    print("请选择演示模式:")
    print("1. 图像文件腐蚀操作")
    print("2. 实时摄像头腐蚀操作")
    print("3. 腐蚀操作可视化演示")

    choice = input("请输入选项 (1-3): ")

    if choice == '1':
        image_erosion_demo()
    elif choice == '2':
        live_camera_erosion_demo()
    elif choice == '3':
        erosion_visualization()
    else:
        print("无效输入")


if __name__ == "__main__":
    main()

腐蚀操作详解

1. 腐蚀算法原理

腐蚀操作的数学表达式:

dst(x,y) = min_{(i,j)∈kernel} src(x+i,y+j)
  • 用结构元素(kernel)扫描图像的每一个像素
  • 用结构元素覆盖区域内的最小像素值替换中心像素
  • 对于二值图像(0和255),效果是使白色区域缩小,黑色区域扩大

2. cv2.erode() 函数参数

dst = cv2.erode(src, kernel[, dst[, anchor[, iterations[, borderType[, borderValue]]]]])
  • src: 输入图像(通常是二值图像)
  • kernel: 结构元素(可以使用cv2.getStructuringElement()创建)
  • iterations: 腐蚀次数(默认为1)
  • anchor: 锚点位置(默认(-1,-1)表示中心)
  • borderType: 边界处理方式
  • borderValue: 边界值

3. 结构元素类型

cv2.getStructuringElement(shape, ksize[, anchor])
  • shape: 结构元素形状
    • cv2.MORPH_RECT: 矩形
    • cv2.MORPH_ELLIPSE: 椭圆形
    • cv2.MORPH_CROSS: 十字形
  • ksize: 结构元素大小

image-20250328140049165

image-20250328140122180

卷积核基本概念

卷积核是一个小矩阵,用于在图像上滑动并进行形态学操作(如腐蚀、膨胀等)。它的两个关键属性是:

  1. 形状:决定了邻域像素的包含方式
  2. 大小:决定了操作的影响范围

cv2.getStructuringElement() 函数

kernel = cv2.getStructuringElement(shape, ksize[, anchor])

参数说明:

  • shape:卷积核的形状,有以下三种主要类型
    • cv2.MORPH_RECT:矩形
    • cv2.MORPH_ELLIPSE:椭圆形
    • cv2.MORPH_CROSS:十字形
  • ksize:卷积核的大小,格式为(宽度, 高度),必须是正奇数
  • anchor:锚点位置,默认(-1,-1)表示中心点

三种卷积核类型详解

1. 矩形卷积核 (MORPH_RECT)

cv2.getStructuringElement(cv2.MORPH_RECT, (5,5))

结构特点:

  • 最简单的卷积核类型
  • 核内所有像素的权重相等
  • 形状为完整的矩形

可视化表示(5×5):

1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
1 1 1 1 1

适用场景:

  • 需要均匀处理所有方向的情况
  • 对方向不敏感的操作
  • 计算效率最高

2. 椭圆形卷积核 (MORPH_ELLIPSE)

cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5,5))

结构特点:

  • 近似椭圆形状
  • 中心对称
  • 边缘像素的权重可能不同

可视化表示(5×5):

0 0 1 0 0
1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
0 0 1 0 0

适用场景:

  • 需要各向同性(旋转不变性)处理
  • 自然形状的模拟
  • 边缘保留操作

3. 十字形卷积核 (MORPH_CROSS)

cv2.getStructuringElement(cv2.MORPH_CROSS, (5,5))

结构特点:

  • 十字形状
  • 只有中心行和中心列的像素被包含
  • 对角线的像素不包含

可视化表示(5×5):

0 0 1 0 0
0 0 1 0 0
1 1 1 1 1
0 0 1 0 0
0 0 1 0 0

适用场景:

  • 需要强调水平或垂直方向的操作
  • 线条检测
  • 特定方向的结构处理

对比演示代码

import cv2
import numpy as np

# 创建不同形状的卷积核
size = (7,7)  # 可以修改为(3,3)、(5,5)等观察效果

kernel_rect = cv2.getStructuringElement(cv2.MORPH_RECT, size)
kernel_ellipse = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, size)
kernel_cross = cv2.getStructuringElement(cv2.MORPH_CROSS, size)

# 可视化卷积核
def visualize_kernel(kernel, title):
    # 放大显示
    display = cv2.resize(kernel*255, (200,200), interpolation=cv2.INTER_NEAREST)
    cv2.imshow(title, display)
    print(title)
    print(kernel)  # 打印矩阵值

visualize_kernel(kernel_rect, "Rectangle Kernel")
visualize_kernel(kernel_ellipse, "Ellipse Kernel")
visualize_kernel(kernel_cross, "Cross Kernel")

cv2.waitKey(0)
cv2.destroyAllWindows()

卷积核大小的影响

卷积核大小直接影响形态学操作的效果:

  1. 小尺寸(3×3):
    • 精细操作
    • 保留更多细节
    • 对微小特征敏感
  2. 大尺寸(如15×15):
    • 更强的形态学效果
    • 可能丢失细节
    • 计算量更大

实际应用示例

1. 使用不同卷积核进行腐蚀操作

img = cv2.imread('image.png', 0)
_, binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)

# 使用不同卷积核腐蚀
erode_rect = cv2.erode(binary, kernel_rect)
erode_ellipse = cv2.erode(binary, kernel_ellipse)
erode_cross = cv2.erode(binary, kernel_cross)

2. 卷积核在边缘检测中的应用

# 使用十字形卷积核增强垂直线条
vertical_kernel = cv2.getStructuringElement(cv2.MORPH_CROSS, (1, 15))
vertical_lines = cv2.morphologyEx(img, cv2.MORPH_OPEN, vertical_kernel)

选择指南

卷积核类型适用场景优点缺点
MORPH_RECT通用处理、快速操作计算效率高各向异性
MORPH_ELLIPSE自然形状处理、边缘保留各向同性计算量稍大
MORPH_CROSS特定方向处理、线条检测方向性强可能引入方向偏差

5-10 膨胀操作

膨胀的算法

让kernel在原图像上滑动 输出像素被设置为kernel区域内最大值

image-20250328141355627

image-20250328141422548

import cv2
import numpy as np


def image_dilation_demo():
    """
    图像膨胀操作演示(静态图像)
    """
    # 读取图像(转换为灰度图)
    img = cv2.imread('image.jpg', cv2.IMREAD_GRAYSCALE)

    if img is None:
        print("无法加载图像,请检查文件路径")
        return

    # 二值化处理(便于演示膨胀效果)
    _, binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY_INV)  # 反转以便观察膨胀效果

    # 定义不同形状的结构元素(kernel)
    kernel_rect = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
    kernel_cross = cv2.getStructuringElement(cv2.MORPH_CROSS, (5, 5))
    kernel_ellipse = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))

    # 应用不同kernel的膨胀操作
    dilation_rect = cv2.dilate(binary, kernel_rect, iterations=1)
    dilation_cross = cv2.dilate(binary, kernel_cross, iterations=1)
    dilation_ellipse = cv2.dilate(binary, kernel_ellipse, iterations=1)

    # 显示结果
    cv2.imshow('Original', img)
    cv2.imshow('Binary (Inverted)', binary)
    cv2.imshow('Dilation (Rectangle Kernel)', dilation_rect)
    cv2.imshow('Dilation (Cross Kernel)', dilation_cross)
    cv2.imshow('Dilation (Ellipse Kernel)', dilation_ellipse)

    print("按任意键退出...")
    cv2.waitKey(0)
    cv2.destroyAllWindows()


def live_camera_dilation_demo():
    """
    实时摄像头膨胀操作演示
    """
    cap = cv2.VideoCapture(0)

    if not cap.isOpened():
        print("无法打开摄像头")
        return

    print("实时摄像头膨胀操作演示")
    print("按键说明:")
    print("1: 矩形结构元素")
    print("2: 十字形结构元素")
    print("3: 椭圆形结构元素")
    print("0: 原始图像")
    print("+/-: 增加/减少膨胀程度")
    print("q: 退出")

    # 初始参数
    kernel_type = 1  # 1:矩形, 2:十字形, 3:椭圆形
    iterations = 1  # 膨胀次数
    kernel_size = 5  # 结构元素大小

    while True:
        ret, frame = cap.read()
        if not ret:
            print("无法获取图像帧")
            break

        # 转换为灰度图并二值化(反转以便观察膨胀效果)
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV)

        # 根据选择创建不同的结构元素
        if kernel_type == 1:
            kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernel_size, kernel_size))
            title = f"Rect Kernel (Size:{kernel_size}, Iter:{iterations})"
        elif kernel_type == 2:
            kernel = cv2.getStructuringElement(cv2.MORPH_CROSS, (kernel_size, kernel_size))
            title = f"Cross Kernel (Size:{kernel_size}, Iter:{iterations})"
        elif kernel_type == 3:
            kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size))
            title = f"Ellipse Kernel (Size:{kernel_size}, Iter:{iterations})"
        else:
            kernel = None

        # 应用膨胀操作
        if kernel is not None:
            processed = cv2.dilate(binary, kernel, iterations=iterations)
        else:
            processed = binary.copy()
            title = "Original"

        # 显示结果
        cv2.imshow('Dilation Demo - ' + title, processed)

        # 处理键盘输入
        key = cv2.waitKey(1) & 0xFF
        if key == ord('q'):
            break
        elif key == ord('0'):
            kernel_type = 0
        elif key == ord('1'):
            kernel_type = 1
        elif key == ord('2'):
            kernel_type = 2
        elif key == ord('3'):
            kernel_type = 3
        elif key == ord('+'):
            iterations = min(iterations + 1, 10)
        elif key == ord('-'):
            iterations = max(iterations - 1, 1)
        elif key == ord('['):
            kernel_size = max(kernel_size - 2, 3)
        elif key == ord(']'):
            kernel_size = min(kernel_size + 2, 21)

    cap.release()
    cv2.destroyAllWindows()


def dilation_visualization():
    """
    膨胀操作可视化演示(使用自定义图像)
    """
    # 创建测试图像(白色背景,黑色文字)
    img = np.ones((300, 400), dtype=np.uint8) * 255
    cv2.putText(img, 'OpenCV', (50, 150), cv2.FONT_HERSHEY_SIMPLEX, 3, 0, 10)

    # 反转图像以便观察膨胀效果
    binary = cv2.bitwise_not(img)

    # 定义结构元素
    kernel = np.ones((15, 15), np.uint8)

    # 应用膨胀
    dilation = cv2.dilate(binary, kernel, iterations=1)

    # 显示过程动画
    print("膨胀过程可视化(按任意键继续)...")
    cv2.imshow('Original', img)
    cv2.waitKey(500)
    cv2.imshow('Binary (Inverted)', binary)
    cv2.waitKey(500)

    # 显示kernel
    kernel_display = cv2.resize(kernel * 255, (200, 200), interpolation=cv2.INTER_NEAREST)
    cv2.imshow('Kernel (15x15)', kernel_display)
    cv2.waitKey(500)

    # 显示膨胀结果
    cv2.imshow('After Dilation', dilation)
    cv2.waitKey(0)
    cv2.destroyAllWindows()


def main():
    print("OpenCV 图像膨胀操作演示")
    print("请选择演示模式:")
    print("1. 图像文件膨胀操作")
    print("2. 实时摄像头膨胀操作")
    print("3. 膨胀操作可视化演示")

    choice = input("请输入选项 (1-3): ")

    if choice == '1':
        image_dilation_demo()
    elif choice == '2':
        live_camera_dilation_demo()
    elif choice == '3':
        dilation_visualization()
    else:
        print("无效输入")


if __name__ == "__main__":
    main()

膨胀操作详解

1. 膨胀算法原理

膨胀操作的数学表达式:

dst(x,y) = max_{(i,j)∈kernel} src(x+i,y+j)
  • 用结构元素(kernel)扫描图像的每一个像素
  • 用结构元素覆盖区域内的最大像素值替换中心像素
  • 对于二值图像(0和255),效果是使白色区域扩大,黑色区域缩小

2. cv2.dilate() 函数参数

dst = cv2.dilate(src, kernel[, dst[, anchor[, iterations[, borderType[, borderValue]]]]])
  • src: 输入图像(通常是二值图像)
  • kernel: 结构元素(可以使用cv2.getStructuringElement()创建)
  • iterations: 膨胀次数(默认为1)
  • anchor: 锚点位置(默认(-1,-1)表示中心)
  • borderType: 边界处理方式
  • borderValue: 边界值

应用场景

  1. 填充孔洞:膨胀可以填充物体内部的小孔
  2. 连接断裂部分:使断裂的边缘或线条重新连接
  3. 增加物体尺寸:使细小的物体变得更明显
  4. 边缘平滑:与腐蚀结合使用(闭运算)可以平滑物体边缘

使用说明

  1. 对于图像文件处理,请确保当前目录下有名为"image.jpg"的图像文件
  2. 实时摄像头演示中可以通过按键控制:
    • 1/2/3切换不同kernel类型
    • +/-调整膨胀次数
    • [/]调整kernel大小
  3. 可视化演示展示了膨胀操作对文字的影响
  4. 按'q'键可以退出程序

5-11 开运算与闭运算

开运算

开运算 = 腐蚀 + 膨胀

image-20250328142054954

import cv2
import numpy as np


def opening_operation_demo():
    """
    开运算演示(先腐蚀后膨胀)
    """
    # 读取图像(转换为灰度图)
    img = cv2.imread('image.jpg', cv2.IMREAD_GRAYSCALE)

    if img is None:
        print("无法加载图像,请检查文件路径")
        return

    # 二值化处理
    _, binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)

    # 定义结构元素
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))

    # 方法1:分步实现开运算
    erosion = cv2.erode(binary, kernel)  # 先腐蚀
    opening_step = cv2.dilate(erosion, kernel)  # 后膨胀

    # 方法2:直接使用开运算函数
    opening = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)

    # 显示结果
    cv2.imshow('Original', img)
    cv2.imshow('Binary', binary)
    cv2.imshow('Erosion (Step 1)', erosion)
    cv2.imshow('Opening (Step by Step)', opening_step)
    cv2.imshow('Opening (Direct Function)', opening)

    print("按任意键退出...")
    cv2.waitKey(0)
    cv2.destroyAllWindows()


def closing_operation_demo():
    """
    闭运算演示(先膨胀后腐蚀)
    """
    # 读取图像(转换为灰度图)
    img = cv2.imread('image.jpg', cv2.IMREAD_GRAYSCALE)

    if img is None:
        print("无法加载图像,请检查文件路径")
        return

    # 二值化处理
    _, binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)

    # 定义结构元素
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))

    # 方法1:分步实现闭运算
    dilation = cv2.dilate(binary, kernel)  # 先膨胀
    closing_step = cv2.erode(dilation, kernel)  # 后腐蚀

    # 方法2:直接使用闭运算函数
    closing = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)

    # 显示结果
    cv2.imshow('Original', img)
    cv2.imshow('Binary', binary)
    cv2.imshow('Dilation (Step 1)', dilation)
    cv2.imshow('Closing (Step by Step)', closing_step)
    cv2.imshow('Closing (Direct Function)', closing)

    print("按任意键退出...")
    cv2.waitKey(0)
    cv2.destroyAllWindows()


def live_morphological_operations():
    """
    实时摄像头形态学操作演示
    """
    cap = cv2.VideoCapture(0)

    if not cap.isOpened():
        print("无法打开摄像头")
        return

    print("实时摄像头形态学操作演示")
    print("按键说明:")
    print("o: 开运算")
    print("c: 闭运算")
    print("e: 仅腐蚀")
    print("d: 仅膨胀")
    print("0: 原始图像")
    print("+/-: 增加/减少操作强度")
    print("q: 退出")

    # 初始参数
    operation = 0  # 0:原始, 1:开运算, 2:闭运算, 3:腐蚀, 4:膨胀
    kernel_size = 5

    while True:
        ret, frame = cap.read()
        if not ret:
            print("无法获取图像帧")
            break

        # 转换为灰度图并二值化
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)

        # 创建结构元素
        kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size))

        # 应用选择的形态学操作
        if operation == 1:
            processed = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)
            title = f"Opening (Size:{kernel_size})"
        elif operation == 2:
            processed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
            title = f"Closing (Size:{kernel_size})"
        elif operation == 3:
            processed = cv2.erode(binary, kernel)
            title = f"Erosion (Size:{kernel_size})"
        elif operation == 4:
            processed = cv2.dilate(binary, kernel)
            title = f"Dilation (Size:{kernel_size})"
        else:
            processed = binary.copy()
            title = "Original"

        # 显示结果
        cv2.imshow('Morphological Operations - ' + title, processed)

        # 处理键盘输入
        key = cv2.waitKey(1) & 0xFF
        if key == ord('q'):
            break
        elif key == ord('0'):
            operation = 0
        elif key == ord('o'):
            operation = 1
        elif key == ord('c'):
            operation = 2
        elif key == ord('e'):
            operation = 3
        elif key == ord('d'):
            operation = 4
        elif key == ord('+'):
            kernel_size = min(kernel_size + 2, 21)
        elif key == ord('-'):
            kernel_size = max(kernel_size - 2, 3)

    cap.release()
    cv2.destroyAllWindows()


def main():
    print("OpenCV 形态学操作演示")
    print("请选择演示模式:")
    print("1. 开运算演示")
    print("2. 闭运算演示")
    print("3. 实时摄像头形态学操作")

    choice = input("请输入选项 (1-3): ")

    if choice == '1':
        opening_operation_demo()
    elif choice == '2':
        closing_operation_demo()
    elif choice == '3':
        live_morphological_operations()
    else:
        print("无效输入")


if __name__ == "__main__":
    main()

一、开运算 (Opening)

1. 定义

开运算 = 先腐蚀后膨胀 数学表达式:Opening(src) = dilate(erode(src))

2. 作用原理

  • 腐蚀阶段:消除所有比结构元素小的亮区域(前景物体)
  • 膨胀阶段:恢复保留下来物体的原始大小(但不恢复被消除的小物体)

3. 主要特点

  • 能够消除孤立的亮点(噪声)
  • 断开狭窄的连接部分
  • 平滑物体的轮廓
  • 基本不改变物体的面积和位置

4. 效果示意图

原始图像:[1 1 1 1 0 1 1 1 1]
腐蚀后: [0 1 1 0 0 0 1 1 0] (消除孤立点)
膨胀后: [1 1 1 1 0 1 1 1 1] (恢复主要部分)

5. 典型应用场景

  • 去除小噪声点(白噪声)
  • 分离紧密连接的物体
  • 指纹图像中分离纹线
  • 文本图像中去除墨渍

二、闭运算 (Closing)

1. 定义

闭运算 = 先膨胀后腐蚀 数学表达式:Closing(src) = erode(dilate(src))

2. 作用原理

  • 膨胀阶段:填充比结构元素小的暗区域(孔洞)
  • 腐蚀阶段:恢复物体的原始大小(但保留填充的孔洞)

3. 主要特点

  • 能够填充小的孔洞和裂缝
  • 连接邻近的物体
  • 平滑物体的轮廓
  • 基本不改变物体的面积和位置

4. 效果示意图

原始图像:[1 0 1 1 0 1 0 1]
膨胀后: [1 1 1 1 1 1 1 1] (填充孔洞)
腐蚀后: [1 1 1 1 1 1 1 1] (保持填充效果)

5. 典型应用场景

  • 填充物体内部的小孔
  • 连接断裂的边缘
  • 平滑物体边界
  • 医学图像中填充组织间隙

三、开运算与闭运算对比

特性开运算闭运算
操作顺序先腐蚀后膨胀先膨胀后腐蚀
主要作用消除小物体填充小孔洞
对亮区域影响消除小的亮区域连接邻近的亮区域
对暗区域影响基本不影响暗区域消除小的暗区域
常用场景去噪、分离物体填充孔洞、连接断裂

5-12 开运算与其它复杂运算背后的原理

开运算与闭运算的算法

image-20250328142920387

消除噪点的办法

将kernel size 设置的比噪点大

开运算时,将iterations设置为N

一般情况下,我们只需要消除3x3之下的噪点

开闭运算与形态学复杂运算原理详解

形态学图像处理是计算机视觉中的核心技术之一,开闭运算作为基础操作,与其它复杂形态学运算共同构成了强大的图像分析工具集。下面我将从原理层面深入解析这些运算。

一、开闭运算的数学基础

1. 基本定义

开运算和闭运算都基于集合论中的Minkowski运算:

  • 开运算:A∘B = (A⊖B)⊕B
  • 闭运算:A•B = (A⊕B)⊖B

其中:

  • A为图像集合
  • B为结构元素
  • ⊖表示腐蚀(Minkowski减法)
  • ⊕表示膨胀(Minkowski加法)

2. 代数性质

开闭运算具有以下重要数学性质:

性质开运算闭运算
幂等性(A∘B)∘B = A∘B(A•B)•B = A•B
递增性A₁⊆A₂ ⇒ A₁∘B⊆A₂∘BA₁⊆A₂ ⇒ A₁•B⊆A₂•B
对偶性(Aᶜ•B)ᶜ = A∘B̌(Aᶜ∘B)ᶜ = A•B̌

(注:B̌表示结构元素B的对称集)

二、复杂形态学运算原理

1. 形态学梯度

定义:G = (A⊕B) - (A⊖B)

物理意义:

  • 膨胀结果减去腐蚀结果
  • 反映图像中物体的边界信息
  • 可用于边缘检测

类型:

  • 基本梯度:如上定义
  • 内部梯度:A - (A⊖B)
  • 外部梯度:(A⊕B) - A

2. 顶帽(Top-hat)变换

定义:T = A - (A∘B)

特点:

  • 提取比结构元素小的亮特征
  • 对不均匀光照有补偿作用

应用场景:

  • 背景校正
  • 小目标检测
  • 文本图像中的笔画提取

3. 黑帽(Black-hat)变换

定义:B = (A•B) - A

特点:

  • 提取比结构元素小的暗特征
  • 与顶帽变换形成对偶

应用场景:

  • 孔洞检测
  • 阴影分析
  • 工业缺陷检测

三、高级形态学技术

1. 粒度测定

原理: 通过改变结构元素大小进行多尺度分析:

G(r) = ∑(A∘rB - A∘(r+1)B)

其中r为尺度参数

应用:

  • 颗粒大小分布分析
  • 纹理分类

2. 形态学重建

基于标记图像的迭代膨胀过程:

R(f|g) = lim_{n→∞} δ_g^n(f)

其中:

  • f:标记图像
  • g:掩模图像
  • δ:条件膨胀算子

优势:

  • 保持原始形状特征
  • 不受结构元素形状严格限制

3. 分水岭算法

结合形态学的图像分割方法:

  1. 计算形态学梯度
  2. 寻找标记点
  3. 模拟洪水淹没过程

关键步骤:

watershed( gradient | markers )

四、OpenCV实现示例

import cv2
import numpy as np

# 高级形态学运算演示
def advanced_morphology_demo():
    img = cv2.imread('image.png', 0)
    _, binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5,5))
    
    # 形态学梯度
    gradient = cv2.morphologyEx(binary, cv2.MORPH_GRADIENT, kernel)
    
    # 顶帽变换
    tophat = cv2.morphologyEx(binary, cv2.MORPH_TOPHAT, kernel)
    
    # 黑帽变换
    blackhat = cv2.morphologyEx(binary, cv2.MORPH_BLACKHAT, kernel)
    
    # 显示结果
    cv2.imshow('Original', binary)
    cv2.imshow('Gradient', gradient)
    cv2.imshow('Top-hat', tophat)
    cv2.imshow('Black-hat', blackhat)
    cv2.waitKey(0)

# 形态学重建实现
def morphological_reconstruction(marker, mask, kernel_size=3):
    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size,kernel_size))
    while True:
        expanded = cv2.dilate(marker, kernel)
        expanded = cv2.bitwise_and(expanded, mask)
        if cv2.countNonZero(expanded - marker) == 0:
            break
        marker = expanded.copy()
    return marker

# 分水岭算法示例
def watershed_demo():
    img = cv2.imread('cells.png')
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)
    
    # 去除噪声
    kernel = np.ones((3,3), np.uint8)
    opening = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel, iterations=2)
    
    # 确定背景区域
    sure_bg = cv2.dilate(opening, kernel, iterations=3)
    
    # 寻找确定前景区域
    dist_transform = cv2.distanceTransform(opening, cv2.DIST_L2, 5)
    _, sure_fg = cv2.threshold(dist_transform, 0.7*dist_transform.max(), 255, 0)
    
    # 标记创建
    sure_fg = np.uint8(sure_fg)
    unknown = cv2.subtract(sure_bg, sure_fg)
    _, markers = cv2.connectedComponents(sure_fg)
    markers += 1
    markers[unknown==255] = 0
    
    # 分水岭算法
    markers = cv2.watershed(img, markers)
    img[markers == -1] = [255,0,0]
    
    cv2.imshow('Watershed', img)
    cv2.waitKey(0)

五、工程实践建议

  1. 结构元素选择:

    • 矩形:通用场景
    • 圆形:各向同性处理
    • 十字形:强调特定方向
  2. 参数调优方法:

    # 自动确定结构元素大小
    def auto_kernel_size(image, percentile=95):
        edges = cv2.Canny(image, 50, 150)
        nonzero = np.nonzero(edges)
        if len(nonzero[0]) == 0:
            return 3
        dists = distance_matrix(nonzero)
        return int(np.percentile(dists, percentile))
    
  3. 性能优化技巧:

    • 对小图像使用查表法(LUT)
    • 对大图像使用并行处理
    • 对视频流使用滑动窗口优化
  4. 常见问题解决方案:

    • 过度腐蚀:减小迭代次数或结构元素尺寸
    • 连接过度:先开运算后闭运算
    • 边界效应:使用BORDER_REFLECT填充

形态学运算作为计算机视觉的基础工具,其背后的数学原理和工程实现技巧值得深入研究和实践。掌握这些知识将极大提升图像处理任务的解决能力。

iterations的真实含义

对于开运算 cv::morphologyEx(src, dst, MORPH_OPEN, kernel, anchor, iterations, borderType, borderValue):

case MORPH_OPEN:
    // 先执行iterations次腐蚀
    erode(src, dst, kernel, anchor, iterations, borderType, borderValue);
    // 再对腐蚀结果执行iterations次膨胀 
    dilate(dst, dst, kernel, anchor, iterations, borderType, borderValue);
    break;

关键点:

  • 腐蚀和膨胀各执行iterations次,不是总共iterations次
  • 每次迭代都使用相同的结构元素(kernel)
  • 中间结果存储在同一个dst中

其它算法

image-20250328144146195

5-13 查找图像轮廓

什么是图像轮廓?

具有相同颜色或强度的连续点的曲线

轮廓

image-20250328144400698

图像轮廓的作用

可以用于图形分析物体的识别与检测

注意点

  • 为了检测的准确性,需要先对图像进行二值化或Canny操作
  • 画轮廓时会修改输入的图像

轮廓查找的API

findContours(img,mode,ApproximationMode..)

两个返回值,contours和hierarchy

mode

  • RETR_EXTERNAL=0,表示只检测外轮廓
  • RETR_LIST=1,检测的轮廓不建立等级关系
  • RETR_CCOMP=2,每层最多两级
  • RETR_TREE=3,按树形存储轮廓

EXTERNAL

image-20250328144938122

LIST

image-20250328145024289

CCOMP

image-20250328145124224

TREE

image-20250328145234954

ApproximationMode

  • CHAIN_APPROX_NONE,保存所有轮廓上的点
  • CHAIN_APPROX_SIMPLE,只保存角点

5-14 如何绘制轮廓

API

cv2.drawContours() 是 OpenCV 中用于绘制轮廓的函数,其完整参数列表如下:

函数原型(Python):

cv2.drawContours(
    image,                # 输入/输出的图像(会被修改)
    contours,             # 轮廓列表(通常为 `findContours` 的返回值)
    contourIdx,           # 要绘制的轮廓索引
    color,                # 轮廓颜色(BGR 格式)
    thickness=None,       # 轮廓线宽
    lineType=None,        # 线型(如 8-connected、AA 等)
    hierarchy=None,       # 可选层级关系(来自 `findContours`)
    maxLevel=None,        # 最大绘制层级深度
    offset=None           # 轮廓点坐标偏移量
)

参数详解:

  1. image
    • 类型:numpy.ndarray(BGR 图像)
    • 作用:输入的图像,绘制结果会直接修改此图像。
  2. contours
    • 类型:List[numpy.ndarray]
    • 作用:轮廓列表,通常来自 cv2.findContours() 的返回值。每个轮廓是一个点集(形状为 (n, 1, 2) 的数组)。
  3. contourIdx
    • 类型:int
    • 作用:指定要绘制的轮廓索引:
      • 若为 -1,绘制所有轮廓;
      • 若为 0 或正整数,绘制对应索引的轮廓。
  4. color
    • 类型:Tuple[int, int, int](BGR 格式)
    • 示例:(255, 0, 0) 表示蓝色。
  5. thickness(可选)
    • 类型:int
    • 作用:轮廓线宽:
      • 若为 -1,填充轮廓内部(实心);
      • 若为 1 或更大值,表示线条像素宽度。
  6. lineType(可选)
    • 类型:int
    • 作用:线条类型,默认为 cv2.LINE_8(8-connected),可选:
      • cv2.LINE_4(4-connected)
      • cv2.LINE_AA(抗锯齿)。
  7. hierarchy(可选)
    • 类型:numpy.ndarray
    • 作用:轮廓层级关系(来自 findContours 的 hierarchy),用于控制嵌套轮廓的绘制。
  8. maxLevel(可选)
    • 类型:int
    • 作用:最大层级深度:
      • 若为 0,仅绘制指定轮廓;
      • 若为 1,绘制当前轮廓及其嵌套轮廓;
      • 若为 2,绘制到更深层级(依此类推)。
  9. offset(可选)
    • 类型:Tuple[int, int]
    • 作用:轮廓点坐标的偏移量(例如 (10, 20) 将所有点向右平移 10 像素,向下平移 20 像素)。

示例代码:

import cv2
import numpy as np

# 创建黑色背景图像
img = np.zeros((300, 300, 3), dtype=np.uint8)

# 生成一个矩形轮廓
contour = np.array([[[50, 50]], [[50, 200]], [[200, 200]], [[200, 50]]], dtype=np.int32)
contours = [contour]

# 绘制轮廓(绿色,线宽 2)
cv2.drawContours(
    img, 
    contours, 
    contourIdx=-1, 
    color=(0, 255, 0), 
    thickness=2,
    lineType=cv2.LINE_AA
)

cv2.imshow("Result", img)
cv2.waitKey(0)

image-20250328150621131

注意事项:

  • 如果 thickness=-1,会填充轮廓内部(类似 cv2.fillPoly)。
  • 轮廓点需为整数类型(np.int32 或 np.int64)。
  • 输入图像通常是 findContours 后的二值图,但 drawContours 本身可作用于任何彩色/灰度图像。
import cv2
import numpy as np


def preprocess_image(image_path):
    """图像预处理:转为灰度图并二值化"""
    img = cv2.imread(image_path)
    if img is None:
        print("无法加载图像,请检查文件路径")
        return None, None

    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    binary = cv2.adaptiveThreshold(gray, 255,
                                   cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
                                   cv2.THRESH_BINARY_INV, 11, 2)
    return img, binary


def find_contours_demo(image_path):
    """轮廓检测与绘制演示"""
    original, binary = preprocess_image(image_path)
    if original is None:
        return

    contours, hierarchy = cv2.findContours(binary,
                                           cv2.RETR_TREE,
                                           cv2.CHAIN_APPROX_SIMPLE)

    contour_img = original.copy()
    cv2.drawContours(contour_img, contours, -1, (0, 255, 0), 2)

    cv2.imshow("Original", original)
    cv2.imshow("Binary", binary)
    cv2.imshow("Contours", contour_img)

    analyze_contours(original.copy(), contours)
    cv2.waitKey(0)
    cv2.destroyAllWindows()


def analyze_contours(img, contours):
    """轮廓特征分析"""
    filtered_contours = [c for c in contours if cv2.contourArea(c) > 1000]

    for i, cnt in enumerate(filtered_contours):
        area = cv2.contourArea(cnt)
        perimeter = cv2.arcLength(cnt, True)
        x, y, w, h = cv2.boundingRect(cnt)

        rect = cv2.minAreaRect(cnt)
        box = cv2.boxPoints(rect)
        box = np.int32(box)  # 修改这里:np.int0 → np.int32

        (x_c, y_c), radius = cv2.minEnclosingCircle(cnt)

        cv2.drawContours(img, [box], 0, (255, 0, 0), 2)
        cv2.circle(img, (int(x_c), int(y_c)), int(radius), (0, 0, 255), 2)

        epsilon = 0.02 * perimeter
        approx = cv2.approxPolyDP(cnt, epsilon, True)

        vertices = len(approx)
        shape = ""
        if vertices == 3:
            shape = "TRI"
        elif vertices == 4:
            aspect_ratio = w / float(h)
            shape = "SQR" if 0.95 <= aspect_ratio <= 1.05 else "RECT"
        elif vertices > 8:
            shape = "CIRCLE"
        else:
            shape = f"POLY{vertices}"

        M = cv2.moments(cnt)
        if M["m00"] != 0:
            cX = int(M["m10"] / M["m00"])
            cY = int(M["m01"] / M["m00"])
            cv2.putText(img, f"{shape} A:{area:.0f}", (cX - 50, cY),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 2)

    cv2.imshow("Contour Analysis", img)


def main():
    print("OpenCV 轮廓检测演示")
    print("请选择模式:")
    print("1: 图像文件轮廓检测")
    print("2: 实时摄像头轮廓检测")

    choice = input("请输入选项 (1-2): ")

    if choice == '1':
        image_path = input("请输入图像路径(或直接回车使用默认图像): ").strip()
        if not image_path:
            image_path = "image.jpg"  # 确保项目目录下有这个图像文件
        find_contours_demo(image_path)
    elif choice == '2':
        print("实时检测功能需要摄像头支持")
    else:
        print("无效输入")


if __name__ == "__main__":
    main()

关键代码解析

1. 图像预处理

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
binary = cv2.adaptiveThreshold(gray, 255, 
                             cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
                             cv2.THRESH_BINARY_INV, 11, 2)

使用自适应阈值二值化,比固定阈值更能适应光照变化,THRESH_BINARY_INV反转结果使前景为白色。

2. 查找轮廓

contours, hierarchy = cv2.findContours(binary, 
                                     cv2.RETR_TREE, 
                                     cv2.CHAIN_APPROX_SIMPLE)
  • RETR_TREE:获取轮廓的完整层级关系
  • CHAIN_APPROX_SIMPLE:压缩水平、垂直和对角线段,只保留端点

3. 轮廓分析

# 面积过滤
filtered_contours = [c for c in contours if cv2.contourArea(c) > 1000]

# 几何特征计算
area = cv2.contourArea(cnt)
perimeter = cv2.arcLength(cnt, True)
x, y, w, h = cv2.boundingRect(cnt)

# 形状识别
epsilon = 0.02 * perimeter
approx = cv2.approxPolyDP(cnt, epsilon, True)
vertices = len(approx)

轮廓检测的注意事项

  1. 预处理至关重要:
    • 高斯模糊减少噪声影响
    • 自适应阈值处理光照不均
    • 考虑使用Canny边缘检测作为替代方案
  2. 参数调优建议:
    • 面积阈值根据实际对象大小调整
    • approxPolyDP的epsilon值影响形状识别精度
    • 对于复杂场景,考虑使用RETR_EXTERNAL只检测最外层轮廓
  3. 性能优化:
    • 实时应用中限制检测的轮廓数量
    • 对ROI区域进行处理而非整幅图像
    • 适当降低图像分辨率

扩展应用

  1. 运动检测:结合背景减除和轮廓分析
  2. 手势识别:分析手部轮廓特征
  3. 工业检测:通过轮廓匹配识别缺陷
  4. 文档分析:提取文本区域轮廓

5-15 轮廓的面积与周长

轮廓的面积和周长

轮廓的面积:

contourArea(contour)

contour:轮廓

轮廓的周长

arcLength(curve, closed)

curve:轮廓 closed:是否是闭合的轮廓

功能说明

  1. 读取一张图像(或创建一个空白图像并绘制形状)。
  2. 使用 cv2.findContours() 检测轮廓。
  3. 计算每个轮廓的 面积(cv2.contourArea()) 和 周长(cv2.arcLength())。
  4. 在图像上绘制轮廓,并标注面积和周长。

完整代码

import cv2
import numpy as np

# 1. 创建一个空白图像(或读取一张图像)
img = np.zeros((400, 400, 3), dtype=np.uint8)  # 黑色背景

# 2. 在图像上绘制一个矩形和一个圆形(用于生成轮廓)
cv2.rectangle(img, (50, 50), (200, 200), (0, 255, 0), 2)  # 绿色矩形
cv2.circle(img, (300, 150), 80, (0, 0, 255), 2)  # 红色圆形

# 3. 转换为灰度图并二值化(便于 findContours 处理)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 1, 255, cv2.THRESH_BINARY)

# 4. 查找轮廓
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

# 5. 计算每个轮廓的面积和周长,并绘制结果
for i, contour in enumerate(contours):
    # 计算面积
    area = cv2.contourArea(contour)
    # 计算周长(closed=True 表示闭合轮廓)
    perimeter = cv2.arcLength(contour, closed=True)

    # 绘制轮廓(蓝色)
    cv2.drawContours(img, [contour], -1, (255, 0, 0), 2)

    # 在轮廓中心显示面积和周长
    M = cv2.moments(contour)
    if M["m00"] != 0:
        cx = int(M["m10"] / M["m00"])
        cy = int(M["m01"] / M["m00"])
        cv2.putText(img, f"Area: {area:.1f}", (cx - 50, cy - 20), 
                   cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
        cv2.putText(img, f"Perimeter: {perimeter:.1f}", (cx - 50, cy + 20), 
                   cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)

# 6. 显示结果
cv2.imshow("Contours with Area and Perimeter", img)
cv2.waitKey(0)
cv2.destroyAllWindows()

代码解析

  1. 创建图像
    • 生成一个黑色背景图像,并绘制一个 矩形 和一个 圆形(用于测试轮廓检测)。
  2. 二值化处理
    • cv2.threshold() 将图像转换为二值图(黑白),便于 findContours 检测轮廓。
  3. 查找轮廓
    • cv2.findContours() 返回检测到的轮廓列表(contours)。
  4. 计算面积和周长
    • cv2.contourArea(contour):计算轮廓的面积(像素单位)。
    • cv2.arcLength(contour, closed=True):计算轮廓的周长(closed=True 表示闭合轮廓)。
  5. 绘制轮廓和文本
    • cv2.drawContours() 绘制轮廓(蓝色)。
    • cv2.moments() 计算轮廓的质心(用于放置文本)。
    • cv2.putText() 在图像上显示 面积 和 周长。
  6. 显示结果
    • 最终图像会显示轮廓及其面积、周长信息。

输出示例

image-20250328151144247

(实际运行会显示轮廓、面积和周长数值)


关键点

  • cv2.contourArea():适用于闭合轮廓,计算其面积(单位:像素)。
  • cv2.arcLength():
    • closed=True:计算闭合轮廓的周长(如矩形、圆形)。
    • closed=False:计算曲线长度(如未闭合的折线)。
  • cv2.moments():用于计算轮廓的几何中心(方便放置文本)。

你可以替换 img 为任意图像(如 img = cv2.imread("your_image.jpg"))来测试真实场景。

5-16 ROI

Region of Image (ROI) 一个图像中的某一个区域

Numpy获取子矩阵

  • [y1:y2,x1:x2]
  • [:,:], 对整个图像进行变更

功能说明

  1. 读取一张图像。
  2. 使用 NumPy 切片 提取 ROI(子矩阵)。
  3. 对 ROI 进行修改(如绘制矩形、改变颜色)。
  4. 显示原始图像和修改后的图像。

完整代码

import cv2
import numpy as np

# 1. 读取图像
img = cv2.imread("iamge.jpg")  # 替换为你的图片路径
if img is None:
    print("Error: 图片未加载,请检查路径!")
    exit()

# 2. 提取 ROI(使用 NumPy 切片 [y1:y2, x1:x2])
roi = img[100:300, 200:400]  # 提取 y=100~300, x=200~400 的区域

# 3. 修改 ROI(示例:绘制绿色矩形)
cv2.rectangle(roi, (50, 50), (150, 150), (0, 255, 0), 2)  # 在 ROI 内部绘制

# 4. 显示结果
cv2.imshow("Original Image", img)
cv2.imshow("ROI (Modified)", roi)
cv2.waitKey(0)
cv2.destroyAllWindows()

代码解析

  1. 读取图像
    • cv2.imread() 加载图像(确保路径正确)。
  2. 提取 ROI
    • NumPy 切片语法:img[y1:y2, x1:x2]
      • y1:y2:高度范围(行)。
      • x1:x2:宽度范围(列)。
    • 示例:img[100:300, 200:400] 提取从第 100 行到 300 行、第 200 列到 400 列的子区域。
  3. 修改 ROI
    • 直接操作 roi 会修改原图(因为 NumPy 数组是引用传递)。
    • 示例代码在 ROI 内部绘制了一个绿色矩形。
  4. 显示结果
    • cv2.imshow() 展示原始图像和修改后的 ROI。

关键点

  • ROI 是原图的视图(不是副本),修改 ROI 会直接影响原图。
  • 若需独立副本,使用 roi_copy = roi.copy()。
  • ROI 的应用场景:
    • 局部图像处理(如人脸检测时只处理人脸区域)。
    • 图像拼接(如将小图嵌入到大图的指定位置)。

扩展示例:替换 ROI

# 将 ROI 替换为纯红色
roi[:, :] = [0, 0, 255]  # BGR 格式 (红色)

# 或者用另一张图像替换 ROI
small_img = cv2.imread("small_logo.png")
roi[:small_img.shape[0], :small_img.shape[1]] = small_img  # 确保尺寸匹配

注意事项

  • 边界检查:确保 y1:y2 和 x1:x2 不超出图像尺寸。
  • 性能优化:直接操作 NumPy 数组比逐像素修改更高效。

5-17 边缘检测Canny

Canny

  • 使用5*5的高斯滤波对原有图像进行降噪
  • 计算图像梯度的方向(0度/45度/90度/135度)
  • 取局部最大值
  • 阈值计算

image-20250331141621120

Canny API

  • Canny(Img、MinVal,MaxVal....)

Demo

import cv2
import numpy as np


def canny_edge_detection(image_path, min_val=50, max_val=150):
    """
    Canny边缘检测演示

    参数:
        image_path: 输入图像路径
        min_val: 低阈值
        max_val: 高阈值
    """
    # 1. 读取图像并转换为灰度图
    img = cv2.imread(image_path)
    if img is None:
        print("无法加载图像,请检查路径")
        return

    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    # 2. 使用5x5高斯滤波降噪
    blurred = cv2.GaussianBlur(gray, (5, 5), 0)

    # 3. 应用Canny边缘检测
    edges = cv2.Canny(blurred, min_val, max_val)

    # 4. 显示结果
    cv2.imshow('Original Image', img)
    cv2.imshow('Canny Edges', edges)

    # 保存结果
    cv2.imwrite('image2.jpg', edges)

    cv2.waitKey(0)
    cv2.destroyAllWindows()


if __name__ == "__main__":
    # 使用示例
    image_path = 'image.jpg'  # 替换为你的图像路径
    canny_edge_detection(image_path, 50, 150)
  1. 图像读取与灰度转换:
  • 首先读取输入图像并转换为灰度图,因为Canny算法通常在灰度图像上操作
  1. 高斯滤波:
  • 使用5x5高斯核进行滤波,消除噪声
  1. Canny边缘检测:
  • cv2.Canny()函数接受三个主要参数:
    • 输入图像(经过高斯模糊后的图像)
    • 低阈值(minVal):用于边缘连接
    • 高阈值(maxVal):用于初始边缘检测
  • 建议高低阈值比在1:2或1:3之间
  1. 结果显示与保存:
  • 显示原始图像和边缘检测结果
  • 将边缘检测结果保存为文件

使用建议:

  1. 对于不同的图像,可以调整minVal和maxVal参数以获得最佳效果
  2. 可以先尝试1:3的阈值比(如50:150),然后根据效果调整
  3. 对于噪声较多的图像,可以增大高斯核大小(如7x7)

重点

  • 传统计算机视觉目前主要的作用是 预处理
  • 它处理图像或视频的基本流程是怎样的

image-20250331143138758

**第6章 人工智能必知必会的数学知识

6-1 向量

必知必会的数学知识

  • 线性代数
  • 高数(求导部分)

向量

  • 什么是向量

    • 向量有两个重要属性:方向和长度
  • 向量的特性

    • 当两个向量方向相同,长度相同,表示两向量相等
    • 我们不关心向量的起始点
  • 标量与向量的区别

    • 标量只是一个值,它没有方向性,如5
    • 而向量不仅有长度,还有方向
  • 单位向量

    • 向量的长度:|| a ||
    • 单位向量:长度为1的向量
  • 向量的代数表示法

    image-20250331145136103

6-2 向量的基本运算(加法与点乘)

向量加法-三角形法则

image-20250331145425818

image-20250331145453625

向量加法-平行四边行法则

image-20250331145548233

向量点乘

image-20250331145704017

向量点乘的性质

image-20250331145815704

计算向量点乘-代数法

image-20250331145859177

6-3 运算-向量的基本运算(叉乘)

向量的叉乘运算

1. 定义

对于三维向量 a = (a₁, a₂, a₃) 和 b = (b₁, b₂, b₃),它们的叉乘结果为: $$ \mathbf{a} \times \mathbf{b} = \begin{vmatrix} \mathbf{i} & \mathbf{j} & \mathbf{k} \ a_1 & a_2 & a_3 \ b_1 & b_2 & b_3 \ \end{vmatrix} = (a_2b_3 - a_3b_2,\ a_3b_1 - a_1b_3,\ a_1b_2 - a_2b_1) $$

2. 几何意义

方向特性

  • 垂直性: 叉乘得到的结果向量始终垂直于原始两个向量a和b所在的平面。
  • 方向判定(右手定则):
    • 伸出右手,让四指从第一个向量a的方向自然弯曲指向第二个向量b的方向
    • 此时大拇指伸直所指的方向就是叉乘结果向量a×b的方向
  • 记忆技巧:
    • 可以想象用"开瓶器"的动作:
      • 旋转方向:a转向b
      • 前进方向:叉乘结果方向
    • 坐标系中i×j=k就是典型例子(符合右手定则)

image-20250407091850080

模长特性 $$ |\mathbf{a} \times \mathbf{b}| = |\mathbf{a}| \cdot |\mathbf{b}| \cdot \sin\theta $$

  • 几何意义:|a × b| 等于以 a 和 b 为邻边的平行四边形的面积
  • 三角形面积为模长的一半

3. 运算性质

反交换律 $$ \mathbf{a} \times \mathbf{b} = -(\mathbf{b} \times \mathbf{a}) $$

分配律 $$ \mathbf{a} \times (\mathbf{b} + \mathbf{c}) = \mathbf{a} \times \mathbf{b} + \mathbf{a} \times \mathbf{c} $$

数乘结合性 $$ (k\mathbf{a}) \times \mathbf{b} = \mathbf{a} \times (k\mathbf{b}) = k(\mathbf{a} \times \mathbf{b}) $$

平行判定 $$ \mathbf{a} \parallel \mathbf{b} \iff \mathbf{a} \times \mathbf{b} = \mathbf{0} $$

4. 计算方法

行列式法(推荐) $$ \mathbf{a} \times \mathbf{b} = \begin{vmatrix} \mathbf{i} & \mathbf{j} & \mathbf{k} \ a_1 & a_2 & a_3 \ b_1 & b_2 & b_3 \ \end{vmatrix} $$

分量计算法 $$ \begin{cases} x = a_2b_3 - a_3b_2 \ y = a_3b_1 - a_1b_3 \ z = a_1b_2 - a_2b_1 \end{cases} $$

5. 典型应用

几何应用

  • 求平面法向量
  • 计算多边形面积
  • 判断点与直线的位置关系

物理应用

  • 力矩计算(扭矩) 力矩的计算公式为: τ = r × F 其中:
    • r 是力的作用点到转轴的位移向量
    • F 是作用力向量
    • τ 是产生的力矩向量 力矩的方向遵循右手定则,表示物体绕轴旋转的趋势。
  • 角动量计算 角动量的计算公式为: L = r × p 其中:
    • r 是质点的位置向量
    • p = mv 是质点的动量向量(m为质量,v为速度)
    • L 是角动量向量 角动量的方向表示旋转轴的方向,大小表示旋转的强度。

6. 计算实例

给定向量: $$ \mathbf{a} = (1, 2, 3),\quad \mathbf{b} = (4, 5, 6) $$

计算过程: $$ \begin{aligned} x &= 2×6 - 3×5 = 12 - 15 = -3 \ y &= 3×4 - 1×6 = 12 - 6 = 6 \ z &= 1×5 - 2×4 = 5 - 8 = -3 \ \end{aligned} $$

最终结果: $$ \mathbf{a} \times \mathbf{b} = (-3, 6, -3) $$

6-4 矩阵的基本运算

什么是矩阵:

image-20250407092521867

什么是单位矩阵:

  • 对角线为1,其它项为 0,它是一个方阵,行与列相同

image-20250407092816808

  • 单位矩阵用 Ⅰ 或者 E 表示

  • 任何非0矩阵与单位矩阵相乘,不会发生变化

逆矩阵

矩阵 A 的逆矩阵记作 A-1image-20250407093028089

矩阵转置

  • 交换行、列

    • image-20250407093107095
  • 矩阵转置的性质

    image-20250407093144896

  • 矩阵加法

    • 同形矩阵(同行、同列)可以进行加法运算

      image-20250407093243379

  • 矩阵的乘法

    image-20250407093352711

image-20250407093449161

矩阵的性质

  • 矩阵乘法没有交换率 AB 不等于 BA
  • 支持结合率(AB)C = A(BC)
  • 支持分配率A(B+C)= AB + AC

6-5 2D变换

  • 2D变换
  • 齐次坐标
  • 3D变换

为什么需要变换?

image-20250407094006146

缩放

image-20250407094237103

image-20250407094307281

镜像翻转

image-20250407094343701

切变

image-20250407094435964

旋转

image-20250407094713024

平移

image-20250407094824521

6-6 齐次坐标

齐次坐标

  • 平移的问题

    • 通常的变换直接乘以2D线性变換矩阵即可
    • 平移是个例外
  • 平移变换是否可以通过矩阵乘法实现?

齐次坐标

在二维的基础上增加一个维度(w)对于每一点由原来的(xy)变成(x,y,w),w通常设置为1

线性变换矩阵由2x2变成3x3

image-20250407095122418

平移可以变成矩阵乘法

image-20250407095200716

image-20250407095248455

使用齐次坐标的线性变换

image-20250407095403685

组合变换

image-20250407095525510

结论

  • 矩阵的乘法不支持交换律

  • 复杂的变换可以拆解成多个基本变换

为了将平移转换成矩阵乘法,引入了齐次坐标。复杂的变换可以由多个基本变换组合而成。

6-7 利用齐次坐标实现各种3D变换

3D变换

3D空间中的点

  • 3D空间中的一个点(x,y,z)
  • 齐次坐标点(x,y,z,w)
  • 范化的齐次坐标点(x/w,y/w,z/w,w/w)

3D空间中的齐次坐标矩阵

image-20250407095938807

3D缩放

image-20250407100253883

3D平移

image-20250407100348655

3D旋转

image-20250407100430063

旋转的逆等于转置

image-20250407100652255

小结

3D空间的齐次坐标矩阵。3D空间中的基本变换,缩放、平移、旋转

6-8 求导

导数的定义

image-20250407102113216

根据定义对f(x)= x"求导

b"-a"的多项式

image-20250407104401607

根据定义对f(x)=x"求导

image-20250407104606653

根据定义对f(x)= ex求导

image-20250407105207241

常见导数公式表

image-20250407105230263

6-9 链式求导与偏导

求导的复合运算与偏导

  • 函数和、差、积、商的求导法则

image-20250407110021564

链式求导法则

image-20250407110224513

偏导

一阶导是求斜率,偏导的意义是什么呢?

偏导的意义

  • 偏导数是多变量函数的导数,如f(x,y)
  • 它表示某一个变量的对于函数的变化率

对多元函数中每一个变量求导

  • 对多元函数中每一个变量求导
  • 把非求导的变量看作常数

例子

image-20250407111001450

6-10 张量

什么是张量?

0维张量称为标量,如5,6

比如我们前面讲的标量、向量、矩阵,这些都属于张量

  • 0维张量称为标量,如5,6
  • 1维张量称为向量,如(1,2,3)
  • 2维张量称为矩阵
  • 3维以上的张量称为高维张量,如图像、视频等

张量的作用

  • 张量是包含数据的容器
  • 张量可以用于表示输入数据、模型参数和输出数据
  • 张量支持各种数学运算,如加法、乘法等
  • 张量运算可以在GPU上并行处理,可以大大提高计算效率

为什么要引入张量

  • 主要是为了处理和表示高维数据
  • 张量可以扩展到任意维度
  • 适用于更复杂的数据结构和计算需求
  • 张量提供了统一的框架,使得各种计算更加简洁和高效
  • 更好的利用GPU

不同库中的张量

  • 不同的库有各自不同的张量类型
  • 如PyTorch和Tensorflow中的张量类型是不同的
  • 不过它们的功能都很类似,与NumPy的ndarray很像
  • 不同在于,ndarray在CPU运行,且不支持自动求导

6-11 本章小结

总结

总的来说,深度学习用到的数学知识并不难、线性代数用的较多、高数只用了一点求导相关的内容

  • 向量:加法、点乘、叉乘
  • 矩阵:加法、乘法(点乘、叉乘)
  • 图像的基本变换:旋转、缩放、平移、齐次坐标….
  • 张量:在GPU中运行、提供了方便、统一的接口,自动求导
  • 求导:基本求导、链式求导、偏导

第7章 深度学习必备的基础知识

7-1 导学

image-20250407133155717

  • 清楚深度学习、机器学习与人工智能的区别

  • 了解深度神经网络与线性回归

  • 清楚深度神经网络的构建过程

  • 明白深度神经网络中的多种经典算法

  • 知道如何调整参数提高模型的精确率

  • 学习深度学习的各种基础知识

  • 使用PyTorch/Tensorflow搭建深度神经网络

  • 实现手写字的识别

  • 清楚CNN卷积神经网络

  • 对YOLO进行重点讲解,并能自己训练YOLO模型

  • 剖析大语言模型的工作原理

7-2 人工智能、机器学习与深度学习的关系

人工智能,机器学习与深度学习

  • 人工智能:让机器可以像人一样完成任务的智能系统
  • 机器学习:让机器从数据中学习,实现人工智能
  • 深度学习:通过多层神经网络从数据中学习的人工智能

image-20250331152546878

深度学习本质是为解决某个问题找到一个函数

image-20250331152656241

函数是如何被找到的呢?

  • 通过大量的数据送给深度神经网络
  • 找到每个神经元的权重(复杂的过程)
  • 神经网络+各神经元的权重就是函数
  • 各神经元的权重的集合称为模型

7-3 神经元与神经网络

神经元与神经网络

  • 深度神经网络

    • 深度神经网络是从生物学中的神经网络借鉴来的
    • 两者的概念基本相同,如神经元,多个层级….
    • 但两者有本质的不同
  • 神经元

    • 它是构成神经网络的基本单元
    • 它的作用是接收、处理和传递信息
  • 最简单的神经网络-线性回归

    image-20250331153311596

  • 多维特征线性回归

    image-20250331153419703

  • 标准神经网络

image-20250407134436443

7-4 监督学习与无监督学习

数据的种类

结构化数据:房子大小、卧室数量(数据库中的数据)

非结构化数据:图像、音频、视频、文字

计算机善于处理结构化数据,人类善于处理非结构化数据

深度学习是从非结构化数据中学习,找到解决问题的函数

什么是监督学习?

每个训练样本都有标签(答案)

训练时,将预测值与标签进行比较

根据代价函数的结果调整参数权重

目标是用训练模型预测未见过数据的标签

主要应用有**:图像识别(分类),股票价格预测(回归)**

什么是无监督学习?.

从未标注的数据中学习**(数据集中的数据没有标签)**

工作流程如下:

image-20250331154435508

无监督学习

无监督学习的目标是发现数据中的结构、模式和分布

主要用于图像去噪,社交网络分析(聚类)等场景

半监督学习

使用少量的标记数据和大量未标记数据进行训练

目标是利用未标注数据提高训练效率

如通过标注数据训练模型,给未标注数据打标签

主要应用包括文本分类、图像识别等

强化学习

通过交互来学习最佳行为或策略,如给某种奖励

大语言模型中的点赞就属于强化学习

通过与人的交互,让大语言模型的输出更符合人的行为

7-5 数据集的划分

数据集的划分

深度学习是数据驱动的

我们需要利用大量的数据来训练模型

数据集:训练数据集、验证数据集、测试数据集

  • 训练、验证与测试数据集
    • 训练数据集用于模型的训练
    • 验证数据集用于评估模型的性能,防止过拟合
    • 测试数据集用于模型的最终评估
    • 数据划分的比例:8:1:1或6:2:2

需要注意的点

  • 确保数据集的划分是随机的,以避免数据集之间存在偏差
  • 确保每个数据集中的样本都覆盖不同的类别和特征
  • 避免训练过程中用于验证数据集或测试数据集中的数据
  • 一致的数据预处理

7-6 过拟合、欠拟合与代价函数

过拟合、欠拟合与代价函数

拟合与代价函数

image-20250407135204655

结果越小,拟合的效果越好

过拟合

过拟合:在训练数据集上表现很好,测试集表现很差

image-20250331155943061

原因1:网络过于复杂原因2:数据量不够

欠拟合

欠拟合:在训练数据集上表现就很差

image-20250331160127695

网络复杂性太低训练数据不够

  • 清楚什么是过拟合,什么是欠拟合
  • 知道过拟合和欠拟合的原因是什么
  • 清楚什么是损失函数和代价函数
  • 不同的网络其损失函数和代价函数是不同的

7-7 代价函数的意义

模型与代价函数的关系

image-20250331160524981

代价函数的3D图

image-20250331160627959

代价函数的等高图

image-20250331160657984

通过代价函数获得最佳模型

image-20250331162122582

简化线性回归函数,可知其代价函数是一个抛物线

推理可知,标准线性回归的代价函数是一个碗状型

要想获得好的线性回归模型,只需求代价函数的最小值

推而广之,所有的模型都是求代价函数的最小值

7-8 线性回归代价函数的导数

线性回归代价函数的导数

image-20250407150339394

已知条件

image-20250407150535802

推导过程

image-20250407150737090

7-9 梯度下降

梯度下降

  • 什么是梯度下降?
  • 用来找到函数局部最小值的算法

代价函数的最小值

  • 当找到代价函数的最小值,就找到了最佳的w和b
  • 找到了最佳的w和b,就找到了线性回归的最佳模型
  • 对于深度学习网络,也是要找到其代价函数的最小值
  • 使用的方法就是梯度下降

梯度下降的目标

image-20250407151133313

梯度下降的过程

image-20250407151244459

梯度下降函数

image-20250407151433652

注意的点

  • w与b要同时更新

image-20250407151625535

为什么梯度下降可以找到局部最小值?

image-20250407153237912

小结

  • 梯度下降就是找函数局部最小值的算法
  • 深度学习需要使用梯度下降找代价函数的局部最小值
  • 找到代价函数的局部最小值就可以找最模型的最佳参数
  • 梯度下降算法就是让各参数减(学习率*参数的偏导数)
  • 学习率指明了更新步伐的大小,偏导数指明了移动的方向

7-10 学习率

学习率:学习率的作用就是控制我们梯度下降时,所迈出的步伐的大小

学习率过小的情况

image-20250407155134437

  • 会导致训练一个模型会花费很长时间

学习率过大的情况

image-20250407155320799

  • 梯度下降始终无法找到局部最小值 而且训练的模型效果越来越差

学习率要设置在:0.1 - 0.01之间,效率最高的

固定的学习率不影响梯度收敛

image-20250407155452910

总结:

  • 学习率不应设置的太小,否则模型训练需要好很久的时间
  • 学习率不应设置的太大,否则很难找到代价函数的最小值
  • 学习率通常设置为0.1 ~ 0.01之间
  • 当梯度消失后,模型训练的参数不再发生变化

7-11 逻辑回归

逻辑回归

  • 线性回归是预测一个连续的值,而逻辑回归是预测二分类

  • 即线性回归的结果是一条线,逻辑回归输出的是0或1

  • 逻辑回归通过激活函数(sigmoid)将线性结果转为0到1的概率

激活函数

  • 激活函数在每个神经元中引入了非线性
  • 如果没有激活函数,无论多深的网络都是线性的
  • 线性网络,无论多深,都无法处理复杂的问题
  • 激活函数有很多:如sigmoid,ReLU,softmax...

逻辑回归精典案例:图片中是否有猫

image-20250407155847650

图片的表述

image-20250407160008320

样本的表述

image-20250407160121593

以矩阵的方式表式多个样本

image-20250407160154954

小结

  • 清楚了什么是逻辑回归,并知道它与线性回归的区别
  • 知道了在深度学习中如何将图片输入给神经网络
  • 了解了如何将多张图片转成矩阵送给神经网络

7-12 sigmoid激活函数

逻辑回归模型函数

image-20250407160454701

Sigmoid函数的由来

  • 将线性回归的结果送给sigmoid函数,就可以输出1的概率
  • sigmoid函数就是将任意值映射到0~1的范围内

image-20250407160603675

看个例子

image-20250407160712989

小结

image-20250407160752130

7-13 逻辑回归的代价函数

线性回归代价函数的曲线图

image-20250407162503287

为什么逻辑回归不能再使用原来的代价函数

image-20250407162542454

逻辑回归的损失函数

image-20250407162707030

逻辑回归损失函数的曲线图

image-20250407162937234

交叉熵损失函数

image-20250407163117577

逻辑回归的代价函数

image-20250407163158562

小结

  • 线性回归的代价函数不能满足逻辑回归的要求

  • 科学家们为逻辑回归找到了一个新的代价函数

    image-20250407163321183

7-14 逻辑回归的梯度下降

逻辑回归的梯度下降

已知条件

image-20250408085257257

疑问:为什么线性回归与逻辑回归关于w,b的偏导数公式一样?

image-20250408085346967

巧合

  • 形式一样,但含义不同

image-20250408085540520

7-15 逻辑回归代价函数关于w和b偏导后的公式证明

逻辑回归代价函数关于w、b的偏导证明

推导过程

image-20250408085752359

链式法则

image-20250408085859399

求L关于σ的偏导

image-20250408085940035

推导过程

image-20250408090059387

image-20250408090230868

image-20250408090353516

image-20250408090604654

image-20250408090622904

image-20250408090712812

7-16 深度神经网络与前向传播

深层神经网络的前向传播

  • 神经网络中不同层的意义

image-20250408090855464

  • 神经网络的工作原理

image-20250408091349309

深层神经网络的结构

image-20250408091445905

某个隐藏层做的事儿

image-20250408091603998

前向传播

image-20250408091752614

小结

  • 我们对深度神经网络前三层的作用有了一个大体的了解
  • 知道了神经网络的基本工作原理
  • 对数学公式中的上标、下标的含义要理解清楚
  • 清楚神经网络的前向传播机制

7-17 多种激活函数

多种激活函数

  • Sigmoid
  • Tanh
  • ReLU(Rectified Lieaner Unit)
  • Leaky ReLU
  • Softmax

Sigmoid

image-20250408092416777

Tanh

image-20250408092640043

ReLu

image-20250408092819286

使用ReLU的深度神经网络

image-20250408092946652

Leaky ReLU

image-20250408093141711

Softmax

  • 用于多分类问题,它可以将输入转换成概率分布

  • Softmax输出种类的概率总和1

  • 它用在输出层

image-20250408093246369

7-18 反向传播与计算图

反向传播与计算图

  • 前向传播与激活函数告诉你如何构建深度神经网络
  • 反向传播则用于计算价值函数关于网络中每个参数的梯度
  • 有了每个参数的梯度就可以在训练的过程中更新每个参数
  • 计算图则是反向传播计算梯度的高效工具

计算图的构造

  • 可以根据数学公式构造计算图,并根据链式法则计算梯度

image-20250408093627885

通过计算图求导

image-20250408094101336

结论

进行加法操作时,导数是反向传播前的导数

进行乘法操作时,导数是另一乘法数

image-20250408094308212

小结

  • 根据数学公式可以建立计算图
  • 根据计算图反向计算可以求得各变量的偏导数
  • 计算图最后一项的导数需要手工计算

7-19 前向传播与反向传播的完整过程

前向传播与反向传播的完整过程

image-20250408094420144

第8章 训练优化深度神经网络模型的方法

8-1 导学

为什么呢?

  • 不确定性:相同算法、数据,不同实验结果不同
  • 理论不完善:很多现象无法解释清其原理
  • 过程复杂:训练大型神经网络需要大量的数据、算力、时间
  • 成本高昂

确实有一些方法、可以提高“炼丹”的成功率

主要内容

  • 神经网络向量化实现
  • 正则化
    • 正则化2
    • Dropout
  • 输入数据归一化
  • 权重随机初始化
  • 批量梯度下降
    • 全批量梯度下降
    • 小批量梯度下降
  • 代价函数参数优化

8-2 向量化与矩阵化

向量/矩阵化

线性回归向量化的实现

image-20250408102643901

逻辑回归的向量化实现

image-20250408102848574

矩阵化-只有一个神经元

image-20250408102922454

矩阵化-多神经元

image-20250408103043964

逻辑回归向量化梯度输出

image-20250408103123422

image-20250408103234637

去掉for循环

image-20250408103308726

8-3 L2正则化

正则化

什么是正则化?

  • 为了让模型更"规则"或"平滑"、从而得到更稳定、泛化能力更强的模型

如何理解“规则”与“平滑”?

  • 更“规则”

    • 指模型参数的分布更加均匀或简单
    • 减少模型的复杂性,避免过于复杂的函数关系
    • 使模型更容易理解和解释。
  • 更“平滑”

    • 指模型的输出对输入的变化不那么敏感
    • 减少模型在训练数据上的过拟合
    • 使模型的预测结果更加连续和稳定

正则化之L2

  • 解决过拟合问题
    • 解决过拟合时,可以增加数据量,或简化模型
    • L2正则化就是修改特征值的权重,如让权重为0
    • 间接简化了模型。防止了过拟合

例子

image-20250408104048629

正则化的核心思想

  • 让某些参数(w1、W2......)最小化,从而简化模型

image-20250408104339228

  • 让模型自己学习惩罚那些特征,从而防止过拟合

正则化的代价函数

image-20250408104514712

  • 入必须是大于0的值,一般设置为1
  • b不需要进行正则化

讨论一下λ

λ= 0,退回到了原来的模型

λ 特别大,所有参数基本为0

image-20250408104704571

正则化后的梯度下降

image-20250408104724720

image-20250408104746358

再看正则化的意义

image-20250408132609711

正则化:每次在更新参数的迭代中都会将w减少一点

小结

  • 正则化:每次更新参数前先减一点w,再做梯度下降
  • 它通过让某些权重变小(0),从而减少模型的复杂度
  • 它有一个专有名词:L2正则化

8-4 Dropout

Dropout正则化

  • L2正则化虽然可以有效降低过拟合,但也有局限性
  • 当神经网络特别复杂时,仍然会出现过拟合
  • 一种新的正则化提了出来,就是Dropout
  • Dropout是 辛顿 团队在2012年提出的
  • 当时辛顿团队训练的模型比较复杂又没有足够的算力和数据
  • 他们想到了让一些神经元不起作用这个办法
  • 由此产生了Dropout

Dropout如何工作

在神经网络的每一层随机让一些神经元失效

image-20250408133003427

小结

  • 对于输入层可以不使用Dropout
  • 隐藏层可以设置50%的比例
  • 如果模型较小,可以设置20%-40%
  • 输出层一般不设置Dropout

8-5 数据归一化处理

输入数据归一化的意义

image-20250408133309486

输入数据归一化的好处

  • 加快收敛速度:归一化使不同特征大小相似
  • 避免梯度消失:归一化可以防止输入范围过大或过小
  • 提高模型的稳定性和泛化性
  • 简化特征空间的映射关系

如何进行归一化处理

image-20250408133633320

为什么归一化会起到作用

image-20250408133841756

小结

  • 输入数据归一化,可以加快收敛速度
  • 输入数据归一化,可以避免梯度消失
  • 有的数据本身就已经做了归一化
  • 有的需要在导入数据时进行归一化
  • 如何进行归一化

8-6 初始化权重参数

权重初始化的方法

0初始化随机初始化Xavier初始化He初始化

0初始化

  • 将所有的权重初始化为0
  • 这种方式会带来严重问题,看个具体的例子
  • 当w=0时,wx+b=b,反向传播时梯度消失
  • 因此无法有效训练模型

随机初始化

  • 使用均匀分布或正态分布的随机数初始化
  • 当随机数没有选好,也会出现梯度消失

Xavier初始化

  • 专门针对sigmoid/tanh激活函数的初始化方法
  • 可以使每一层输入与输出方差尽量保持一致(推导略)
  • 有效防止梯度消失或爆炸
  • sigmoid/tanh很少使用,所以这种方法用的不多
  • Xavier.

image-20250408134736840

He初始化

  • He是由何凯明团队发明的
  • 它是专门针对ReLU激活函数的初始化方法

image-20250408134850083

8-7 全批量梯度下降

什么是全批量梯度下降?

  • 使用整个数据集计算梯度,之后再更新模型参数

举个例子

image-20250408135203496

image-20250408135227935

epochs

  • 它是一个超参,用于决定神经网络训练的轮次
  • 同样的数据为什么要训练多轮呢?
  • 每一轮得到的模型参数不同
  • 训练轮次过多容易出现过拟合,次数过少欠拟合

小结

  • 使用整个数据集计算梯度并更新模型参数
  • epochs决定了训练时的轮次
  • 优点是梯度准确
  • 缺点是计算量大,需要大内存

8-8 随机梯度下降与小批量梯度下降

随机梯度下降

  • 随机梯度下降也称SGD,每次使用一个样本进行梯度更新

随机梯度降的优缺点

  • 每次使用一个样本更新一次参数,计算量小
  • 它可以更快地进行参数调整,有可能加速训练过程
  • 无法充分利用GPU硬件
  • 收敛不稳定,容易出现较大的抖动

小批量梯度下降

  • 将数据集分成多个小批次,每个批次都更新一下模型参数

小批量的优点

  • 学习的更快:梯度下降的速度比全批量梯度下降的速度快
  • 收敛稳定:它比随机梯度下降收敛的更稳定
  • 利用GPU硬件更充分

小批量的缺点

  • 引入了新的超参数:mini-batch size
  • 如果超参数设置不好,会引起梯度下降的剧烈震荡

image-20250408135837423

小结

  • SGD是在数据规模大,GPU资源不充分时提出的
  • 目前大家普遍使用mini-batch,很少使用SGD
  • 全批量梯度下降只有在数据规模不大时使用

8-9 参数优化

参数优化

  • 动量梯度下降的作用

  • 通过加权平均,使得梯度下降的更平滑

举个例子

image-20250408140217039

如何做到的呢?

image-20250408140413465

动量各项的含义

image-20250408140505977

RMSprop的作用

  • 通过对梯度平方的指数加权平均来调整学习率,使得参数更新更加平滑和稳定。

如何做到的呢?

image-20250408140654260

对RMSprop的理解

image-20250408140746261

  • 这样就相当于变相可以调整学习率了
  • 但这不意味着我们可以随便设置学习率

Adam

  • 它是一种将动量算法与RMSProp 结合到一起使用的算法

小结

  • 动量梯度下降:提出了参考历史梯度来平滑梯度震荡
  • RMSProp:提出了通过历史梯度的倒数控制学习率控制震荡
  • Adam:动量梯度下降+ RMSProp

8-10 BatchNormalization

什么是Batch Normalization?

  • 输入数据归一化,可以加快收敛

image-20250408142109168

  • Batch normalization 是对隐藏层的数据归一化

Batch Normalization的作用

  • 与输入数据归一化类似,可以加快训练的收敛
  • 同时提升模型的稳定性
  • 在使用小批量梯度下降时使用
  • 用在神经网络的隐藏层

Batch Normalization 算法

image-20250408142609117

Batch normalization与线性回归

  • z = wx + b,其中w,b可以通过梯度下降学习

image-20250408142723438

Mini-batch与Batch normalization

image-20250408142954949

学习Y、β

image-20250408143106130

第9章 实战-手写字的识别

9-1 导学

9-2 Tensorflow与keras

Tensorflow的一点历史

  • Tensorflow是由Google Brain 2015年推出的
  • Google Brain刚成立时开发了DistBelief框架
  • 但DistBelief特别难用,学习成本极高
  • 2015年Jeff Dean决定开发新的深度学习框架Tensorflow

keras

  • Keras也是Google在2015推出的
  • 它提供了一组特别易用的深度学习API,可以支持多种后端+
  • 2017年Keras被整合到了Tensorflow中+
  • 此后Keras对其它后端的支持就越来越弱了

为什么Tensorflow要整合keras?

  • Tensorflow虽然功能强大,但使用起来还是复杂
  • Keras提供的API更易用
  • Keras积累了大量用户,更利于推广

如何使用tensorflow和keras

import tensorflow as tf
通过tensorflow可以直接访问keras接口
如tf.keras.datasets.mnist


import tensorflow as tf
mnist =tf.keras.datasets.mnist
print(mnist.load_data())

keras数据集

  • tf.keras.dataset.minist
  • cifar10/cifar100分类的数据集,如猫、狗等;32x32
  • Boston-housing,回归任务,预测房价
  • fashin-minist,时尚物品分类
  • Reuters路透社新闻,用于文本分类
  • IMDB用于情感分析,分析影评是正面还是负面情绪

9-3 手写字识别-加载mnist数据集

  • 使用load_data()可以将mnist数据加载进来
  • 如果你本地没有该数据集,它首先会下载

加载mnist数据集并显示它

import tensorflow as tf
import matplotlib.pyplot as plt

# 加载 MNIST 数据集
mnist = tf.keras.datasets.mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data("/Users/programmer3.cc.vwed/PycharmProjects/pythonProject4/mnist.npz")


def show_image(x_train, y_train, x_test, y_test):
    fig, axes = plt.subplots(1, 2, figsize=(8, 4))
    axes[0].set_title('Label: {}'.format(y_train[0]))
    axes[0].imshow(x_train[0], cmap='gray')
    axes[1].set_title('Label: {}'.format(y_test[0]))
    axes[1].imshow(x_test[0], cmap='gray')
    plt.tight_layout()
    plt.show()


# 显示图像
show_image(x_train, y_train, x_test, y_test)

9-4 构造神经网络

使用keras训练神经网络的基本步骤

  1. 构建神经网络
  2. 编译神经网络
  3. 训练神经网络
  4. 评估神经网络

创建神经网络模型

  • tf.keras.models.Sequential() 通过这个方法创建神经网络
  • 添加层model.add() 添加那些层
  • 或者直接将层数组当做参数传入Sequantial

层的种类

  • tf.keras.layers.Flatten() 将图片数据转换为向量
  • tf.keras.layers.Dense() 指定神经元的个数

image-20250526122132454

import tensorflow as tf
import matplotlib.pyplot as plt

# 创建一个顺序模型(Sequential Model),这是最简单的线性、逐层堆叠的神经网络架构。
model = tf.keras.models.Sequential(
    [
        # 第一层:Flatten层。将输入的28x28的二维图像数据"压平"成一维数组,长度为784(28*28)。
        # 这一步骤通常在处理图像输入时使用,作为卷积层与全连接层之间的过渡。
        tf.keras.layers.Flatten(input_shape=(28, 28)),
        
        # 第二层:全连接层(Dense Layer)。包含128个神经元节点,激活函数选用ReLU(Rectified Linear Unit)。
        # ReLU函数是深度学习中常用的激活函数之一,它的表达式为f(x)=max(0,x),有助于增加模型的非线性能力。
        tf.keras.layers.Dense(128, activation='relu'),
        
        # 第三层:输出层。同样是一个全连接层,但其节点数与类别数量相同(这里是10,因为MNIST数据集有0-9共10种数字),
        # 并且使用softmax作为激活函数。Softmax函数可以将模型的输出转换为概率分布,使得每个输出值都在[0,1]之间并且总和为1。
        tf.keras.layers.Dense(10, activation='softmax')
    ]
)

# 注释掉的部分是另一种添加网络层的方式,通过调用model.add()方法逐一加入各层。
# model.add(tf.keras.layers.Flatten(input_shape=(28, 28)))
# model.add(tf.keras.layers.Dense(128, activation='relu'))
# model.add(tf.keras.layers.Dense(10, activation='softmax'))

9-5 编译神经网络

神经网络的编译阶段

compile函数

  • model.compile(.)
  • 根据神经网络构建计算图
  • 根据选择的优化器与硬件,制定合适的训练计划
  • 分配内存,初始化各种变量等

compile函数的一些参数

  • Optimizer= 'adam'
  • Loss = 'sparse_categorical_crossentropy' 设置损失函数
  • metrics= 'accuracy'

优化器

  • SGD,最基本优化器,可设置学习率,动量等参数
  • RMSprop,通过自适应率解决梯度消失问题
  • Adam,结合了动量和RMSprop的优点,首先优化器
  • Adagrad,自适应梯度算法

损失函数

  • mean_squared_error:均方误差,预测值与真实值的误差
  • binary_crossentropy:二元交叉商,适用于0或1二分类
  • sparse_categorical_crossentropy:多分类交叉商
  • categorical_crossentropy:交叉商,one-hot编码

metrics

  • accuracy:准确率,适用于大多数分类问题
  • binary-accuracy:二元准确率,适用于二分类问题
  • categorical_accuracy:多分类准确率,one-hot编码
  • sparse_categorical_accuracy:多分类准确率
  • precision:精确率

「实战」编译神经网络

import tensorflow as tf
import matplotlib.pyplot as plt

# 构建神经网络模型
model = tf.keras.models.Sequential([
    tf.keras.layers.Input(shape=(28, 28)),      # 更推荐的方式:显式定义输入层
    tf.keras.layers.Flatten(),                  # 将 28x28 图像展平为 784 维向量
    tf.keras.layers.Dense(128, activation='relu'),  # 隐藏层,128个神经元,ReLU激活函数
    tf.keras.layers.Dense(10, activation='softmax') # 输出层,10个类别,Softmax输出概率
])

# 编译模型
model.compile(
    loss='categorical_crossentropy',   # 损失函数:用于分类任务
    optimizer='adam',                  # 优化器:Adam 自动调整学习率
    metrics=['accuracy']               # 评估指标:准确率
)

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

9-6 训练神经网络模型

训练神经网络模型

  • model.fit(...)
    • x_train, y_train 训练数据与训练标签数据
    • epochs=5 训练数据的轮次

评估神经网络模型

  • model.evaluate(...)
    • x_test, y_test 测试数据集与测试数据集标签
import tensorflow as tf
import matplotlib.pyplot as plt

# 从 test.py 文件中导入训练数据(x_train, y_train)
# 注意:test.py 中必须有定义好的 x_train 和 y_train 变量
from test import x_train, y_train

# 构建神经网络模型
model = tf.keras.models.Sequential([
    # 定义输入层,指定输入形状为 (28, 28),表示每张图是 28x28 像素的灰度图像
    tf.keras.layers.Input(shape=(28, 28)),

    # 将输入的二维图像展平成一维向量(784个像素),便于传入全连接层
    tf.keras.layers.Flatten(),

    # 全连接隐藏层,包含128个神经元,使用 ReLU 激活函数(常见于隐藏层)
    tf.keras.layers.Dense(128, activation='relu'),

    # 输出层,包含10个神经元(对应MNIST的10个数字类别),使用 Softmax 激活函数
    # Softmax会将输出转化为概率分布,表示属于每个类别的可能性
    tf.keras.layers.Dense(10, activation='softmax')
])

# 编译模型:配置损失函数、优化器和评估指标
model.compile(
    # 使用 sparse_categorical_crossentropy 损失函数:
    # - 适用于整数标签(如 y_train 是 [5, 3, 0, ...] 这样的形式)
    loss='sparse_categorical_crossentropy',

    # 使用 Adam 优化器:自适应学习率,适合大多数深度学习任务
    optimizer='adam',

    # 在训练过程中监控准确率(accuracy)
    metrics=['accuracy']
)

# 打印模型结构和参数数量,帮助了解模型复杂度
model.summary()

# 加载 MNIST 数据集
# 如果你没有提前加载数据,可以直接用 Keras 提供的 load_data 方法
mnist = tf.keras.datasets.mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data("/Users/programmer3.cc.vwed/PycharmProjects/pythonProject4/mnist.npz")

# 训练模型:使用训练数据进行训练,迭代5轮(epochs)
# - x_train: 输入数据(图片像素值)
# - y_train: 标签数据(数字0-9)
# - epochs=5: 整个数据集被训练5次
model.fit(x_train, y_train, epochs=5)

# 评估模型:在测试集上评估模型性能
# 返回的是 loss 和 metrics 中指定的指标(这里是 accuracy)
test_loss, test_acc = model.evaluate(x_test, y_test)

# 打印测试结果
print(f"测试集上的准确率: {test_acc:.4f}")
print(f"测试集上的损失值: {test_loss:.4f}")

9-7 优化神经网络

优化神经网络模型

  • 输入数据归一化.
  • 神经网络正则化,dropout
  • 训练时,采用小批量梯度下降
import tensorflow as tf

# 构建神经网络模型
model = tf.keras.models.Sequential([
    tf.keras.layers.Flatten(input_shape=(28, 28)),
    tf.keras.layers.Dense(128, activation='relu'),
    tf.keras.layers.Dropout(0,2), # 优化:丢失20%的神经元
    tf.keras.layers.Dense(10, activation='softmax')
])
model.compile(
    loss='sparse_categorical_crossentropy',
    optimizer='adam',
    metrics=['accuracy']
)

model.summary()

mnist = tf.keras.datasets.mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data("/Users/programmer3.cc.vwed/PycharmProjects/pythonProject4/mnist.npz")

# 优化:数据归一化
x_train = x_train / 255.0
x_test = x_test / 255.0

model.fit(x_train, y_train, epochs=10,batch_size=128)
test_loss, test_acc = model.evaluate(x_test, y_test)

# 打印测试结果
print(f"测试集上的准确率: {test_acc:.4f}")
print(f"测试集上的损失值: {test_loss:.4f}")

9-8 Pytorch的一点历史

Pytorch的一点历史

  • 说到那个AI框架性能最好,回答一定是Tensorflow
  • 要问那个AI框架最易用,回答一定是Pytorch
  • Pytorch的成功正是因为它的易用性
  • 而且从Pytorch1.0开始,Pytorch的性能也得到了大幅提升
  • 2012年AlexNet在图像识别中打败了Google
  • 它是使用Cuda-Convnet训练出来的神经网络
  • 后来贾杨清受到了Cuda-Convnet启发,创造了Caffe
  • Facebook整合了Torch和Caffe产生了Pytorch
  • Tensorflow和Pytorch在众多AI框架中胜出
  • Tensorflow胜在性能,Pytorch胜在易用性
  • 现在Pytorch越来越受到大家的欢迎

9-9 Pytorch加载数据集

Pytorch加载MNIST数据集

引入Pytorch

import torch #引入pytorch
from torch import nn #构建神经网络的模块
from torch.utils.data import DataLoader #数据加载器,加载数据集
from torchvision import datasets #数据集模块,下载数据集
from torchvision.transforms import ToTensor #张量模

Pytorch的三大模块

  • Torchvision,用于处理计算机视觉相关的模块
  • Torchaudio,用于处理音频相关的模块
  • Torchtext,用于处理自然语言处理相关的模块

Pytorch加载MNIST数据集

from torchvision import datasets
from torchvision.transforms import ToTensor

# 下载并加载训练数据
training_data = datasets.MNIST(
    root="data",        # 数据存放的位置
    train=True,         # 表示这是训练集
    download=True,      # 如果本地没有数据,则从网上下载
    transform=ToTensor() # 将图像数据转换为 Tensor 格式
)

参数说明:

  • root="data":表示数据存储的文件夹路径。如果该路径下没有 MNIST 数据集,且 download=True,则会自动下载。
  • train=True:表示这是训练集(若设为 False 则是测试集)。
  • download=True:如果本地没有数据,并且设置了此参数为 True,就会从网上下载数据集。
  • transform=ToTensor():表示在加载数据时将其转换为 PyTorch 的 Tensor 格式。这一步通常是深度学习处理图像的标准流程。

注意,转成tensor后,数据就已经做了归一化处理

「实战」使用pytorch加载数据

import os
os.environ['KMP_DUPLICATE_LIB_OK'] = 'TRUE'

import torch  # 引入pytorch
from torch import nn  # 构建神经网络的模块
from torch.utils.data import DataLoader  # 数据加载器
from torchvision import datasets  # 数据集模块
from torchvision.transforms import ToTensor  # 转换为 Tensor

# 下载并加载训练数据
training_data = datasets.MNIST(
    root="data",        # 数据存放的位置
    train=True,         # 表示这是训练集
    download=True,      # 如果本地没有数据,则从网上下载
    transform=ToTensor() # 将图像数据转换为 Tensor 格式
)

# 下载并加载测试数据
test_data = datasets.MNIST(
    root="data",        # 数据存放的位置
    train=False,        # 表示这是测试集
    download=True,      # 如果本地没有数据,则从网上下载
    transform=ToTensor() # 将图像数据转换为 Tensor 格式
)

9-10 Pytorch导入训练数据和测试数据

Pytorch导入训练数据与测试数据

导入数据

  • DataLoader (data, batch_size)
    • data:datasets.MNIST(...)
    • batch_size:小批量梯度下降的批量大小

「实战」使用pytorch导入数据

# 导入 os 模块,用于设置环境变量
import os
# 允许在 macOS 或某些环境中重复加载 OpenMP 库(解决可能出现的 libiomp5.dylib 冲突问题)
os.environ['KMP_DUPLICATE_LIB_OK'] = 'TRUE'

# 引入 PyTorch 相关模块
import torch  # 引入 PyTorch 主库
from torch import nn  # 引入神经网络模块,用于构建模型
from torch.utils.data import DataLoader  # 引入数据加载器,用于批量加载数据
from torchvision import datasets  # 引入 torchvision 的数据集模块,提供 MNIST 等标准数据集
from torchvision.transforms import ToTensor  # 引入图像变换工具,将图像转换为张量 Tensor

# 下载并加载训练数据集 MNIST
training_data = datasets.MNIST(
    root="data",        # 数据存放的位置,会在当前目录下创建 data 文件夹
    train=True,         # 表示这是训练集
    download=True,      # 如果本地没有数据,则从网络下载
    transform=ToTensor() # 将图像数据转换为 Tensor 格式(PyTorch 可处理的数据格式)
)

# 下载并加载测试数据集 MNIST
test_data = datasets.MNIST(
    root="data",        # 数据存放的位置,与训练集放在同一文件夹中
    train=False,        # 表示这是测试集
    download=True,      # 如果本地没有数据,则从网络下载
    transform=ToTensor() # 同样将图像数据转换为 Tensor 格式
)

# 设置每次训练所使用的样本数量(批大小)
batch_size = 64

# 创建训练数据的数据加载器,用于按批次加载训练数据
train_data_loder = DataLoader(training_data, batch_size=batch_size)

# 创建测试数据的数据加载器,用于按批次加载测试数据
test_data_loder = DataLoader(test_data, batch_size=batch_size)

# 遍历训练数据加载器,取出一个 batch 的数据进行展示
for x, y in train_data_loder:
    print(f"Shape of x [N, C, W, H]: {x.shape}")  # 打印输入数据的形状:
                                                 # N: batch 数量(即样本数)
                                                 # C: 通道数(1 表示灰度图)
                                                 # W: 图像宽度(28 像素)
                                                 # H: 图像高度(28 像素)
    print(f"Shape of y: {y.shape}, Type: {y.dtype}")  # 打印标签数据的形状和类型
                                                    # y 是一维张量,每个元素是类别标签(0~9)
    break  # 只打印一次就退出循环

# 遍历测试数据加载器,取出一个 batch 的数据进行展示
for x, y in test_data_loder:
    print(f"Shape of x_test [N, C, W, H]: {x.shape}")  # 打印输入数据的形状:
                                                 # N: batch 数量(即样本数)
                                                 # C: 通道数(1 表示灰度图)
                                                 # W: 图像宽度(28 像素)
                                                 # H: 图像高度(28 像素)
    print(f"Shape of y_test: {y.shape}, Type: {y.dtype}")  # 打印标签数据的形状和类型
                                                    # y 是一维张量,每个元素是类别标签(0~9)
    break  # 只打印一次就退出循

9-11 Pytorch构建神经网络

构建神经网络

  • 定义神经网络类,并继承自nn.Module
  • Pytorch使用nn.Sequential对象创建神经网络
  • 神经网络类中必须实现forward()方法
  • Pytorch使用nn.Flatten对象将图片转中列向量
  • nn.to方法可以让模型运行在不同的硬件上

「实战」使用Pytorch构建神经网络

# 设置设备:如果支持 CUDA(即有可用的 GPU),则使用 'cuda';否则使用 'cpu'
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# 打印当前使用的设备(输出可能是 "cuda" 或 "cpu")
print(device)

# 定义一个神经网络类 NeuralNetWork,继承自 PyTorch 的 nn.Module 基类
class NeuralNetWork(nn.Module):
    def __init__(self):
        # 调用父类构造函数
        super().__init__()

        # 使用 Sequential 容器定义一个简单的全连接神经网络(多层感知机)
        self.nmodel = nn.Sequential(
            # 第一层:线性层(全连接层),输入大小 28*28(每张图片展平后的像素数),输出大小 128
            nn.Linear(28 * 28, 128),
            # 激活函数 ReLU,引入非线性
            nn.ReLU(),
            # 第二层:线性层,输入大小 128,输出大小 10(对应 MNIST 的 10 个类别:数字 0~9)
            nn.Linear(128, 10)
        )

    # 定义前向传播过程
    def forward(self, x):
        # 将输入图像从形状 [batch_size, 1, 28, 28] 展平为 [batch_size, 28*28]
        x = nn.Flatten()(x)
        
        # 将展平后的数据传入定义好的网络结构中进行计算
        output = self.nmodel(x)
        
        # 返回模型输出结果
        return output

# 实例化神经网络模型,并将其移动到指定的设备上(GPU 或 CPU)
model = NeuralNetWork().to(device)

# 打印模型结构,查看模型各层信息
print(model)

9-12 用Pytorch实现训练神经网络的逻辑

Pytorch训练神经网络的步骤

  • 指定model工作模式:train训练模式/eval
  • 循环读取小批量数据
  • 进行训练(前向传播)
  • 通过损失函数计算损失
  • 反向传播,计算梯度
  • 更新梯度
  • 清空本次小批量梯度,确保每次小批量都重新计算梯度

损失函数

  • 回归相关:nn.MSELoss(均方差)、nn.L1Loss(平均绝对差)
  • 多分类相关:nn.CrossEntropyLoss包含了softmax nn.NULLLoss需要手动添加softmax

优化器

  • SGD:随机梯度下降
  • Adam:最佳优化器
  • RMSprop

「实战」使用Pytorch训练神经网络

# 设置学习率为 0.001(即 1e-3)
learning_rate = 1e-3
# 使用 Adam 优化器来优化模型的所有参数,设置学习率为 learning_rate
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
# 定义损失函数为交叉熵损失函数(CrossEntropyLoss),适用于分类任务
loss_fn = nn.CrossEntropyLoss()
# 定义训练函数
def train(dataloader, model, loss_fn, optimizer):
    # 获取数据集中总样本数
    size = len(dataloader.dataset)
    # 将模型设为训练模式(启用 dropout、batch normalization 等训练时的行为)
    model.train()
    # 遍历数据加载器中的每个 batch
    for batch, (X, y) in enumerate(dataloader):
        # 将输入数据 X 和标签 y 移动到指定设备(如 GPU 或 CPU)
        X, y = X.to(device), y.to(device)
        # 前向传播:模型对输入 X 进行预测
        pred = model(X)
        # 计算预测结果与真实标签之间的损失
        loss = loss_fn(pred, y)
        # 反向传播:计算梯度
        loss.backward()
        # 更新优化器中的参数(根据梯度进行参数更新)
        optimizer.step()
        # 清空当前的梯度,防止梯度累积
        optimizer.zero_grad()
        # 每处理 100 个 batch 打印一次当前损失和训练进度
        if batch % 100 == 0:
            loss, current = loss.item(), batch * len(X)  # loss.item() 获取标量值,current 表示已处理的样本数
            print(f"Loss: {loss:>7f}, [{current:>5d}/{size:>5d}]")

9-13 用Pytorch实现评估神经网路的逻辑

Pytorch实现神经网络评估

  • 指定model工作模式:eval
  • 禁止梯度计算与跟踪,应为在测试阶段不需要计算梯度
  • 进行预测(前向传播)
  • 通过损失函数计算损失

[实战] 使用Pytorch 实现神经网络评估

def test(dataloader, model, loss_fn):
    # 获取数据集总样本数和 batch 的数量
    size = len(dataloader.dataset)
    num_batches = len(dataloader)

    # 将模型设为评估模式(禁用 dropout、batch normalization 等训练时的行为)
    model.eval()

    # 初始化测试损失和正确预测的数量
    test_loss = 0
    correct = 0

    # 在评估过程中不需要计算梯度,使用 torch.no_grad() 提高效率
    with torch.no_grad():
        for X, y in dataloader:
            # 将输入数据和标签移动到指定设备(如 GPU 或 CPU)
            X, y = X.to(device), y.to(device)
            # 前向传播:模型对输入进行预测
            pred = model(X)
            # 累加每个 batch 的测试损失
            test_loss += loss_fn(pred, y).item()
            # 计算当前 batch 中预测正确的样本数
            correct += (pred.argmax(1) == y).type(torch.float).sum().item()
    # 计算平均损失和准确率
    test_loss /= num_batches   # 平均每个 batch 的损失
    correct /= size            # 准确率:正确预测的比例
    # 打印测试结果
    print(f"Test Error: \n Accuracy: {(100 * correct):>0.1f}% Average loss: {test_loss:>8f} \n")

9-14 Pytorch训练和评估神经网络

完整实例

# 导入 os 模块,用于设置环境变量
import os

from tensorflow.python.ops.gen_batch_ops import batch
from torch.fft import ifftshift

from 构造神经网络 import test_loss

# 允许在 macOS 或某些环境中重复加载 OpenMP 库(解决可能出现的 libiomp5.dylib 冲突问题)
os.environ['KMP_DUPLICATE_LIB_OK'] = 'TRUE'

# 引入 PyTorch 相关模块
import torch  # 引入 PyTorch 主库
from torch import nn  # 引入神经网络模块,用于构建模型
from torch.utils.data import DataLoader  # 引入数据加载器,用于批量加载数据
from torchvision import datasets  # 引入 torchvision 的数据集模块,提供 MNIST 等标准数据集
from torchvision.transforms import ToTensor  # 引入图像变换工具,将图像转换为张量 Tensor

# 下载并加载训练数据集 MNIST
training_data = datasets.MNIST(
    root="data",  # 数据存放的位置,会在当前目录下创建 data 文件夹
    train=True,  # 表示这是训练集
    download=True,  # 如果本地没有数据,则从网络下载
    transform=ToTensor()  # 将图像数据转换为 Tensor 格式(PyTorch 可处理的数据格式)
)

# 下载并加载测试数据集 MNIST
test_data = datasets.MNIST(
    root="data",  # 数据存放的位置,与训练集放在同一文件夹中
    train=False,  # 表示这是测试集
    download=True,  # 如果本地没有数据,则从网络下载
    transform=ToTensor()  # 同样将图像数据转换为 Tensor 格式
)

# 设置每次训练所使用的样本数量(批大小)
batch_size = 64

# 创建训练数据的数据加载器,用于按批次加载训练数据
train_data_loder = DataLoader(training_data, batch_size=batch_size)

# 创建测试数据的数据加载器,用于按批次加载测试数据
test_data_loder = DataLoader(test_data, batch_size=batch_size)

# 遍历训练数据加载器,取出一个 batch 的数据进行展示
for x, y in train_data_loder:
    print(f"Shape of x [N, C, W, H]: {x.shape}")  # 打印输入数据的形状:
    # N: batch 数量(即样本数)
    # C: 通道数(1 表示灰度图)
    # W: 图像宽度(28 像素)
    # H: 图像高度(28 像素)
    print(f"Shape of y: {y.shape}, Type: {y.dtype}")  # 打印标签数据的形状和类型
    # y 是一维张量,每个元素是类别标签(0~9)
    break  # 只打印一次就退出循环

# 遍历测试数据加载器,取出一个 batch 的数据进行展示
for x, y in test_data_loder:
    print(f"Shape of x_test [N, C, W, H]: {x.shape}")  # 打印输入数据的形状:
    # N: batch 数量(即样本数)
    # C: 通道数(1 表示灰度图)
    # W: 图像宽度(28 像素)
    # H: 图像高度(28 像素)
    print(f"Shape of y_test: {y.shape}, Type: {y.dtype}")  # 打印标签数据的形状和类型
    # y 是一维张量,每个元素是类别标签(0~9)
    break  # 只打印一次就退出循环

# 设置设备:如果支持 CUDA(即有可用的 GPU),则使用 'cuda';否则使用 'cpu'
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# 打印当前使用的设备(输出可能是 "cuda" 或 "cpu")
print(device)


# 定义一个神经网络类 NeuralNetWork,继承自 PyTorch 的 nn.Module 基类
class NeuralNetWork(nn.Module):
    def __init__(self):
        # 调用父类构造函数
        super().__init__()

        # 使用 Sequential 容器定义一个简单的全连接神经网络(多层感知机)
        self.nmodel = nn.Sequential(
            # 第一层:线性层(全连接层),输入大小 28*28(每张图片展平后的像素数),输出大小 128
            nn.Linear(28 * 28, 128),
            # 激活函数 ReLU,引入非线性
            nn.ReLU(),
            # 第二层:线性层,输入大小 128,输出大小 10(对应 MNIST 的 10 个类别:数字 0~9)
            nn.Linear(128, 10)
        )

    # 定义前向传播过程
    def forward(self, x):
        # 将输入图像从形状 [batch_size, 1, 28, 28] 展平为 [batch_size, 28*28]
        x = nn.Flatten()(x)

        # 将展平后的数据传入定义好的网络结构中进行计算
        output = self.nmodel(x)

        # 返回模型输出结果
        return output


# 实例化神经网络模型,并将其移动到指定的设备上(GPU 或 CPU)
model = NeuralNetWork().to(device)

# 打印模型结构,查看模型各层信息
print(model)

learning_rate = 1e-3
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
loss_fn = nn.CrossEntropyLoss()


def train(dataloader, model, loss_fn, optimizer):
    size = len(dataloader.dataset)
    model.train()
    for batch, (X, y) in enumerate(dataloader):
        X, y = X.to(device), y.to(device)
        pred = model(X)
        loss = loss_fn(pred, y)
        loss.backward()
        optimizer.step()
        optimizer.zero_grad()
        if batch % 100 == 0:
            loss, current = loss.item(), batch * len(X)
            print(f"Loss: {loss:>7f},[{current:>5d}/{size:>5d}]")




def test(dataloader, model, loss_fn):
    # 获取数据集总样本数和 batch 的数量
    size = len(dataloader.dataset)
    num_batches = len(dataloader)
    # 将模型设为评估模式(禁用 dropout、batch normalization 等训练时的行为)
    model.eval()
    # 初始化测试损失和正确预测的数量
    test_loss = 0
    correct = 0
    # 在评估过程中不需要计算梯度,使用 torch.no_grad() 提高效率
    with torch.no_grad():
        for X, y in dataloader:
            # 将输入数据和标签移动到指定设备(如 GPU 或 CPU)
            X, y = X.to(device), y.to(device)
            # 前向传播:模型对输入进行预测
            pred = model(X)
            # 累加每个 batch 的测试损失
            test_loss += loss_fn(pred, y).item()
            # 计算当前 batch 中预测正确的样本数
            correct += (pred.argmax(1) == y).type(torch.float).sum().item()
    # 计算平均损失和准确率
    test_loss /= num_batches   # 平均每个 batch 的损失
    correct /= size            # 准确率:正确预测的比例
    # 打印测试结果
    print(f"Test Error: \n Accuracy: {(100 * correct):>0.1f}% Average loss: {test_loss:>8f} \n")


epochs = 5
for epoch in range(epochs):
    print(f"Epoch {epoch+1}/{epochs}")
    train(train_data_loder, model, loss_fn, optimizer)
    test(test_data_loder, model, loss_fn)
print('Finished Training')

9-15 模型与训练优化

优化神经网络和训练方法

9-16 模型的保存部署与使用

**第10章 卷积神经网络

10-1 导学

认识卷积网络CNN

CNN带来了哪些好处?

  • 参数共享,相较全连接,大大减少了学习参数数量
  • 稀疏连接,每个神经元只需连接输入图像的一个局部
  • 平移不变性,无论目标出现在图片在哪儿,filter都可以扫到
  • 多层次特征提取,通过多层卷积和池化来实现
  • CNN适合处理图像,它会考虑空间关系,全连接只考虑一维

CNN主要用来做什么?

  • 图像分类
  • 目标检测
  • 图像分割
  • 超分
  • 图像风格迁移
  • 图像修复
  • 动作识别
  • 图像生成

主要内容

  • 卷积操作
  • 卷积的一些基本概念
  • 池化操作
  • 卷积网络
  • 经典的CNN架构
    • AlexNet
    • ResNet

10-2 卷积操作

卷积操作

边缘检测

  • 在传统的计算机视觉中我们可以使用Canny检测边缘
  • 或者可以使用多种Filter/Kernel实现边缘的检测

image-20250527143844880

卷积

  • 在边缘检测/Blur中,我们就介绍过卷积
  • 所谓卷积就是让图片与Filer/Kernel相乘

image-20250527144005235

深度学习中的Filter/Kernel

image-20250527144040488

小结

  • 在传统的计算机视觉中,检测边缘需要自己定义kernel
  • 在深度学习中,可以让深度神经网络自己找到Kernel

10-3 CNN的一些基本概念

Padding与Stride

  • Padding与Stride相关的知识在传统计算机视觉中已介绍
  • 它们都会影响卷积后的结果
  • 为了让输出等于原图,一般会在原图外围加一圈Padding
  • Stride是Kernel在原图上移动的距离,一般设置为1

例子

image-20250527153411874

卷积输出的计算公式

image-20250527153602213

这里:

  • nn 表示输入图像的宽度或高度(对于正方形图像,宽和高相等)。
  • ff 表示卷积核(kernel/filter)的大小(假设为正方形卷积核,则宽和高相等)。
  • pp 表示填充(padding)层数,即在输入图像每一边扩展的像素数。
  • ss 表示卷积核移动的跨度(stride),即每次卷积操作之间在输入图像上移动的距离。

这个公式考虑了输入图像尺寸 nn、通过两边各填充 pp 层后增加的尺寸、减去卷积核大小 ff,然后除以步长 ss,最后加上1得到输出尺寸。需要注意的是,结果需要向下取整(⌊⋅⌋⌊⋅⌋ 表示向下取整),因为卷积操作产生的特征图尺寸必须是整数。

举个例子

image-20250527153643717

小结

image-20250527153715085

10-4 三维卷积与多核卷积

三维卷积

image-20250527153903751

多核卷积

image-20250527154009131

卷积网络的过程

image-20250527154120674

卷积层中的一些符号

输入(Input)

  • 符号:n × n × d
  • 示例:28 × 28 × 3
  • 含义:
    • n:输入图像的宽/高
    • d:输入通道数(如RGB图像为3)

Filter大小(Filter Size)

  • 符号:f
  • 示例:f = 3 表示 3 × 3 的卷积核

Padding大小(Padding Size)

  • 符号:p
  • 示例:p = 1

Stride大小(Stride Size)

  • 符号:s
  • 示例:s = 2

输出(Output)

  • 符号:n_out × n_out × k
  • 计算公式:n_out = floor((n + 2p - f) / s) + 1
  • k:输出特征图的数量(即Filter个数)

Filter的个数(Number of Filters)

  • 符号:k
  • 每个Filter尺寸:(f × f × d),其中d是上一层输入的channels

激活函数(Activation Function)

  • 符号:A_l
  • 示例:ReLU(A_l)

权重(Weights)

  • 每个Filter权重尺寸:(f × f × d) → k,共k个Filter
  • 总权重:(f × f × d × k)

偏置(Bias)

  • 符号:b
  • 数量:与Filter个数相同,共k个

小结

image-20250527154706526

同一个图片可以同时乘n个kernel,得到n层输出

10-5 CNN中的池化

池化的作用

  • 减少特征图的大小,降低计算量和参数数量同时还能保留重要的特征

池化的好处

  • 降低计算量
  • 防止过拟合
    • 通过减少特征尺寸降低了模型 的复杂度

池化的种类

  • 最大值池化:在池化窗口内选择元素的最大值作为输出
  • 平均值池化:在池化窗口内计算所有元素的平均值作为输出

最大池化

image-20250527155040038

平均值池化

池化在什么时候使用?

  • 一般在卷积之后使用池化

小结

  • 知道池化的作用
  • 池化的种类
  • 什么时候使用池化

10-6 标准CNN神经网络

CNN网络的构成

  • 卷积层-CONV(Convolution)
  • 池化层-POOL(Pooling)
  • 全连接层-FC(Fully Connected)

标准CNN网络-LeNet5

image-20250527155547598

小结

  • CNN网络由CONV、POOL、
  • FC构成
  • (CONV,POOL)是一套组合有点像搭积木

10-7 Keras实现CNN神经卷积网络

「实战」Keras实现标准CNN网络

10-8 KerasCNN网络架构优化

# 导入 TensorFlow 和 MNIST 数据集
import tensorflow as tf
mnist = tf.keras.datasets.mnist

# 加载 MNIST 数据集(包含训练集和测试集)
# 数据将被保存在 "./mnist.npz" 路径下
(x_train, y_train), (x_test, y_test) = mnist.load_data("./mnist.npz")

# 将图像像素值归一化到 [0, 1] 区间(因为原始像素值是 0~255)
x_train, x_test = x_train / 255.0, x_test / 255.0

# 导入 matplotlib.pyplot 用于显示图像
import matplotlib.pyplot as plt

# 定义一个函数来展示训练集和测试集中第一张图像及其标签
def show_image(x_train, y_train, x_test, y_test):
    # 创建一个 1 行 2 列的子图布局
    fig, axes = plt.subplots(1, 2)

    # 显示训练集中第一张图像
    axes[0].set_title('Label:{}'.format(y_train[0]))  # 设置标题为对应标签
    axes[0].imshow(x_train[0], cmap='gray')           # 使用灰度图显示

    # 显示测试集中第一张图像
    axes[1].set_title('Label:{}'.format(y_test[0]))   # 设置标题为对应标签
    axes[1].imshow(x_test[0], cmap='gray')            # 使用灰度图显示

    plt.show()  # 展示图像窗口

# 调用函数展示图像
show_image(x_train, y_train, x_test, y_test)

# 构建卷积神经网络模型
model = tf.keras.models.Sequential([
    # 第一层卷积层:32个 3x3 的卷积核,使用 ReLU 激活函数
    # 输入形状为 (28, 28, 1),即 28x28 的图像,1个通道(灰度图)
    tf.keras.layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)),

    # 批标准化层(BatchNormalization):加速训练并提高稳定性
    tf.keras.layers.BatchNormalization(),

    # 最大池化层:使用 2x2 的窗口进行下采样,减少空间维度
    tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),

    # 第二层卷积层:64个 3x3 的卷积核,同样使用 ReLU 激活函数
    tf.keras.layers.Conv2D(64, (3, 3), activation='relu'),

    # 批标准化层
    tf.keras.layers.BatchNormalization(),

    # 再次最大池化
    tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),

    # 将多维特征图展平为一维向量,以便输入全连接层
    tf.keras.layers.Flatten(),

    # 全连接层(Dense Layer):128个神经元
    tf.keras.layers.Dense(128),

    # 批标准化层
    tf.keras.layers.BatchNormalization(),

    # 激活函数层:使用 ReLU 激活函数
    tf.keras.layers.Activation('relu'),

    # Dropout 层:随机丢弃 20% 的神经元,防止过拟合
    tf.keras.layers.Dropout(0.2),

    # 输出层:10个神经元,对应10个类别(数字0~9),使用 softmax 激活函数
    tf.keras.layers.Dense(10, activation='softmax')
])

# 定义优化器:Adam,学习率为 0.0001
optimizer = tf.keras.optimizers.Adam(learning_rate=0.0001)

# 编译模型
# 损失函数:sparse_categorical_crossentropy(适用于整数标签)
# 优化器:前面定义的 Adam
# 评估指标:准确率
model.compile(loss='sparse_categorical_crossentropy',
              optimizer=optimizer,
              metrics=['accuracy'])

# 训练模型
# 使用训练数据 x_train, y_train
# 进行 15 轮训练(epochs)
# 每批训练使用 128 个样本(batch_size=128)
model.fit(x_train, y_train, epochs=15, batch_size=128)

# 在测试集上评估模型性能
# 返回损失值和准确率
model.evaluate(x_test, y_test)

10-9 使用Pytorch实现标准CNN网络

10-10 经典神经网络AlexNet

几种经典的CNN网络

  • LeNet-5
  • AlexNet
  • VGGNet
  • ResNet

AlexNet与LeNet比较

  • AlexNet相较于LeNet-5,它的网络规模更大
  • AlexNet有5个卷积3个全连接,LeNet是2个卷积3个全连接
  • AlexNet才开始使用ReLU激活函数
  • AlexNet使用多GPU进行并行训练
  • AlexNet使用数据增强技术:图像平移、翻转、裁剪
  • AlexNet开始使用Dropout正则化
  • AlexNet开始使用最大值池化,LeNet使用平均值池化
  • AlexNet使用ImageNet数据集,MNIST数据集较小

AlexNet网络架构

image-20250527162533751

10-11 经典神经网络VGGNet

VGGNet

  • VGGNet是由牛津大学的VGG团队开发的
  • 它发布于2014年,相较AlexNet其具有更多的卷积层
  • 它使用多个连续的3x3卷积核替代AlexNet中的大卷积核
  • 具有与大卷积核相同的受视野,同时参数最大幅减少
  • 此外,VGGNet具有更深的层级,如VGG-16、VGG-19
  • 这也是使用小卷积核带来的好处,即可以构建更深的网络
  • 更深的网络可以学到更复杂的特征表示,因此其性能更优
  • 其网络结构更简单、一致,都是3x3卷积+2x2的最大池化

image-20250527164621198

10-12 经典神经网络ResNet

ResNet

  • ResNet是何恺明、张详雨、任少卿、孙剑几个人发明的
  • ResNet最大的作用是解决了深层网络梯度消失与爆炸问题
  • ResNet可以让神经网络达到上百层甚至上千层

ResNet核心思想

image-20250603131303754

结论

  • 即使残差函数学习到的权重都为0,仍能表现得像一个恒等映射
  • 从而保证训练更深网络时,性能不会出现退化
  • 由于网络层级变深,模型可以学到更多的特征

ResBlock

image-20250603131537795

image-20250603131707806

小结

  • 残差网络用于训练深层网络可以有效防止梯度消失或梯度爆炸
  • 因可训练更深层的网络,所以用大规模数据训练时准确率更高
  • 即使残差函数学习到的权重都为0,仍能表现得像一个恒等映射
  • 从而保证训练更深网络时,性能不会出现退化
  • 另外,ResNet可像积木一样堆叠,方便构建深层、复杂网络
最近更新:: 2025/9/8 11:05
Contributors: yanpeng_