恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
OpenCV+Keras多模态目标检测:CASIS数据集教学项目实战
首页
资讯中心
/
OpenCV+Keras多模态目标检测:CASIS数据集教学项目实战
OpenCV+Keras多模态目标检测:CASIS数据集教学项目实战
发布时间:2026/9/15 16:01:04
简介这是一套面向高校计算机专业学生的开源教学项目聚焦OpenCV、Python、TensorFlow/Keras在CASIS多模态图像数据集上的处理与目标检测实践。资源共220个文件、约41.23MB包含可运行的Python源码、Jupyter Notebook分步笔记、jpg/png图像样本、xml标注文件、avi/mp4演示视频以及环境配置说明和PDF参考资料覆盖数据准备、模型训练、检测演示的完整链路。目前已有60人学习使用适合需要完成课程设计、毕业设计或系统入门计算机视觉与深度学习的读者。通过该资源学习者既能理解多模态图像预处理、目标边界框标注与特征提取的原理也能跟进代码实践掌握TensorFlow/Keras模型构建和OpenCV调用的工程方法项目目录清晰、代码注释较完整便于分模块拆解和二次扩展是产教融合场景下不可多得的教学参考。1. 从 CASIS 数据集出发的多模态目标检测教学项目期末周最常见的画面不是考试而是计算机视觉课的大作业答辩现场有人抱着 YOLO 权重文件跑了个 demo有人用 OpenCV 做了个滤镜合集真正能把“图像预处理、深度模型、目标检测、结果评估”串成一条完整链路的作业并不多。你手里这个标题指向的正是这样一条链路——以 CASIS 数据集为输入用 OpenCV-Python 完成多模态图像读取与预处理再交给 TensorFlow/Keras 搭建的检测模型做分类与定位最后形成一个能演示、能评估、能写进简历的开源教学项目。它不追求刷爆公开榜单而是把计算机视觉学习路线里最关键的几个环节数据管道、模型设计、训练调试、结果验证一次走通。适合第一次接触深度学习的本科生也适合想从传统图像处理跨到深度方向的工程师。2. 环境搭建与 OpenCV-Python 多模态图像预处理管道2.1 Anaconda 创建虚拟环境与 OpenCV、TensorFlow、Keras 安装拿到项目第一件事不是读代码而是先把环境隔离出来。高校机房和实验室的 Python 环境往往被不同课程反复装过包直接pip install可能把某个依赖升上去然后弄坏另一个作业。我一般会在 Anaconda 里为这个项目单独建一个虚拟环境Python 版本固定在 3.10 附近——这个版本对 TensorFlow 2.x 和 opencv-python 的兼容性都比较稳。conda create -n cvlab python3.10 -y conda activate cvlab pip install opencv-python opencv-contrib-python pip install tensorflow2.10.* pip install numpy matplotlib pandas scikit-learn注意这里没有单独装 Keras因为从 TensorFlow 2.0 开始Keras 已经作为tf.keras内置在 TensorFlow 里。网上很多老教程让你先装 tensorflow 再装 keras结果版本不匹配导致tf.keras和keras两个包互相覆盖这是常见的ModuleNotFoundError: No module named keras根源之一。装完可以用一个很短的 Python 命令验证环境是否可用python -c import cv2; print(cv2.__version__); import tensorflow as tf; print(tf.__version__)如果输出 OpenCV 4.x 和 TensorFlow 2.10 两个版本号说明环境就绪。此时如果提示缺少libGL.so.1之类的问题通常发生在 Linux 服务器上执行apt install libgl1 libglib2.0-0即可。Windows 上一般不会遇到这种依赖但要注意 opencv-python 和 opencv-contrib-python 不能同时存在否则会出现AttributeError: module cv2 has no attribute SIFT_create这类冲突。2.2 OpenCV 读取多模态图像并拼接成多通道输入CASIS 数据集在这个教学项目语境下通常以“同场景多源图像”的形式组织常见模态包括可见光 RGB 图像和红外灰度图像。目标检测算法如果只把 RGB 图像送进网络相当于主动丢掉了红外模态里的温度特征反过来如果只用红外又丢失了色彩和纹理信息。多模态处理的核心问题就是怎么把不同来源、不同通道数的图像变成一个深度学习模型能消费的张量。import cv2 import numpy as np height, width 416, 416 rgb cv2.imread(samples/001_vis.jpg) # RGB 三通道 ir cv2.imread(samples/001_ir.jpg, cv2.IMREAD_GRAYSCALE) # 红外单通道 rgb cv2.resize(rgb, (width, height)) ir cv2.resize(ir, (width, height)) clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) ir_eq clahe.apply(ir) ir_3ch cv2.merge([ir_eq, ir_eq, ir_eq]) multi_input np.concatenate([rgb, ir_3ch], axis-1) # shape: (416, 416, 6) multi_input_norm multi_input.astype(np.float32) / 255.0这段代码做了四件事按统一尺寸缩放、对红外单通道做 CLAHE 对比度增强、把单通道复制成三通道、在通道维度上拼接。cv2.createCLAHE里的clipLimit控制对比度限制幅度值越大增强越明显但过大容易出现块状伪影tileGridSize是局部直方图均衡化的分块大小8×8 是默认值对于 416×416 的小图来说不需要改。通道拼接完成后得到的是 6 通道输入此时搭建 Keras 模型时输入形状必须写成(416, 416, 6)。很多初学者在这里犯的错是图像明明是 6 通道模型第一层却写成input_shape(416, 416, 3)训练直接报Input 0 of layer sequential is incompatible。如果你的 CASIS 版本只有单模态 RGB把ir_3ch换成一个全零张量或者直接去掉参与拼接即可但注意去掉后模型输入维度要同步改回 3。2.3 多模态图像配准从特征点匹配到简单兜底同一个场景的两张图像如果拍摄时间、传感器角度存在偏差直接拼接会让目标边缘出现重影。严格的做法是用 ORB 特征点提取加单应性矩阵变换做配准但教学项目的数据集大多已经做过粗对齐这里只需要做兜底检查。def align_to_rgb(rgb, other): orb cv2.ORB_create(nfeatures1000) kp1, des1 orb.detectAndCompute(rgb, None) kp2, des2 orb.detectAndCompute(other, None) bf cv2.BFMatcher(cv2.NORM_HAMMING, crossCheckTrue) matches bf.match(des1, des2) matches sorted(matches, keylambda x: x.distance)[:50] if len(matches) 10: return cv2.resize(other, (rgb.shape[1], rgb.shape[0])) src_pts np.float32([kp2[m.trainIdx].pt for m in matches]).reshape(-1, 1, 2) dst_pts np.float32([kp1[m.queryIdx].pt for m in matches]).reshape(-1, 1, 2) H, _ cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0) return cv2.warpPerspective(other, H, (rgb.shape[1], rgb.shape[0]))逻辑是先用 ORB 找两张图的特征点和描述子用汉明距离暴力匹配筛出距离最小的前 50 个匹配对匹配对数少于 10 就认为找不到可靠关系直接缩放兜底。cv2.findHomography里的cv2.RANSAC和阈值 5.0 表示用随机采样一致性估计变换矩阵并拒绝误差大于 5 像素的误匹配。这个函数在数据已经对齐时不会产生明显变化但在某些模态之间存在偏移的样本上能把目标轮廓的对齐误差从几十像素压到几个像素。3. TensorFlow 与 Keras 目标检测模型设计与训练3.1 教学项目为什么先不直接上 YOLO 或 SSD看到“目标检测”四个字很多学生第一反应是把 YOLOv5 或 YOLOv8 的权重拉下来直接跑。这个思路在演示环节确实省事但放在教学项目里有三个问题一是模型结构与损失函数高度封装很难在报告中讲清楚每一个模块的作用二是想要微调离不开大量数据和 GPU 资源学生机跑不动三是 CASIS 这类小规模数据集用大模型训练几乎必然过拟合。我建议在课程设计里先做一个简化版检测模型——用分类头输出目标的类别用回归头输出目标的边界框坐标。这个框架麻雀虽小但卷积、迁移学习、多任务损失、坐标回归这些核心概念都能覆盖之后切到 YOLO 理解起来也更顺。3.2 用 Keras 定义 MobileNetV2 主干与双输出检测头模型主干选择 MobileNetV2 而不是 ResNet50核心考虑是参数量。ResNet50 有超过 2300 万参数在 CPU 上训练一轮要数十分钟MobileNetV2 只有 350 万参数CPU 也能扛住。既然数据集规模不大这种轻量主干反而更合适。代码里首先加载去掉分类层的 MobileNetV2然后把它的输出接上自己的检测头。import tensorflow as tf from tensorflow.keras import layers, models def build_detector(input_shape(416, 416, 6), num_classes3): base tf.keras.applications.MobileNetV2( input_shapeinput_shape, include_topFalse, weightsNone ) base.trainable True x layers.GlobalAveragePooling2D()(base.output) x layers.Dense(128, activationrelu)(x) x layers.Dropout(0.3)(x) cls_out layers.Dense(num_classes, activationsoftmax, namecls)(x) box_out layers.Dense(4, activationsigmoid, namebox)(x) model models.Model(inputsbase.input, outputs[cls_out, box_out]) return model两个输出头的设计意图很明确cls_out的softmax输出 3 个类别的概率分布box_out的sigmoid输出 4 个值表示归一化后的中心点坐标和宽高。为什么用sigmoid而不用线性激活因为边界框坐标在预处理时已经除以图像尺寸归一化到 0 到 1 之间sigmoid的输出范围正好匹配还能避免早期训练时预测出负坐标或超过图像边界的坐标。weightsNone表示不加载预训练权重如果你的 CASIS 图分布和 ImageNet 差得远从零训练反而更可控等模型结构调通后再换成weightsimagenet做迁移学习通常能再涨几个点。3.3 多任务损失、学习率策略与训练参数表这个模型有两个输出compile 时要同时对两个输出指定损失函数和权重model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-4), loss{ cls: tf.keras.losses.SparseCategoricalCrossentropy(), box: tf.keras.losses.MeanSquaredError() }, loss_weights{cls: 1.0, box: 4.0}, metrics{cls: accuracy} )分类头用稀疏交叉熵尺寸与cls_out的softmax匹配回归头用 MSE衡量预测框和真实框的 L2 距离。loss_weights里 box 权重设为 4.0是因为分类损失的数值通常收敛到 0.1 量级而边界框损失在 0.01 量级不给 box 更大的权重梯度会被分类任务完全淹没。这里没有用 Smooth L1是考虑到教学项目的数据标注噪声不大MSE 梯度行为更直观。训练阶段还必须配置回调否则训练到一半 loss 震荡你只能手动 CtrlC回调参数建议作用ModelCheckpointmonitorval_loss, save_best_onlyTrue每轮结束保存验证集最优权重EarlyStoppingpatience15, restore_best_weightsTrue验证 loss 连续 15 个 epoch 不下降就停ReduceLROnPlateaufactor0.5, patience5验证 loss 停滞后学习率减半callbacks [ tf.keras.callbacks.ModelCheckpoint(best.h5, monitorval_loss, save_best_onlyTrue), tf.keras.callbacks.EarlyStopping(patience15, restore_best_weightsTrue), tf.keras.callbacks.ReduceLROnPlateau(factor0.5, patience5) ] history model.fit( train_gen, validation_dataval_gen, epochs80, callbackscallbacks )patience15对于小数据集来说足够宽裕不会因为单次抖动就提前停止restore_best_weightsTrue确保停止后模型回到验证 loss 最低的那一轮权重而不是最后一轮。batch_size具体值在 Generator 中设置一般 8 到 16取决于你的显存大小。4. CASIS 数据集标注解析与数据增强策略4.1 教学项目中常见的 CSV 标注方案与解析CASIS 原始数据集的标注格式如果只有官方文档里才有教学项目通常会把他转换成简易 CSV 来降低上手门槛。CSV 每一行对应一个目标实例包含五个字段图像文件名、类别编号、归一化后的边界框坐标。要注意这里的归一化是除以图像原始宽高模型训练时还要再做一次适配。import pandas as pd # 生成训练集标注 DataFrame df pd.read_csv(annotations_train.csv) # 列名: image, cls_id, x1, y1, x2, y2坐标已除以原图宽高范围 0~1 print(df.head()) def load_image_and_label(row, base_dirdata/): path base_dir row[image] img cv2.imread(path) h, w img.shape[:2] img cv2.resize(img, (416, 416)) cls int(row[cls_id]) x1, y1, x2, y2 row[x1], row[y1], row[x2], row[y2] # 统一转成 中心点 宽高 格式方便模型回归 cx ((x1 x2) / 2.0) cy ((y1 y2) / 2.0) bw (x2 - x1) bh (y2 - y1) return img, cls, np.array([cx, cy, bw, bh], dtypenp.float32)注意这里把 x1、y1、x2、y2 转成了中心点和宽高这和前面模型box_out的 4 个输出一一对应。很多学生直接让模型回归 x1、y1、x2、y2结果发现收敛非常慢原因在于左上角坐标和右下角坐标之间的相关性很强模型要同时学两组互相牵制的值。改成中心点加宽高之后每个输出相对独立回归任务简单得多。4.2 自定义 Sequence 生成器实现多模态读取model.fit不能直接吃 DataFrame需要写一个继承tf.keras.utils.Sequence的生成器。Sequence的好处是它实现了多进程安全的数据预取并且在每个 epoch 结束时可以原地打乱数据顺序class MultiModalGenerator(tf.keras.utils.Sequence): def __init__(self, df, base_dir, batch_size8, shuffleTrue): self.df df.reset_index(dropTrue) self.base_dir base_dir self.batch_size batch_size self.shuffle shuffle self.on_epoch_end() def __len__(self): return int(np.ceil(len(self.df) / self.batch_size)) def __getitem__(self, idx): batch self.df.iloc[idx * self.batch_size : (idx 1) * self.batch_size] images, cls_list, box_list [], [], [] for _, row in batch.iterrows(): img, cls, box load_image_and_label(row, self.base_dir) # 增强开关由类内逻辑控制 img, box self.augment(img, box) images.append(img / 255.0) cls_list.append(cls) box_list.append(box) X np.stack(images) y {cls: np.array(cls_list), box: np.array(box_list)} return X, y def on_epoch_end(self): if self.shuffle: self.df self.df.sample(frac1).reset_index(dropTrue)4.3 随机翻转时边界框坐标必须同步修正数据增强是目标检测教学项目里最容易出错的一环。普通分类任务的增强只涉及图像本身改错了最多效果差一点目标检测里如果图像做了翻转而边界框坐标没有跟着变模型就会用错标签学习指标看着正常可视化输出全乱。水平翻转的坐标变换规律是翻转后新 x 坐标 图像宽度 - 翻转前 x 坐标且 x1 和 x2 要互换保证左上角小于右下角。def augment(self, img, box): # 水平翻转概率 0.5 if np.random.rand() 0.5: img cv2.flip(img, 1) cx, cy, bw, bh box cx 1.0 - cx box np.array([cx, cy, bw, bh]) # 宽高不受影响 # 亮度扰动只作用在第一个三通道部分 if np.random.rand() 0.3: img[:, :, :3] cv2.convertScaleAbs(img[:, :, :3], alpha1.0, betanp.random.randint(-20, 20)) return img, box这个函数里cv2.flip(img, 1)的水平翻转对多模态图像同样有效因为 6 个通道一起翻转不会破坏模态间对齐关系但中心点 x 坐标必须用1.0 - cx映射。垂直翻转也可以同理处理只是把cy改成1.0 - cy翻转参数从 1 改成 0。亮度扰动只做在[:, :, :3]上即只扰动可见光部分保留红外通道数值稳定。这是多模态与单模态增强的关键差异红外图像的灰度绝对值代表温度信息随便加亮度扰动会让模型学到错误的温度映射。5. 推理可视化与 IoU 评估的教学落地技巧5.1 用 OpenCV 在原图上画预测框与类别标签模型训练完成后推理代码通常写成独立脚本。预测输出要经过一次“反归一化”才能以像素坐标画到图上def visualize_predict(model, img, class_names, conf_threshold0.5): h, w img.shape[:2] input_tensor cv2.resize(img, (416, 416)).astype(np.float32) / 255.0 input_tensor np.expand_dims(input_tensor, axis0) cls_pred, box_pred model.predict(input_tensor, verbose0) cls_id np.argmax(cls_pred[0]) conf np.max(cls_pred[0]) if conf conf_threshold: return img cx, cy, bw, bh box_pred[0] x1 int((cx - bw / 2) * w) y1 int((cy - bh / 2) * h) x2 int((cx bw / 2) * w) y2 int((cy bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) label f{class_names[cls_id]}: {conf:.2f} cv2.putText(img, label, (x1, max(0, y1 - 8)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) return imgcv2.rectangle的线宽设为 2 是为了在小尺寸图像上依然醒目cv2.putText的坐标放在框上方如果 y1 已经接近 0取max(0, y1 - 8)防止文字被截掉。画框前把浮点坐标int()转换是必要的因为 OpenCV 的绘制函数不接受浮点像素坐标类型错误会直接抛异常。5.2 用 IoU 阈值评估定位质量训练集的准确率只能说明模型记住了多少训练样本教学项目指标至少要算 IoU。流程是对每张验证集图像做预测与真实框计算 IoU大于 0.5 记为正确检测再按类别统计 precision 和 recalldef compute_iou(box_a, box_b): # 输入格式: [cx, cy, bw, bh] 0~1 归一化坐标 xa1, ya1 box_a[0] - box_a[2] / 2, box_a[1] - box_a[3] / 2 xa2, ya2 box_a[0] box_a[2] / 2, box_a[1] box_a[3] / 2 xb1, yb1 box_b[0] - box_b[2] / 2, box_b[1] - box_b[3] / 2 xb2, yb2 box_b[0] box_b[2] / 2, box_b[1] box_b[3] / 2 inter_x1 max(xa1, xb1) inter_y1 max(ya1, yb1) inter_x2 min(xa2, xb2) inter_y2 min(ya2, yb2) inter_w max(0, inter_x2 - inter_x1) inter_h max(0, inter_y2 - inter_y1) inter_area inter_w * inter_h area_a box_a[2] * box_a[3] area_b box_b[2] * box_b[3] union area_a area_b - inter_area return inter_area / union if union 0 else 0.0IoU 计算的全部关键在于交集宽高取max(0, ...)两个框不相交时交集面积为 0此时直接除一个正数分母得到 0。这套评估代码在之后的 YOLO 迁移中可以直接复用因为目标检测评估本质上与具体模型结构无关。教学项目答辩中最容易被追问的也正是这个指标的计算方式和阈值选取理由能说清 IoU 的完整计算过程比跑通模型更有说服力。本文还有配套的精品资源点击获取