简介:这份数字图像处理大作业资源包面向计算机视觉入门学习者与高校课程实践者,围绕图像分割、人脸检测与边缘检测三大经典任务展开。包内共11个文件,以7个Python脚本为核心,配合2个Markdown说明文档、1个压缩包及1个gitignore配置,整体约97.64MB,脚本覆盖预处理、分类、训练与评估等模块,便于直接运行与二次修改。内容涉及Canny边缘检测的高斯滤波、梯度计算、非极大值抑制与双阈值流程,OTSU自适应二值化的类间方差最大化思路,以及基于OpenCV Haar级联分类器的人脸检测实现,并借助matplotlib完成结果可视化。目前已有8348人学习下载,适合希望系统掌握OpenCV与Python图像处理基础、完成课程大作业或夯实计算机视觉入门技能的读者参考。
1. 拆开这份 DIP 大作业:三个算法怎么串成一条能跑的流水线
数字图像处理大作业里,图像分割、人脸检测、边缘检测这三件事经常被拆成三道独立题,交完报告就扔。但真到工程里,它们往往是同一条流水线上的不同工位:先做预处理压噪,再上边缘或阈值把前景抠出来,最后在候选区域里跑检测。这份 DIP-Project-master 的价值就在这——它不是三个孤立的 demo,而是一个带 Gradle 构建、带 classification 训练子模块、带 Preliminaries 说明的完整工程骨架。你拿到手能直接跑 Canny、OTSU、Haar 人脸检测,也能顺着 classification 目录往下摸到 ResNet 和 3D 训练脚本。适合谁?正在交大作业但不想只交一份 notebook 的学生,以及想用 OpenCV + Python 快速验证传统算子效果的从业者。它不解决 SOTA 精度问题,但能让你把「预处理→分割→检测」这条链路亲手跑通一遍,知道每个参数动了之后图像到底变成什么样。
2. 环境与工程结构:Gradle 6.5 和 Python 依赖怎么摆平
2.1 先看清压缩包里到底有什么
解压 DIP-Project-master.zip 之后,根目录下大致是这么几类东西:.gitignore管版本控制忽略项,README.md和Preliminaries.md是说明文档,gradle-6.5-bin.zip是构建工具本体,classification目录是分类训练相关的 Python 代码,根目录还散着classification.py、divide_trdev.py、preprocess.py、dataset.py、metric.py、resnet.py、train_3d.py这些脚本。注意,gradle 的 zip 直接放在工程里,说明作者希望你在没网或网络受限的环境下也能把构建跑起来,不用再去下载。
| 文件/目录 | 作用 | 你什么时候会碰它 |
|---|---|---|
Preliminaries.md | 预备知识说明 | 第一次跑之前读一遍 |
gradle-6.5-bin.zip | Gradle 构建工具 | 需要编译 Java/Kotlin 部分时 |
classification/ | 分类训练代码 | 想跑 ResNet 或 3D 训练时 |
preprocess.py | 预处理脚本 | 做数据清洗和增强时 |
dataset.py | 数据集加载 | 换自己的数据时 |
metric.py | 评估指标 | 看分类结果时 |
resnet.py | ResNet 模型定义 | 改网络结构时 |
train_3d.py | 3D 训练入口 | 跑三维数据训练时 |
这张表不是让你背,是让你在报错时能快速定位该翻哪个文件。常见做法是先把Preliminaries.md过一遍,里面通常会写清楚 Python 版本和依赖,别跳过。
2.2 Python 侧依赖装法
传统算子这部分,核心依赖就是 OpenCV、NumPy、Matplotlib。我一般会建一个干净虚拟环境,避免和系统里的包打架:
python -m venv dip-env source dip-env/bin/activate # Windows 用 dip-env\Scripts\activate pip install opencv-python numpy matplotlibopencv-python是主包,带cv2.Canny、cv2.threshold、cv2.CascadeClassifier这些函数;numpy负责数组运算,OpenCV 读进来的图像本身就是 ndarray;matplotlib用来把原图和结果并排画出来对比。如果你还要跑classification里的训练脚本,那还得补 PyTorch 或 TensorFlow,具体看resnet.py的 import 头。这里有个坑:opencv-python和opencv-contrib-python不要同时装,会互相覆盖,导致某些函数找不到。
2.3 Gradle 部分要不要动
gradle-6.5-bin.zip的存在说明工程里有需要 Gradle 构建的模块,可能是 Java 写的图像处理工具,也可能是 Android 侧的调用封装。如果你只关心 Python 侧的三个算法,这个 zip 可以先放着不动。但如果你要跑完整工程,就得把它解压到某个目录,然后把GRADLE_HOME指过去,或者直接用工程里的 wrapper。常见做法是:
unzip gradle-6.5-bin.zip -d /opt/gradle export PATH=/opt/gradle/gradle-6.5/bin:$PATH gradle -v # 验证版本gradle -v能打印出 6.5 版本号就说明环境通了。这一步不急,先把 Python 侧的三个算法跑出图来,再回头处理构建的事。
3. Canny 边缘检测:高斯核、双阈值到底怎么调
3.1 算法四步拆解
Canny 不是一步出结果的,它内部走了四步:高斯滤波去噪、Sobel 算子算梯度幅值和方向、非极大值抑制细化边缘、双阈值连接强弱边缘。很多人调cv2.Canny只改两个阈值,结果边缘要么断断续续要么糊成一片,就是因为没意识到前面还有高斯核在起作用。cv2.Canny的签名里其实有apertureSize和L2gradient两个参数,前者控制 Sobel 核大小,后者决定梯度幅值用 L1 还是 L2 范数算。
3.2 可抄作业的调用代码
import cv2 import numpy as np import matplotlib.pyplot as plt # 读图并转灰度,Canny 只接受单通道 img = cv2.imread('test.jpg') gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 先手动高斯模糊,让核大小可控 blurred = cv2.GaussianBlur(gray, (5, 5), 1.4) # 双阈值:低阈值管弱边缘,高阈值管强边缘 edges = cv2.Canny(blurred, threshold1=50, threshold2=150, apertureSize=3, L2gradient=True) # 并排显示 plt.subplot(1, 2, 1); plt.imshow(gray, cmap='gray'); plt.title('Gray') plt.subplot(1, 2, 2); plt.imshow(edges, cmap='gray'); plt.title('Canny') plt.show()逻辑说明:先cvtColor转灰度是因为 Canny 内部只处理单通道;GaussianBlur的核大小(5,5)和 sigma1.4是经验起点,噪声大就加大核,但核太大会把细边缘一起抹掉。threshold1=50、threshold2=150的比例常见做法是 1:2 到 1:3,高阈值定强边缘,低阈值把和强边缘相连的弱边缘接上。apertureSize=3是 Sobel 核尺寸,默认就是 3,改成 5 或 7 会更抗噪但定位变粗。L2gradient=True用平方和开根算幅值,比默认的 L1 更准,代价是稍慢。
3.3 参数怎么根据图像改
如果边缘断得多,先把threshold1往下调,比如从 50 降到 30;如果噪点被当成边缘,先把threshold2往上提,或者把高斯核从(5,5)加到(7,7)。光照不均匀的图,建议先做一次直方图均衡化再进 Canny,否则暗部边缘全丢。这里有个反直觉的点:L2gradient=True并不总是更好,在某些低对比度图上它会把噪声幅值也放大,反而多出假边缘,这时候切回False试试。
4. OTSU 图像分割:阈值自动选取和光照对抗
4.1 类间方差最大化的直觉
OTSU 的核心思想很朴素:遍历所有可能的灰度阈值,把像素分成前景和背景两类,算这两类的类间方差,取方差最大的那个阈值。类间方差越大,说明前景和背景分得越开。它不需要你手动给阈值,适合光照不均匀或对比度低的图。但它有个前提:图像直方图最好呈双峰分布,如果前景背景面积悬殊,或者直方图是单峰,OTSU 会翻车。
4.2 代码实现与参数
import cv2 import numpy as np import matplotlib.pyplot as plt img = cv2.imread('test.jpg', 0) # 直接以灰度读入 # 先做一次高斯滤波压噪,OTSU 对噪声敏感 blur = cv2.GaussianBlur(img, (5, 5), 0) # THRESH_OTSU 让 OpenCV 自动算阈值,返回值里 ret 就是它算出来的 ret, otsu_bin = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) print(f'OTSU 自动选取的阈值: {ret}') plt.subplot(1, 2, 1); plt.imshow(blur, cmap='gray'); plt.title('Blurred') plt.subplot(1, 2, 2); plt.imshow(otsu_bin, cmap='gray'); plt.title(f'OTSU th={ret}') plt.show()逻辑说明:cv2.threshold的第二个参数在用了THRESH_OTSU之后会被忽略,写 0 就行,真正生效的是 OTSU 自己算出来的ret。THRESH_BINARY表示大于阈值置 255,小于置 0。先高斯模糊是因为 OTSU 只看灰度分布,噪声会把直方图抹平,导致阈值偏移。print(ret)这一步别省,你能直观看到它选了多少,方便和手动阈值对比。
4.3 什么时候 OTSU 会失效
如果图像里前景只占很小一块,比如医学图像里的病灶,OTSU 算出来的阈值会偏向背景,把前景吞掉。这时候常见做法是先做形态学开运算去掉小噪点,或者改用自适应阈值cv2.adaptiveThreshold。另外,彩色图不能直接丢给 OTSU,必须先转灰度,否则它只按第一个通道算,结果完全不对。如果你要做的是广告牌图像分割这类前景背景面积接近的场景,OTSU 通常表现不错;但如果是颜色变化不大的边缘检测场景,OTSU 的二值化可能把渐变区域整块吞掉,得配合边缘算子一起用。
5. Haar 级联人脸检测:加载 XML 和 detectMultiScale 参数
5.1 级联分类器怎么工作
OpenCV 自带的haarcascade_frontalface_default.xml是一个预训练好的级联分类器,内部由多个阶段串联,每个阶段是一组 Haar 特征的弱分类器。检测时,滑动窗口在图像上逐块扫描,只有通过所有阶段的窗口才被判为人脸。这种设计的好处是前几个阶段就能刷掉大量非人脸区域,速度快;代价是对侧脸、遮挡、光照极端的情况召回率下降。
5.2 完整检测代码
import cv2 # 加载预训练模型,路径按你实际解压位置改 face_cascade = cv2.CascadeClassifier( cv2.data.haarcascades + 'haarcascade_frontalface_default.xml') img = cv2.imread('group.jpg') gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 直方图均衡化,提升暗部对比度,对人脸检测帮助明显 gray = cv2.equalizeHist(gray) faces = face_cascade.detectMultiScale( gray, scaleFactor=1.1, # 每次缩放比例,越小越慢但越全 minNeighbors=5, # 一个候选框要被多少个邻居确认 minSize=(30, 30) # 最小人脸尺寸,过滤小噪点 ) for (x, y, w, h) in faces: cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2) cv2.imshow('Faces', img) cv2.waitKey(0) cv2.destroyAllWindows()逻辑说明:cv2.data.haarcascades是 OpenCV 安装后自带的模型目录,直接用这个前缀能避免路径写错。equalizeHist不是必须的,但在背光或暗光图上效果提升明显。scaleFactor=1.1表示每次把图像缩小 10% 再扫,值越接近 1 检测越细但越慢;minNeighbors=5控制误报,调大能减少假脸但可能漏掉真脸;minSize用来过滤比人脸还小的误检区域。这三个参数是调优的主战场,建议先固定scaleFactor=1.1,再动minNeighbors。
5.3 和边缘、分割怎么串起来
一个实用的串法是:先 OTSU 或 Canny 把感兴趣区域抠出来,缩小检测范围,再在 ROI 上跑 Haar 检测。这样比全图扫描快,也能减少背景误检。比如做实时摄制视频的人脸检测与标注时,可以每隔几帧做一次全图检测,中间帧只在上一帧的人脸框附近做局部检测。这份工程里preprocess.py和dataset.py就是干类似的事,把数据先过一遍再送进模型。
6. 避坑与排查:五个真实翻车记录
6.1 现象:Canny 输出全黑或全白
原因:图像路径读错,cv2.imread返回None,后续cvtColor直接报错或产生空数组;或者阈值设得极端,比如threshold1=0、threshold2=0。解决:读图后立刻print(img.shape)确认不是None;阈值从50/150起步,别一上来就拉满。
6.2 现象:OTSU 阈值算出来是 0 或 255
原因:图像本身就是纯色或接近纯色,直方图只有一个峰,OTSU 找不到有效分割点。解决:先检查图像方差,print(img.std()),如果接近 0 说明图没内容;换一张有前景背景对比的图再试。
6.3 现象:Haar 检测报错!empty() in function 'detectMultiScale'
原因:CascadeClassifier加载 XML 失败,路径不对或文件损坏。解决:用cv2.data.haarcascades拼路径,别手写绝对路径;加载后加一句print(face_cascade.empty()),返回True就是没加载上。
6.4 现象:Gradle 构建卡在下载依赖
原因:工程里虽然放了gradle-6.5-bin.zip,但构建脚本里可能还配了远程仓库,网络不通就卡住。解决:先确认gradle -v能跑,再把构建脚本里的仓库地址换成本地缓存或离线模式,gradle --offline build试试。
6.5 现象:classification训练脚本 import 报错
原因:resnet.py、train_3d.py依赖的深度学习框架没装,或者版本不匹配。解决:先看脚本头部的 import,确认是 PyTorch 还是 TensorFlow,再按对应版本装;别混装两个框架的 GPU 版本,CUDA 版本冲突会让 import 直接失败。
7. 进阶技巧:把三个算子串成一条可验证的流水线
单跑 Canny、OTSU、Haar 都不难,难的是让它们互相配合还不互相拖累。我一般会写一个统一的入口脚本,把预处理、分割、边缘、检测按顺序串起来,每一步的输出都存成中间图,方便回看是哪一步把结果搞砸了。下面这个骨架你可以直接改成自己的:
import cv2 import numpy as np def pipeline(path): img = cv2.imread(path) assert img is not None, '读图失败,检查路径' gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 第一步:高斯模糊,同时服务 Canny 和 OTSU blur = cv2.GaussianBlur(gray, (5, 5), 1.4) # 第二步:OTSU 分割,拿到前景掩膜 _, mask = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # 第三步:Canny 边缘,只在掩膜内保留 edges = cv2.Canny(blur, 50, 150, L2gradient=True) edges = cv2.bitwise_and(edges, edges, mask=mask) # 第四步:Haar 人脸检测,在灰度图上跑 cascade = cv2.CascadeClassifier( cv2.data.haarcascades + 'haarcascade_frontalface_default.xml') faces = cascade.detectMultiScale(gray, 1.1, 5, minSize=(30, 30)) # 把结果画回原图 vis = img.copy() vis[edges > 0] = [0, 0, 255] # 边缘标红 for (x, y, w, h) in faces: cv2.rectangle(vis, (x, y), (x + w, y + h), (0, 255, 0), 2) return vis, mask, edges, faces vis, mask, edges, faces = pipeline('test.jpg') cv2.imwrite('mask.png', mask) cv2.imwrite('edges.png', edges) cv2.imwrite('result.png', vis) print(f'检测到 {len(faces)} 张人脸')这段代码的关键在于顺序:先模糊再分割和边缘,共用同一份预处理结果,避免重复计算;OTSU 的掩膜用来限制 Canny 的搜索范围,减少背景边缘干扰;Haar 检测放在灰度图上跑,因为级联分类器只认灰度。vis[edges > 0] = [0, 0, 255]这行是把边缘像素直接染红,比addWeighted更直观。跑完之后mask.png、edges.png、result.png三张图一对比,你就知道是哪一步出了问题。
验证方法上,我习惯用一张已知人脸位置的图做基准,手动改scaleFactor从 1.05 到 1.3,看len(faces)怎么变;再固定scaleFactor,改minNeighbors从 3 到 8,记录误检和漏检的平衡点。这套流程走下来,你对三个算子的边界就有手感了。从那以后我每次拿到新的图像处理工程,都强制先把中间结果落盘再调参,不再凭感觉改数字。希望帮到你。
本文还有配套的精品资源,点击获取