ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

课堂行为四分类实战:从数据预处理到轻量部署

课堂行为四分类实战:从数据预处理到轻量部署 简介这是一份面向计算机专业本科生及深度学习初学者的课堂行为识别实战项目资源聚焦于“交流、看书、玩手机、睡觉”四类典型课堂状态的图像分类任务适用于毕业设计、课程设计与期末大作业。资源包含完整Python源码14个.py文件含模型训练、推理与GUI逻辑、标注清晰的图像数据集1183张JPG格式样本覆盖多角度、光照与姿态变化、美观易用的PyQt5图形界面9个PNG资源图、配套论文DOCX及环境配置说明TXT整体压缩包共1211个文件大小为101.3MB。已有242人学习下载体现了较强的教学实践认可度。读者可直接部署运行无需额外数据采集或模型调参代码全程中文注释模块划分明确数据预处理→CNN特征提取→Softmax分类→GUI集成并附带真实场景样例图片与.gitignore等工程规范文件显著降低入门门槛与调试成本。1. 为什么课堂行为四分类不是“换个标签就能跑通”的练手项目你拿到一个标着“Python基于深度学习的课堂行为图像四分类项目源码数据集GUI界面论文”的压缩包第一反应可能是不就是把猫狗分类换成“看书/玩手机/睡觉/交流”改个类别数、换套图片、调个learning_rate——三小时交差。但真实落地时90%的人卡在第2天训练准确率卡在68%不上不下GUI一加载图片就崩溃导出的模型在真实教室监控视频里连“睡觉”和“低头看桌”都分不清。这不是数据量不够或显卡太差的问题而是课堂场景天然存在三大反直觉陷阱光照剧烈波动窗帘开合/投影仪亮灭、人体姿态高度相似都是坐着头微低、动作持续时间极短掏手机可能就0.3秒。这个项目的价值恰恰在于它逼你直面这些工业级图像分类的硬骨头——不是教你堆ResNet而是让你亲手把“交流”和“看书”这种语义模糊、边界模糊、样本稀疏的行为变成模型能稳定判别的像素模式。适合两类人一是刚学完PyTorch想验证自己是否真懂训练全流程的进阶新手二是需要快速交付教学行为分析模块、但没时间从零搭pipeline的教务系统工程师。它不承诺“一键部署”但保证你做完后能独立判断这个模型到底能不能用、哪里会翻车、怎么改才有效。2. 从原始图像到可训练张量数据预处理的四个不可跳过的硬核步骤课堂行为图像分类的成败70%取决于数据预处理是否真正“理解”了场景。不是简单 resize normalize 就完事——那只会让模型学会区分“教室窗边亮区”和“后排暗区”而不是“玩手机”和“交流”。下面这四步每一步都对应一个具体问题必须手动验证效果。2.1 用OpenCV做动态ROI裁剪为什么固定尺寸裁剪会毁掉关键特征教室监控画面中学生只占画面1/51/3且位置随机。若直接resize到224×224人脸和手部细节严重失真。正确做法是先检测人脸肩部区域再扩展为包含手部活动空间的ROI。我们不用YOLOv5这类重型检测器而用轻量级cv2.CascadeClassifier配合自适应阈值import cv2 import numpy as np def dynamic_roi_crop(img, scale_factor1.8): # 灰度化 直方图均衡化增强对比度 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) gray cv2.equalizeHist(gray) # 加载预训练人脸检测器比dlib快10倍精度够用 face_cascade cv2.CascadeClassifier(cv2.data.haarcascades haarcascade_frontalface_default.xml) faces face_cascade.detectMultiScale(gray, scaleFactor1.1, minNeighbors5, minSize(30, 30)) if len(faces) 0: # 无脸时 fallback取画面中心区域防全黑/遮挡 h, w img.shape[:2] x, y w//3, h//3 roi img[y:yh//2, x:xw//2] else: # 取最大人脸框扩展scale_factor倍覆盖手部区域 x, y, w, h max(faces, keylambda f: f[2]*f[3]) pad_w, pad_h int(w * (scale_factor-1)/2), int(h * (scale_factor-1)/2) x1 max(0, x - pad_w) y1 max(0, y - pad_h) x2 min(img.shape[1], x w pad_w) y2 min(img.shape[0], y h pad_h) roi img[y1:y2, x1:x2] return cv2.resize(roi, (224, 224)) # 验证对一张原图执行并可视化 orig_img cv2.imread(sample_classroom.jpg) cropped dynamic_roi_crop(orig_img) cv2.imwrite(roi_debug.jpg, cropped) # 手动检查是否裁到了手部区域参数说明scale_factor1.8是经验值——小于1.5则常漏掉手部大于2.0则引入过多背景噪声。minNeighbors5防止误检教室常见反光、书本边缘被当做人脸。关键逻辑不是所有图都有清晰人脸侧脸/戴帽/低头所以必须有fallback机制否则训练时大量样本会因ROI失败而丢弃。2.2 光照归一化用CLAHE替代简单Gamma校正教室灯光自然光混合导致同一学生在不同时间段亮度差异极大。Gamma校正对全局亮度调整无效比如投影仪亮起时屏幕区域过曝人脸却变暗。必须用局部自适应方法def clahe_normalize(img): # 转HSV分离亮度通道 hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) h, s, v cv2.split(hsv) # 对V通道应用CLAHE限制对比度增强 clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8,8)) v_clahe clahe.apply(v) # 合并回HSV转BGR hsv_clahe cv2.merge([h, s, v_clahe]) return cv2.cvtColor(hsv_clahe, cv2.COLOR_HSV2BGR) # 对比效果原图 vs CLAHE处理后 orig_v cv2.split(cv2.cvtColor(orig_img, cv2.COLOR_BGR2HSV))[2] clahe_v cv2.split(cv2.cvtColor(clahe_normalize(orig_img), cv2.COLOR_BGR2HSV))[2] print(f原图V通道标准差: {orig_v.std():.1f}, CLAHE后: {clahe_v.std():.1f}) # 应接近提升20%~30%为什么不用GammaGamma是全局幂函数会同时压暗阴影和过曝高光CLAHE在8×8网格内独立计算直方图保留局部细节。clipLimit2.0是平衡点——大于3.0会产生噪点小于1.5则增强不足。2.3 四类样本的针对性增强策略不是所有类都该用同一种Augmentation“睡觉”类样本极少学生不会总睡且姿态单一头靠桌“玩手机”类手部小目标易在缩放中丢失“交流”类依赖双人相对位置。因此增强策略必须差异化类别必须启用的增强禁用的增强原因说明睡觉RandomRotation(degrees15)HorizontalFlip睡姿左右不对称靠左/右肩玩手机RandomAffine(degrees0, scale(0.9,1.1))ColorJitter手机屏幕反光色温变化大交流RandomResizedCrop(224, scale(0.8,1.0))Grayscale需保持两人肤色对比度看书RandomPerspective(distortion_scale0.2)Cutout书本边缘易被Cutout误删from torchvision import transforms # 定义四类专属transform训练时按label选择 transforms_dict { sleep: transforms.Compose([ transforms.RandomRotation(degrees15), transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]), phone: transforms.Compose([ transforms.RandomAffine(degrees0, scale(0.9,1.1)), transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]), # ... 其他类同理 }血泪经验曾用统一RandomHorizontalFlip导致“交流”类准确率暴跌12%——模型把“两人面对面”和“两人背对背”当成同一类。必须按类别加载transform而非全局设置。2.4 标签平滑与类别权重解决“看书”和“睡觉”的样本不平衡原始数据集中“看书”占比约45%“交流”30%“玩手机”20%“睡觉”仅5%。若直接用CrossEntropyLoss模型会倾向预测“看书”。需双重矫正# 计算每个类别的权重反比于样本数 class_counts [450, 300, 200, 50] # 示例数量 class_weights 1.0 / torch.tensor(class_counts, dtypetorch.float) class_weights class_weights / class_weights.sum() * len(class_counts) # 归一化到总类数尺度 # 标签平滑防止模型对少数类过度自信 def label_smoothing_loss(pred, target, smoothing0.1): log_probs F.log_softmax(pred, dim-1) nll_loss -log_probs.gather(dim-1, indextarget.unsqueeze(1)) nll_loss nll_loss.squeeze(1) smooth_loss -log_probs.mean(dim-1) loss (1.0 - smoothing) * nll_loss smoothing * smooth_loss return loss # 训练循环中使用 criterion torch.nn.CrossEntropyLoss(weightclass_weights.cuda()) # 或用自定义label_smoothing_loss参数选择依据smoothing0.1是起点——大于0.2会使“睡觉”类置信度普遍低于0.3无法触发报警小于0.05则对不平衡改善有限。权重必须用实际数据集统计值不能凭感觉设。3. 模型选型与轻量化改造为什么不用ViT而选EfficientNet-B3注意力门控看到“深度学习”就上ViT在教室监控这种低算力、高实时性场景下ViT的GPU显存占用和推理延迟会让你放弃部署。我们实测过ResNet50/ViT-B16/EfficientNet-B3在Jetson Xavier NX上的表现模型参数量(M)单帧推理(ms)Top-1 Acc(Val)显存占用(MB)ResNet5025.64278.3%1120ViT-B1686.615681.2%2850EfficientNet-B312.22879.6%780但EfficientNet-B3仍有优化空间——它的瓶颈在于最后两层卷积对“手部小目标”敏感度不足。解决方案在Stage-6输出后插入CBAM注意力模块而非替换整个backbone既提升关键区域权重又不增加显著延迟import torch.nn as nn import torch.nn.functional as F class CBAM(nn.Module): def __init__(self, channels, reduction16): super().__init__() self.channel_att nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Conv2d(channels, channels//reduction, 1), nn.ReLU(), nn.Conv2d(channels//reduction, channels, 1), nn.Sigmoid() ) self.spatial_att nn.Sequential( nn.Conv2d(2, 1, 7, padding3), nn.Sigmoid() ) def forward(self, x): # Channel attention ca self.channel_att(x) x_ca x * ca # Spatial attention avg_out torch.mean(x_ca, dim1, keepdimTrue) max_out, _ torch.max(x_ca, dim1, keepdimTrue) sa self.spatial_att(torch.cat([avg_out, max_out], dim1)) return x_ca * sa # 在EfficientNet-B3的features[6]后插入即最后一组MBConv后 from efficientnet_pytorch import EfficientNet model EfficientNet.from_pretrained(efficientnet-b3) # 替换原有classifier插入CBAM model._conv_head nn.Sequential( model._conv_head, CBAM(channels1536), # B3最后一层通道数 nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Dropout(0.3), nn.Linear(1536, 4) )为什么是CBAM不是SESE只做通道注意力会忽略“玩手机”时手部位置信息CBAM叠加空间注意力能定位到手掌区域。reduction16是平衡点——小于8时通道压缩过度大于32则注意力失效。4. GUI界面开发用PyQt5实现“所见即所得”的行为分析流水线GUI不是炫技而是解决三个核心问题1非技术人员能否直接拖入教室监控截图测试2分析结果能否直观反馈到原图3是否支持批量处理并导出CSV报告PyQt5比Tkinter更稳定比Streamlit更适合本地部署。4.1 主窗口布局用QVBoxLayoutQHBoxLayout构建可伸缩工作流import sys from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFileDialog, QGraphicsView, QGraphicsScene, QGraphicsPixmapItem, QTableWidget, QTableWidgetItem, QHeaderView) from PyQt5.QtGui import QPixmap, QImage, QPainter, QColor from PyQt5.QtCore import Qt class BehaviorAnalyzerGUI(QMainWindow): def __init__(self, model): super().__init__() self.model model self.setWindowTitle(课堂行为智能分析系统) self.setGeometry(100, 100, 1200, 800) # 中央widget与主布局 central_widget QWidget() self.setCentralWidget(central_widget) main_layout QVBoxLayout(central_widget) # 顶部控制区水平布局 control_layout QHBoxLayout() self.load_btn QPushButton(加载单张图片) self.load_btn.clicked.connect(self.load_image) self.batch_btn QPushButton(批量分析文件夹) self.batch_btn.clicked.connect(self.batch_analyze) control_layout.addWidget(self.load_btn) control_layout.addWidget(self.batch_btn) main_layout.addLayout(control_layout) # 中间显示区图片结果 display_layout QHBoxLayout() # 左侧原始图标注图 self.graphics_view QGraphicsView() self.scene QGraphicsScene() self.graphics_view.setScene(self.scene) display_layout.addWidget(self.graphics_view, 6) # 60%宽度 # 右侧结果表格置信度 result_layout QVBoxLayout() self.result_table QTableWidget(4, 2) # 4行2列类别、置信度 self.result_table.setHorizontalHeaderLabels([行为类别, 置信度]) self.result_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) result_layout.addWidget(QLabel(分析结果)) result_layout.addWidget(self.result_table) display_layout.addLayout(result_layout, 4) # 40%宽度 main_layout.addLayout(display_layout) # 底部状态栏 self.statusBar().showMessage(就绪) def load_image(self): file_path, _ QFileDialog.getOpenFileName( self, 选择图片, , Image Files (*.png *.jpg *.jpeg) ) if not file_path: return # 加载并预处理 img cv2.imread(file_path) processed dynamic_roi_crop(img) # 复用2.1节函数 processed clahe_normalize(processed) # 复用2.2节函数 # 模型推理 tensor_img transforms.ToTensor()(processed).unsqueeze(0).cuda() with torch.no_grad(): pred self.model(tensor_img) probs F.softmax(pred, dim1)[0].cpu().numpy() # 在原图上绘制预测框和标签 self.display_annotated_image(img, probs, file_path) self.update_result_table(probs) def display_annotated_image(self, orig_img, probs, file_path): # 在原始图上画红框ROI区域和文字标签 h, w orig_img.shape[:2] # ROI坐标复用dynamic_roi_crop中的计算逻辑 gray cv2.cvtColor(orig_img, cv2.COLOR_BGR2GRAY) face_cascade cv2.CascadeClassifier(cv2.data.haarcascades haarcascade_frontalface_default.xml) faces face_cascade.detectMultiScale(gray, 1.1, 5) if len(faces) 0: x, y, w_roi, h_roi max(faces, keylambda f: f[2]*f[3]) cv2.rectangle(orig_img, (x, y), (xw_roi, yh_roi), (0,0,255), 2) pred_label [交流, 看书, 玩手机, 睡觉][np.argmax(probs)] cv2.putText(orig_img, f{pred_label}:{probs.max():.2%}, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0,0,255), 2) # 转QImage显示 rgb_img cv2.cvtColor(orig_img, cv2.COLOR_BGR2RGB) h, w, ch rgb_img.shape bytes_per_line ch * w qt_img QImage(rgb_img.data, w, h, bytes_per_line, QImage.Format_RGB888) pixmap QPixmap.fromImage(qt_img) self.scene.clear() self.scene.addPixmap(pixmap) self.graphics_view.fitInView(self.scene.sceneRect(), Qt.KeepAspectRatio)关键设计点display_annotated_image直接在原始图上绘制避免resize失真fitInView(..., Qt.KeepAspectRatio)保证图片不拉伸表格列宽用QHeaderView.Stretch自适应内容防止中文重叠。4.2 批量分析模块用QThread避免GUI冻结from PyQt5.QtCore import QThread, pyqtSignal class BatchAnalyzer(QThread): progress pyqtSignal(int) # 发送进度百分比 finished pyqtSignal(list) # 发送结果列表 def __init__(self, model, image_paths): super().__init__() self.model model self.image_paths image_paths def run(self): results [] total len(self.image_paths) for i, path in enumerate(self.image_paths): try: img cv2.imread(path) processed dynamic_roi_crop(img) processed clahe_normalize(processed) tensor_img transforms.ToTensor()(processed).unsqueeze(0).cuda() with torch.no_grad(): pred self.model(tensor_img) probs F.softmax(pred, dim1)[0].cpu().numpy() pred_class [交流, 看书, 玩手机, 睡觉][np.argmax(probs)] results.append({ file: os.path.basename(path), behavior: pred_class, confidence: float(probs.max()), details: {k: float(v) for k, v in zip([交流, 看书, 玩手机, 睡觉], probs)} }) except Exception as e: results.append({file: os.path.basename(path), error: str(e)}) self.progress.emit(int((i1)/total*100)) self.finished.emit(results) # 在GUI中调用 def batch_analyze(self): folder QFileDialog.getExistingDirectory(self, 选择图片文件夹) if not folder: return image_paths [os.path.join(folder, f) for f in os.listdir(folder) if f.lower().endswith((.png, .jpg, .jpeg))] self.batch_thread BatchAnalyzer(self.model, image_paths) self.batch_thread.progress.connect(self.update_progress_bar) self.batch_thread.finished.connect(self.on_batch_finished) self.batch_thread.start()为什么必须用QThread模型推理是CPU/GPU密集型任务若在主线程执行GUI会完全卡死。pyqtSignal是线程安全的通信方式比time.sleep()轮询更可靠。5. 避坑指南训练与部署中踩过的7个真实坑及解决方案这些坑不是理论假设而是我在3所学校真实部署时记录的血泪教训。每个都附带现象、根因和可立即执行的修复命令。5.1 现象训练loss下降但val_acc停滞在65%验证集上“睡觉”类全部预测为“看书”原因数据集里“睡觉”样本多为侧脸闭眼而“看书”样本恰好也有大量侧脸低头姿态模型学到的是“侧脸→看书”的错误关联。根本原因是类别间姿态分布重叠度过高未做姿态解耦。解决用OpenPose提取关键点计算头部俯仰角pitch和滚动角roll对“睡觉”类强制筛选 pitch 25°头明显下垂且 roll 5°非歪头的样本删除重叠样本后重新训练# 用OpenPose生成关键点需提前安装 ./build/examples/openpose/openpose.bin \ --image_dir ./dataset/sleep_raw/ \ --write_json ./dataset/sleep_keypoints/ \ --display 0 --render_pose 0 # Python脚本过滤只保留pitch25的json python filter_sleep_pose.py --input_dir ./dataset/sleep_keypoints/ --output_dir ./dataset/sleep_filtered/5.2 现象GUI加载图片后报错CUDA out of memory但命令行推理正常原因PyQt5的事件循环与PyTorch CUDA上下文冲突首次推理时未预热GPU导致内存分配失败。解决在GUI初始化后立即执行一次空推理def __init__(self, model): super().__init__() self.model model # 预热GPU输入dummy tensor触发CUDA context初始化 dummy torch.randn(1, 3, 224, 224).cuda() with torch.no_grad(): _ self.model(dummy) print(GPU预热完成)5.3 现象导出的ONNX模型在OpenCV dnn模块中加载失败报错Unsupported operator aten::adaptive_avg_pool2d原因EfficientNet的AdaptiveAvgPool2d在ONNX opset 11中不被OpenCV支持需opset 12。解决导出时指定opset并替换为固定尺寸AvgPool# 替换model中的AdaptiveAvgPool2d model.avgpool nn.AvgPool2d(kernel_size7, stride1) # B3的feature map是7x7 # 导出ONNX torch.onnx.export( model, torch.randn(1, 3, 224, 224).cuda(), behavior_model.onnx, opset_version12, # 必须12 input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}} )5.4 现象同一张“玩手机”图片在不同光照下预测结果不一致白天→玩手机傍晚→看书原因CLAHE的tileGridSize未适配不同分辨率。原图1920×1080用(8,8)但手机拍摄的小图640×480用同样参数会导致块效应。解决动态计算grid sizedef clahe_normalize_dynamic(img): h, w img.shape[:2] # grid size随分辨率缩放最小4x4最大16x16 grid_h max(4, min(16, h // 64)) grid_w max(4, min(16, w // 64)) clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(grid_w, grid_h)) # ... 后续同前5.5 现象论文中写的“准确率92.3%”但实际部署到教室摄像头时只有73.1%原因论文用的是实验室采集的高质量正面图而真实监控存在运动模糊、低分辨率、镜头畸变。未做域迁移Domain Adaptation。解决用真实监控视频帧微调模型仅最后两层# 冻结backbone只训练classifier for param in model.features.parameters(): param.requires_grad False # 用监控视频抽帧每30秒1帧构建新数据集 # 训练5个epochlr1e-3 optimizer torch.optim.Adam(model._fc.parameters(), lr1e-3)注意微调数据只需200张真实监控图无需标注——用原模型伪标签confidence0.8自动打标。6. 论文写作与模型验证如何把技术细节转化为可信的学术表达写论文不是罗列代码而是回答审稿人最可能质疑的三个问题1你的方法为什么比基线好2结果是否在真实场景下鲁棒3结论能否被其他研究者复现下面给出可直接套用的框架和避坑点。6.1 实验设计表必须包含“真实监控视频”这一行很多学生论文只在自制数据集上对比这是致命缺陷。表格必须包含真实场景验证方法自制数据集Acc监控视频片段Acc推理速度(FPS)模型大小(MB)ResNet5078.3%62.1%24.598.2ViT-B1681.2%59.7%6.8342.1EfficientNet-B3CBAM79.6%73.1%35.278.5关键技巧监控视频片段Acc必须注明来源——例如“采集自XX中学高二3班2023年9月-10月共128小时录像按课时切分随机抽取200段30秒片段”。不写具体来源审稿人会质疑数据真实性。6.2 混淆矩阵可视化用归一化热力图暴露真实弱点不要只贴总体准确率。用seaborn画归一化混淆矩阵重点标注“看书↔睡觉”和“交流↔玩手机”的误判率import seaborn as sns import matplotlib.pyplot as plt # cm是sklearn.metrics.confusion_matrix输出的矩阵 cm_norm cm.astype(float) / cm.sum(axis1)[:, np.newaxis] # 行归一化 plt.figure(figsize(8,6)) sns.heatmap(cm_norm, annotTrue, fmt.2f, xticklabels[交流, 看书, 玩手机, 睡觉], yticklabels[交流, 看书, 玩手机, 睡觉], cmapBlues) plt.title(归一化混淆矩阵) plt.ylabel(真实标签) plt.xlabel(预测标签) plt.savefig(confusion_matrix.pdf, bbox_inchestight)审稿人关注点如果“睡觉→看书”误判率达45%就要在论文中解释——这是因为样本中“睡觉”多为侧脸而“看书”侧脸样本未剔除呼应5.1节的坑。混淆矩阵不是装饰是问题诊断报告。6.3 消融实验证明每个模块的必要性必须做严格的消融Ablation否则“CBAM有效”只是主观断言模型变体Val Acc监控视频Acc参数增量Baseline (EfficientNet-B3)76.2%68.4%0% CLAHE77.5%70.1%0.1% 动态ROI78.3%71.8%0.2% CBAM79.6%73.1%1.8% 标签平滑79.4%72.9%0.0%操作要点每次只增/删一个模块其他超参完全一致。参数增量用thop库计算from thop import profile; macs, params profile(model, inputs(x,))。6.4 部署验证提供可复现的硬件环境清单审稿人会问“你说在Jetson上35FPS用的什么固件” 必须写清硬件NVIDIA Jetson Xavier NX (16GB), Ubuntu 20.04, JetPack 4.6软件PyTorch 1.10.0, torchvision 0.11.1, OpenCV 4.5.4, CUDA 10.2测试条件输入分辨率1280×720batch_size1FP16推理开启终极验证提供Dockerfile非必需但强烈推荐FROM nvcr.io/nvidia/l4t-pytorch:r32.6.1-pth1.10-py3 COPY requirements.txt . RUN pip install -r requirements.txt COPY . /app WORKDIR /app CMD [python, deploy.py]我带过6个本科生做这个课题最后3人成功把模型部署到学校智慧教室系统。他们共同的经验是别急着调参先花两天把数据分布摸透——用Excel统计每类样本的光照等级、人脸角度、手部可见度。那些跳过这步的人后来都在debug上浪费了三周。希望帮到你。本文还有配套的精品资源点击获取
返回列表