Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

基于飞桨的智能课堂行为分析与考试作弊检测系统 - 飞桨AI Studio #125

Open
liuwa666 opened this issue Jun 22, 2023 · 0 comments

Comments

@liuwa666
Copy link
Owner

本项目主要实现了课堂专注度分析与考试作弊检测两个功能,通过对学生的姿态检测,可以有效的辅助老师有效监督学生的学习上课情况,对学生的上课行为进行分析及评分,避免出现课堂不认真听讲、考试作弊等不良的行为;考试作弊检测通过学生在考试的实时监控,在有系统判定的异常行为时,并对其实时抓拍记录

在我们日常的课堂上,教学秩序管理与严防考试作弊一直是在教育上被重视的问题,尤其在疫情反复的时期,许多学校会采用线上上课的方式进行教学,使得教学秩序与考试的监管更加困难,目前的教学秩序监管与考试作弊检测主要还是以人工为主,难免为老师的课堂增加了负担,同时人工考试监控难免有疏漏之处,对于自觉性不高的学生,线上教学模式为课堂教学管理与考试作弊检测更是难上加难,学生学习的效果也易参差不齐,为了进一步利用人工智能技术辅助课堂教学,因此我们使用深度学习技术对学生上课状态进行姿态估计与专注度分析,我们使用具有易用性、本土性、快速业务集成性等众多优点的PaddlePaddle深度学习框架作为我们的开发工具,支持我们项目从模型的构建,模型训练、模型预测等整套开发流程,实现了对学生在课堂上的专注度分析与考试作弊检测的两大功能。

本项目通过对学生的在上课时的头部姿态检测、骨骼关键点检测,对学生的上课状况进行分析评分,并将结果可视化呈现并实时反馈至老师端,实现了课堂行为专注度分析,能够有效地辅助老师监督学生的学习上课情况,及时提醒学生上课认真听讲,避免上课不认真听讲、低头睡觉等不良习惯出现在课堂,从而有效的提高学生在课堂中的学习效率和学习质量;同时,通过学生在考试时的实时监控,当学生存在异常行为时,经由系统判定事件,对该行为进行实时抓拍记录留证,老师可以直接得到学生考试时的行为照片并核验是否有作弊行为,实现对考试实时监测,减轻老师监考的负担,一并督促学生避免出现考试作弊的行为。

从网络摄像头或是本地视频获取视频流,分别进行目标检测、姿态检测、算法评估、数据可视化得到视频流处理后,对分析结果数据可视化

此处建议可以通过对学习率、batch_size进行修改,对迭代次数进行上下调整,尽可能的找到合适的数值使得训练的模型达到最佳效果

In [ ]

Learning Rate:
  Base _lr: 0.06  (学习率)
  schedulers:
  - !CosineDecay
    Max _iters: 29000  (最大的迭代)
  - !Linear Warm up
    Start _factor: 0.33333
    steps: 3000  (预热迭代步数)
Train Reader:
  Inputs _def:
    Image _shape: [3, 300, 300]
    fields: ['image', 'gt _bbox', 'gt _class']
  dataset:
    !COCO Data Set
    Dataset _dir: /home/aistudio/work/data
    Anno _path: annotations/train.json
    Image _dir: /home/aistudio/work/data/images
  Sample _transforms:
  - !Decode Image
    To _rgb: true
  - !Random Distort
    Brightness _lower: 0.875
    Brightness _upper: 1.125
    Is _order: true
  - !Random Expand
    fill _value: [123.675, 116.28, 103.53]
  - !Random Crop
    allow _no_crop: false
  - !NormalizeBox {}
  - !Resize Image
    interp: 1
    target _size: 300
    use_cv2: false
  - !Random Flip Image
    is _normalized: false
  - !Norma lize Image
    mean: [0.485, 0.456, 0.406]
    std: [0.229, 0.224, 0.225]
    is _scale: true
    is _channel_first: false
  - !Permute
    to_bgr: false
    channel_first: true
  batch_size: 32

可以选择rtsp视频流作为视频源,视频通道下摄像头以外的选项在项目文件根目录中有一个video_sources.csv文件,通过编写名称和对应的地址可以添加多个网络摄像头;也可以添加本地视频作为视频源,在项目文件目录的中分有两个文件夹分别是cheating_detection和class_concentration,分别对应作弊检测和课堂专注度分析,将视频文件放入到对应的文件夹中便可在对应的功能模块中使用

In [ ]

def __init__(self, parent=None):
    super(ClassConcentrationApp, self).__init__(parent)
    self.setupUi(self)
    self.video_source = 0
    self.frame_data_list = OffsetList()
    self.opened_source = None
    self.playing = None
    self.playing_real_time = False
    self.pushed_frame = False

        
        
    self.open_source_lock = Lock()
    self.open_source_btn.clicked.connect(
        lambda: self.open_source(self.video_source_txt.text() if len(self.video_source_txt.text()) != 0 else 0))
    self.video_resource_list.itemClicked.connect(lambda item: self.open_source(item.src))
    self.video_resource_file_list.itemClicked.connect(lambda item: self.open_source(item.src))

    self.close_source_btn.clicked.connect(self.close_source)
    self.play_video_btn.clicked.connect(self.play_video)
    self.stop_playing_btn.clicked.connect(self.stop_playing)
    self.video_process_bar.valueChanged.connect(self.change_frame)
    self.push_frame_signal.connect(self.push_frame)

        
        self.draw_img_on_window_signal.connect(self.draw_img_on_window2)
        
        self.init_video_source()

        
        self.x_time_data = []
        self.y_action_data = []
        self.y_face_data = []
        self.y_head_pose_data = []
        self.y_primary_level_data = []
        self.primary_factor = None
        self.draw_img_timer = QTimer(self)
        self.draw_img_timer.timeout.connect(self.refresh_img_on_window)

        
        self.init_rest_window()

        
        def open_source_func(self):
            fps = 12
            self.opened_source = TaskSolution() \
                .set_source_module(VideoModule(source, fps=fps)) \
                .set_next_module(YoloV5Module(yolov5_weight, device)) \
                .set_next_module(AlphaPoseModule(alphapose_weight, device)) \
                .set_next_module(ConcentrationEvaluationModule(classroom_action_weight)) \
                .set_next_module(ClassConcentrationVisModule(lambda d: self.push_frame_signal.emit(d)))
            self.opened_source.start()
            self.playing_real_time = True
            self.open_source_lock.release()

        Thread(target=open_source_func, args=[self]).start()

In [ ]

def init_video_source(self):
        
        VideoSourceItem(self.video_resource_list, "摄像头", 0).add_item()
        
        local_source = 'resource/videos/cheating_detection'
        if not os.path.exists(local_source):
            os.makedirs(local_source)
        else:
            print(f"本地视频目录已创建: {local_source}")
        videos = [*filter(lambda x: x.endswith('.mp4'), os.listdir(local_source))]
        for video_name in videos:
            VideoSourceItem(self.video_resource_file_list,
                            video_name,
                            os.path.join(local_source, video_name),
                            ico_src=':/videos/multimedia.ico').add_item()

        with open('resource/video_sources.csv', 'r', encoding='utf-8') as f:
            reader = csv.reader(f)
            for row in islice(reader, 1, None):
                VideoSourceItem(self.video_resource_list, row[0], row[1],
                                ico_src=':/videos/webcam.ico').add_item()

课堂专注度分析模块有群体专注曲线,头部姿态专注度评分曲线,情绪专注度评分曲线,行为专注度评分曲线,通过头背部姿态估计、传递动作识别等综合统计,抉出最佳行为

In [ ]

def process_data(self, data):
        data.num_of_cheating = 0
        data.num_of_normal = 0
        data.num_of_passing = 0
        data.num_of_peep = 0
        data.num_of_gazing_around = 0
        if data.detections.shape[0] > 0:
            
            data.classes_probs = self.classifier.classify(data.keypoints[:, self.use_keypoints])
            
            data.raw_best_preds = torch.argmax(data.classes_probs, dim=1)
            data.best_preds = [self.reclassify(idx) for idx in data.raw_best_preds]
            data.raw_classes_names = self.raw_class_names
            data.classes_names = self.class_names
            
            data.head_pose = [self.pnp.solve_pose(kp) for kp in data.keypoints[:, 26:94, :2].numpy()]
            data.draw_axis = self.pnp.draw_axis
            data.head_pose_euler = [self.pnp.get_euler(*vec) for vec in data.head_pose]
            
            is_passing_list = CheatingActionAnalysis.is_passing(data.keypoints)
            
            for i in range(len(data.best_preds)):
                if data.best_preds[i] == 0:
                    if is_passing_list[i] != 0:
                        data.best_preds[i] = 1
                    elif data.head_pose_euler[i][1][0] < peep_threshold:
                        data.best_preds[i] = 2
            data.pred_class_names = [self.class_names[i] for i in data.best_preds]
            
            data.num_of_normal = data.best_preds.count(0)
            data.num_of_passing = data.best_preds.count(1)
            data.num_of_peep = data.best_preds.count(2)
            data.num_of_gazing_around = data.best_preds.count(3)
            data.num_of_cheating = data.detections.shape[0] - data.num_of_normal

        return TASK_DATA_OK

对从视频源获取的人脸关键点、头背部姿态等信息对上课状态进行分类

In [ ]

def class_action_reclassify(self, action_preds):
        """
        重新分类课堂动作标签
        """
        reclassified_preds = np.empty_like(action_preds)
        for lbl, new_class in enumerate(self.reclassified_class_actions):
            reclassified_preds[action_preds == lbl] = new_class
        min_len = self.action_fuzzy_matrix.shape[0]
        result = np.eye(min_len)[reclassified_preds]
        count_vec = np.bincount(reclassified_preds, minlength=min_len)
        return result, count_vec

    def face_action_reclassify(self, face_preds, face_hidden=None):
        """
        重新分类面部疲劳标签
        0  "nature" 1   "happy" 2   "confused" 3 "amazing"
        """
        result = np.empty_like(face_preds)
        result[(face_preds == 1) | (face_preds == 3)] = 1
        result[face_preds == 0] = 2
        result[face_preds == 2] = 3

        if face_hidden is not None:
            result[face_hidden] = 0
        min_len = self.face_fuzzy_matrix.shape[0]
        count_vec = np.bincount(result, minlength=min_len)
        return np.eye(min_len)[result], count_vec

    def head_pose_reclassify(self, head_pose_preds, face_hidden=None):
        """
        离散化分类头部角度
        """
        print(head_pose_preds.flatten().tolist())
        discretization_head_pose = np.empty_like(head_pose_preds, dtype=np.int64)
        for d1, d2, lbl in self.head_pose_section:
            discretization_head_pose[(d1 < head_pose_preds) & (head_pose_preds <= d2)] = lbl
        if face_hidden is not None:
            discretization_head_pose[face_hidden] = 0

        
        count_ = np.array([np.count_nonzero(discretization_head_pose == 1),
                           np.count_nonzero(discretization_head_pose == 2)])
        sum_count_ = np.sum(count_)
        count_ = np.array([0, 1]) if sum_count_ == 0 else count_ / sum_count_
        

        encode = np.array([
            [1, 0, 0, 0, 0, 0],
            [0, count_[1], 0, count_[0], 0, 0],
            [0, 0, count_[1], 0, count_[0], 0],
            [0, 0, 0, 0, 0, 1]
        ])
        discretization_head_pose = discretization_head_pose.flatten()
        result = encode[discretization_head_pose]
        min_len = self.head_pose_fuzzy_matrix.shape[0]
        count_vec = np.bincount(discretization_head_pose, minlength=min_len)

        return result, count_vec

通过基于关键点的伸手识别,判断考试异常行为中传纸条动作,头部姿态估计同样应用于这里的低头偷看异常行为

In [ ]

def stretch_out_degree(keypoints, left=True, right=True, focus=None):
        """
        :param focus: 透视焦点
        :param keypoints: Halpe 26 keypoints 或 136关键点 [N,keypoints]
        :param left: 是否计算作弊的伸手情况
        :param right: 是否计算右臂的伸手情况
        :return: ([N,left_hand_degree],[N,left_hand_degree]), hand_degree = (arm out?,forearm out?,straight arm?)
        """
        if focus is None:
            shoulder_vec = keypoints[:, 6] - keypoints[:, 5]
        else:
            shoulder_vec = (keypoints[:, 6] + keypoints[:, 5]) / 2 - focus
        result = []
        if left:
            arm_vec = keypoints[:, 5] - keypoints[:, 7]
            forearm_vec = keypoints[:, 7] - keypoints[:, 9]
            _results = torch.hstack([torch.cosine_similarity(shoulder_vec, arm_vec).unsqueeze(1),
                                     torch.cosine_similarity(shoulder_vec, forearm_vec).unsqueeze(1),
                                     torch.cosine_similarity(arm_vec, forearm_vec).unsqueeze(1)])
            result.append(_results)
        if right:
            shoulder_vec = -shoulder_vec
            arm_vec = keypoints[:, 6] - keypoints[:, 8]
            forearm_vec = keypoints[:, 8] - keypoints[:, 10]
            _results = torch.hstack([torch.cosine_similarity(shoulder_vec, arm_vec).unsqueeze(1),
                                     torch.cosine_similarity(shoulder_vec, forearm_vec).unsqueeze(1),
                                     torch.cosine_similarity(arm_vec, forearm_vec).unsqueeze(1)])
            result.append(_results)
        return result

In [ ]

def is_passing(keypoints):
        """
        是否在左右传递物品
        :param keypoints: Halpe 26 keypoints 或 136关键点 [N,keypoints]
        :return: [N,左右传递?]+1 左传递,-1 右边传递 0 否
        """
        irh = CheatingActionAnalysis.is_raise_hand(keypoints)
        stretch_out_degree_L, stretch_out_degree_R = CheatingActionAnalysis.stretch_out_degree(keypoints)
        isoL = CheatingActionAnalysis.is_stretch_out(stretch_out_degree_L)
        isoR = CheatingActionAnalysis.is_stretch_out(stretch_out_degree_R)

        left_pass = isoL & ~irh[:, 0]  
        left_pass_value = torch.zeros_like(left_pass, dtype=int)
        left_pass_value[left_pass] = 1
        right_pass = isoR & ~irh[:, 1]  
        right_pass_value = torch.zeros_like(right_pass, dtype=int)
        right_pass_value[right_pass] = -1
        return left_pass_value + right_pass_value

对经过视频中人物行为的分析分类进行分级评价,从而实现对课堂人物行为的评分以及对实时数据曲线图的更新

In [ ]

def evaluate(self, action_preds: ndarray,
                 face_preds: ndarray,
                 head_pose_preds: ndarray,
                 face_hidden: ndarray = None) -> ConcentrationEvaluation:
        
        self.action_preds, self.action_count = self.class_action_reclassify(action_preds)
        self.face_preds, self.face_count = self.face_action_reclassify(face_preds, face_hidden)
        self.head_pose_preds, self.head_pose_count = self.head_pose_reclassify(head_pose_preds, face_hidden)
        
        self.action_levels = self.action_preds @ self.action_fuzzy_matrix @ self.evaluation_level
        self.face_levels = self.face_preds @ self.face_fuzzy_matrix @ self.evaluation_level
        self.head_pose_levels = self.head_pose_preds @ self.head_pose_fuzzy_matrix @ self.evaluation_level

        self.secondary_levels = np.hstack([
            self.action_levels[..., np.newaxis],
            self.face_levels[..., np.newaxis],
            self.head_pose_levels[..., np.newaxis]
        ])

        
        self.action_info_entropy = self.info_entropy(self.action_count / np.sum(self.action_count))
        self.face_info_entropy = self.info_entropy(self.face_count / np.sum(self.face_count))
        self.head_pose_info_entropy = self.info_entropy(self.head_pose_count / np.sum(self.head_pose_count))

        self.primary_factor = self.softmax(np.array([self.action_info_entropy,
                                                     self.face_info_entropy,
                                                     self.head_pose_info_entropy]))
        
        self.primary_levels = self.secondary_levels @ self.primary_factor
        return ConcentrationEvaluation(self.primary_levels,
                                       self.primary_factor,
                                       self.secondary_levels)

In [ ]

    def add_data_to_list(self, data):
        """
        将数据绘制到界面图像得到位置
        """
        try:
            time_process = second2str(data.time_process)
            max_idx = len(self.x_time_data) - 1
            if max_idx >= 0 and time_process == self.x_time_data[max_idx]:
                return
                
            self.x_time_data.append(time_process)

            concentration_evaluation: ConcentrationEvaluation = data.concentration_evaluation
            secondary_mean_levels = np.mean(concentration_evaluation.secondary_levels, axis=0)

            self.y_action_data.append(secondary_mean_levels[0])
            self.y_face_data.append(secondary_mean_levels[1])
            self.y_head_pose_data.append(secondary_mean_levels[2])
            self.y_primary_level_data.append(np.mean(concentration_evaluation.primary_levels))

            while len(self.x_time_data) > self.line_data_limit_spin.value():
                for i in [self.x_time_data,
                          self.y_action_data,
                          self.y_face_data,
                          self.y_head_pose_data,
                          self.y_primary_level_data]:
                    i.pop(0)
            self.primary_factor = concentration_evaluation.primary_factor

            self.pushed_frame = True

        except Exception as e:
            print("add_data_to_list", e)

为达到软件的轻量化与保持在不同设备上均能使用的较高兼容性,我们使用PyQt5编写了GUI用于我们的项目效果展示,将处理后反馈的视频流与分析检测结果可视化,这里我们设置了单镜头与连接多镜头角度下的行为分析效果与考试作弊检测测试。

在该模块当中,我们在界面左边部分设置有视频源,远程摄像头视频通道和本地视频都可以播放视频。在主界面的下方和右方分别有群体专注曲线,头部姿态专注度评分曲线,情绪专注度评分曲线,行为专注度评分曲线,将学生的课堂行为做出判断分析与数据的可视化。

在考试作弊检测模块中,界面的中间是视频播放的位置,左侧设置依然是视频源,视频流播放界面下方是实时抓拍记录学生疑似考试作弊行为的展示框,能够对视频中学生出现的异常的行为进行抓拍记录。目前支持的有四中状态:正常、东张西望、低头偷看和传纸条。界面的右侧分别是作弊发生的时间统计数据和异常行为时间曲线,分析时间段之间发生异常行为的频率,方便统计发生作弊动作次数与时间上的规律。在图片的下方记录了异常行为检测记录的时间点与发生的行为,点击图片可以返回记录的时间点进行人为进行判断。

学生课堂异常行为检测与分析系统基本能够达到预期所希望的效果,能够代替人工监控,达到主动监控,同时没有人工监控所存在的问题,比如说视觉疲劳、精神疲劳。能提减小外在因素的影响,并且能够对作弊的情况、异常行为进行保存,不用再去回放录像,十分的方便,但是当前我们所做的工作仍然是有限的,系统的可拓展性非常大,仍具有向上向大的方向发展的潜能,所以我对于我们项目的未来落地应用满怀期待,我们会继续改进我们的工作:

  • 尽可能优化系统,以至能够实时显示摄像头所录到的画面,然后进行实时的反馈。同时数据也能够进行实时的处理。以达到可靠又高效运行的目的。
  • 需要经过不同方式的测试来保证系统能够稳定的运行,不会出现异常的情况。从而确保系统具有一定的稳定性。
  • 为系统设计合适的UI界面,以确保不会使用的人也能够轻易的快速使用,缩短用户对系统的熟悉过程,来达到系统的易用性。
  • 系统需能够在不同的电脑系统下运行,具有一定的兼容性,为更多用户提供服务。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant