Browse Source

12355

master
lichong 8 months ago
parent
commit
c4ab7067a4
  1. BIN
      api/exam.pt
  2. 55
      api/main.py
  3. 8
      front/src/api/teacher.js
  4. 49
      front/src/component/changeItem.vue
  5. 10
      front/src/component/table.vue
  6. 66
      front/src/views/student.vue
  7. 100
      front/src/views/teacher.vue

BIN
api/exam.pt

Binary file not shown.

55
api/main.py

@ -1,8 +1,13 @@
from flask import Flask, request, jsonify from flask import Flask, request, jsonify
from pymongo import MongoClient from pymongo import MongoClient
from ultralytics import YOLO
from PIL import Image
import base64
from io import BytesIO
from bson import ObjectId from bson import ObjectId
import hashlib import hashlib
import json import json
import os
from flask import Flask, request from flask import Flask, request
from flask_socketio import SocketIO, emit, join_room, leave_room from flask_socketio import SocketIO, emit, join_room, leave_room
import time import time
@ -20,6 +25,12 @@ clients = []
adminSid = "" adminSid = ""
# 初始化考试状态 # 初始化考试状态
studentStatus = {} studentStatus = {}
current_dir = os.path.dirname(os.path.abspath(__file__))
weights = os.path.join(current_dir, "exam.pt")
print(89744, weights)
model = YOLO(weights)
frame_rate_divider = 300 # 设置帧率除数
frame_count = 0 # 初始化帧计数器
def dealStudentStatus(): def dealStudentStatus():
@ -60,12 +71,50 @@ def handle_video_frame(message):
# 广播视频帧给老师 # 广播视频帧给老师
global adminSid global adminSid
global studentStatus global studentStatus
global model
global frame_rate_divider
global frame_count
# print(f"Received video frame from {message['userId']}") # print(f"Received video frame from {message['userId']}")
socketio.emit( socketio.emit(
"teacherVideo" + message["userId"], message, to=adminSid, namespace="/ws/video" "teacherVideo" + message["userId"], message, to=adminSid, namespace="/ws/video"
) )
if studentStatus[message["userId"]]: if frame_count % frame_rate_divider == 0:
print(f"学生{message['userId']}已作弊", f"{studentStatus[message['userId']]}") # 这里省略了实际的base64数据
base64_str = message["data"]
# 解码base64字符串
_, img_data = base64_str.split(",")
img_data = base64.b64decode(img_data)
# 将字节流转换为PIL图像对象
image = Image.open(BytesIO(img_data))
results = model.predict(source=image, iou=0.5, conf=0.25)
det = results[0]
checkVedioResult(det)
frame_count += 1 # 更新帧计数器
frame_count %= frame_rate_divider # 更新帧计数器
# print(99777,frame_count)
# if studentStatus[message["userId"]]:
# print(f"学生{message['userId']}已作弊", f"{studentStatus[message['userId']]}")
def checkVedioResult(det):
# 如果有有效的检测结果
# {"cheating": "疑似作弊", "good": "良好", "normal": "正常"}
if det is not None and len(det):
results = [] # 初始化结果列表
for res in det.boxes:
for box in res:
# 提前计算并转换数据类型
class_id = int(box.cls.cpu())
bbox = box.xyxy.cpu().squeeze().tolist()
bbox = [int(coord) for coord in bbox] # 转换边界框坐标为整数
result = {
"bbox": bbox, # 边界框
"score": box.conf.cpu().squeeze().item(), # 置信度
"class_id": class_id, # 类别ID
}
results.append(result) # 将结果添加到列表
print(587744, results)
@socketio.on("talk", namespace="/ws/video") @socketio.on("talk", namespace="/ws/video")
@ -241,6 +290,7 @@ def insert_data():
"banji": userJson["banji"], "banji": userJson["banji"],
"xueshengxingming": userJson["xueshengxingming"], "xueshengxingming": userJson["xueshengxingming"],
"xuehao": userJson["xuehao"], "xuehao": userJson["xuehao"],
"kaoshengtupian": userJson["kaoshengtupian"],
"mima": md5_encrypt(userJson["mima"]), "mima": md5_encrypt(userJson["mima"]),
"chengji": userJson["chengji"], "chengji": userJson["chengji"],
"zuobiqingkuang": userJson["zuobiqingkuang"], "zuobiqingkuang": userJson["zuobiqingkuang"],
@ -310,6 +360,7 @@ def update_data():
"banji": userJson["banji"], "banji": userJson["banji"],
"xueshengxingming": userJson["xueshengxingming"], "xueshengxingming": userJson["xueshengxingming"],
"xuehao": userJson["xuehao"], "xuehao": userJson["xuehao"],
"kaoshengtupian": userJson["kaoshengtupian"],
"chengji": userJson["chengji"], "chengji": userJson["chengji"],
"zuobiqingkuang": userJson["zuobiqingkuang"], "zuobiqingkuang": userJson["zuobiqingkuang"],
"kaoshileixing": userJson["kaoshileixing"], "kaoshileixing": userJson["kaoshileixing"],

8
front/src/api/teacher.js

@ -51,3 +51,11 @@ export function updatePassword(data = {}) {
data: { ...data, id: localStorage.getItem("userId") }, data: { ...data, id: localStorage.getItem("userId") },
}); });
} }
//查找作弊信息
export function getzuobi(data = {}) {
return _axios({
url: `/v1/getzuobi`,
method: "POST",
data: { id: localStorage.getItem("userId"), ...data },
});
}

49
front/src/component/changeItem.vue

@ -13,7 +13,24 @@
:end-placeholder="headerItem.endplaceholder" width="330px" /> :end-placeholder="headerItem.endplaceholder" width="330px" />
</template> </template>
<template v-else-if="['link'].indexOf(headerItem.type) !== -1"> <template v-else-if="['link'].indexOf(headerItem.type) !== -1">
<el-tag>{{ formData[headerItem.prop] }}</el-tag> <el-tag v-if="formData[headerItem.prop]">{{ formData[headerItem.prop] }}</el-tag>
<span v-else></span>
</template>
<template v-else-if="['img'].indexOf(headerItem.type) !== -1">
<el-upload :show-file-list="false" :before-upload="beforeAvatarUpload" :http-request="successSubmit"
accept=".png,.jpg,.jpeg">
<el-button type="primary">
<span v-if="formData[headerItem.prop]">修改</span>
<span v-else>新增</span>
<span>图片</span>
</el-button>
</el-upload>
<el-image style="width: 100px; height: 100px;margin-left: 16px;" :src="formData[headerItem.prop]"
fit="fill">
<template #error>
<i></i>
</template>
</el-image>
</template> </template>
</el-form-item> </el-form-item>
</el-col> </el-col>
@ -52,7 +69,35 @@ export default {
async mounted() { }, async mounted() { },
beforeUnmount() { }, beforeUnmount() { },
methods: {}, methods: {
/**
* 上传检查图片
*/
beforeAvatarUpload(rawFile) {
let imgList = ["image/jpeg", "image/png"]
if (imgList.indexOf(rawFile.type) === -1) {
this.$msgbox.alert('请上传.png,.jpg,.jpeg格式的图片!')
return false
} else if (rawFile.size / 1024 / 1024 > 50) {
this.$msgbox.alert('图片文件的大小为小于50MB,过大时会处理过慢')
return true
}
return true
},
//
async successSubmit(opts) {
let that = this
let file = opts.file
let fileReader = new FileReader()
fileReader.onload = async function (e) {
that.formData["kaoshengtupian"] = e.target.result
}
fileReader.onerror = function (error) {
console.error('Error reading file:', error)
}
fileReader.readAsDataURL(file)
},
},
}; };
</script> </script>
<style scoped></style> <style scoped></style>

10
front/src/component/table.vue

@ -25,6 +25,13 @@
</el-link> </el-link>
<span v-else></span> <span v-else></span>
</template> </template>
<template v-else-if="headerItem.type === 'img'">
<el-image style="width: 20px; height: 20px;" :src="scope.row[headerItem.prop]"
v-if="_.trim(scope.row[headerItem.prop])" fit="fill" :preview-src-list="[scope.row[headerItem.prop]]"
preview-teleported>
</el-image>
<span v-else></span>
</template>
<template v-else-if="headerItem.type === 'password'"> ****** </template> <template v-else-if="headerItem.type === 'password'"> ****** </template>
<template v-else> <template v-else>
{{ scope.row[headerItem.prop] }} {{ scope.row[headerItem.prop] }}
@ -101,8 +108,9 @@ export default {
</script> </script>
<style scoped> <style scoped>
.mainTable { .mainTable {
height: calc(100vh - 112px); max-height: calc(100vh - 112px);
width: 100%; width: 100%;
margin: 20px 0px;
} }
.pagination { .pagination {

66
front/src/views/student.vue

@ -10,16 +10,26 @@
<p v-for="(userItem, userIndex) in userInfo" :key="userIndex"> <p v-for="(userItem, userIndex) in userInfo" :key="userIndex">
<template v-if="userItem.type === 'datetimerange'"> <template v-if="userItem.type === 'datetimerange'">
<div> <div>
<span>{{ userItem.lable }}</span> <span>{{ userItem.label }}</span>
<span>{{ _.join(userData[userItem.prop], "——") }}</span> <span>{{ _.join(userData[userItem.prop], "——") }}</span>
</div> </div>
</template> </template>
<template v-else-if="userItem.type === 'text'"> <template v-else-if="userItem.type === 'text'">
<div> <div>
<span>{{ userItem.lable }}</span> <span>{{ userItem.label }}</span>
<span>{{ userData[userItem.prop] }}</span> <span>{{ userData[userItem.prop] }}</span>
</div> </div>
</template> </template>
<template v-else-if="userItem.type === 'img'">
<div>
<span>{{ userItem.label }}</span>
<el-image style="width: 20px; height: 20px;" :src="userData[userItem.prop]"
v-if="_.trim(userData[userItem.prop])" fit="fill" :preview-src-list="[userData[userItem.prop]]"
preview-teleported>
</el-image>
<span v-else></span>
</div>
</template>
</p> </p>
<template #footer> <template #footer>
<el-button type="primary" @click="connectTeacher">联系老师</el-button> <el-button type="primary" @click="connectTeacher">联系老师</el-button>
@ -82,62 +92,62 @@ export default {
// , // ,
userInfo: [ userInfo: [
{ {
lable: "学校名称", label: "学校名称",
prop: "xuexiaomingcheng", prop: "xuexiaomingcheng",
type: "text" type: "text"
}, },
{ {
lable: "学校代号", label: "学校代号",
prop: "xuexiaodaihao", prop: "xuexiaodaihao",
type: "text" type: "text"
}, },
{ {
lable: "专业名称", label: "专业名称",
prop: "zhuanyemingcheng", prop: "zhuanyemingcheng",
type: "text" type: "text"
}, },
{ {
lable: "专业代号", label: "专业代号",
prop: "zhuanyedaihao", prop: "zhuanyedaihao",
type: "text" type: "text"
}, },
{ {
lable: "年级", label: "年级",
prop: "nianji", prop: "nianji",
type: "text" type: "text"
}, },
{ {
lable: "班级", label: "班级",
prop: "banji", prop: "banji",
type: "text" type: "text"
}, },
{ {
lable: "学生姓名", label: "学生姓名",
prop: "xueshengxingming", prop: "xueshengxingming",
type: "text" type: "text"
}, },
{ {
lable: "学号", label: "学号",
prop: "xuehao", prop: "xuehao",
type: "text" type: "text"
}, },
{ {
lable: "考生图片", label: "考生图片",
prop: "kaoshengtupian", prop: "kaoshengtupian",
type: "text" type: "img"
}, },
{ {
lable: "考试类型", label: "考试类型",
prop: "kaoshileixing", prop: "kaoshileixing",
type: "text" type: "text"
}, },
{ {
lable: "考试科目", label: "考试科目",
prop: "kaoshikemu", prop: "kaoshikemu",
type: "text" type: "text"
}, },
{ {
lable: "考试时间段", label: "考试时间段",
prop: "kaoshishijianduan", prop: "kaoshishijianduan",
type: "datetimerange" type: "datetimerange"
}, },
@ -153,6 +163,10 @@ export default {
label: "回答次数", label: "回答次数",
prop: "answer" prop: "answer"
}, },
{
label: "讲话次数",
prop: "talk"
},
{ {
label: "举手次数", label: "举手次数",
prop: "jushou" prop: "jushou"
@ -316,18 +330,26 @@ export default {
let res = await getzuobi() let res = await getzuobi()
this.zuobiList = res.list this.zuobiList = res.list
this.zuobiObj = _.groupBy(this.zuobiList, 'type') this.zuobiObj = _.groupBy(this.zuobiList, 'type')
console.log(78444, res) console.log(78444, this.zuobiObj)
let dataLocal = []
for (let key in this.zuobiObj) {
let value = this.zuobiObj[key]
dataLocal.push({
value: value.length,
name: key
})
}
let option = { let option = {
tooltip: { tooltip: {
trigger: 'item' trigger: 'item'
}, },
legend: { legend: {
top: '5%', top: '1%',
left: 'center' left: 'center'
}, },
series: [ series: [
{ {
name: 'Access From', name: '作弊次数',
type: 'pie', type: 'pie',
radius: ['40%', '70%'], radius: ['40%', '70%'],
avoidLabelOverlap: false, avoidLabelOverlap: false,
@ -350,13 +372,7 @@ export default {
labelLine: { labelLine: {
show: false show: false
}, },
data: [ data: dataLocal
{ value: 1048, name: 'Search Engine' },
{ value: 735, name: 'Direct' },
{ value: 580, name: 'Email' },
{ value: 484, name: 'Union Ads' },
{ value: 300, name: 'Video Ads' }
]
} }
] ]
}; };

100
front/src/views/teacher.vue

@ -16,6 +16,15 @@
<vueTable ref="userTableParent" :refName="refName" :tableHeader="tableHeader" :tableData="tableData" <vueTable ref="userTableParent" :refName="refName" :tableHeader="tableHeader" :tableData="tableData"
@detailInfo="detailInfo" @editInfo="editInfo" @updateMima="updateMima"> @detailInfo="detailInfo" @editInfo="editInfo" @updateMima="updateMima">
</vueTable> </vueTable>
<el-card>
<template #header>
<div>
<span>作弊饼状图</span>
<!-- <el-button type="primary" @click="getzuobiList">获取作弊信息</el-button> -->
</div>
</template>
<div id="teacherZuobiEcharts"></div>
</el-card>
</div> </div>
</el-col> </el-col>
<el-col :span="10"> <el-col :span="10">
@ -60,9 +69,11 @@ import {
updateUser, //user updateUser, //user
getUser, //user getUser, //user
updatePassword,// updatePassword,//
getzuobi,//
} from "@/api/teacher"; } from "@/api/teacher";
import io from "socket.io-client"; import io from "socket.io-client";
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import * as echarts from 'echarts';
import getip from "@/plugins/getip" import getip from "@/plugins/getip"
export default { export default {
name: "teacher", name: "teacher",
@ -128,6 +139,13 @@ export default {
tableShow: true, tableShow: true,
formShow: true formShow: true
}, },
{
label: "照片",
prop: "kaoshengtupian",
type: "img",
tableShow: true,
formShow: true
},
{ {
prop: "mima", prop: "mima",
label: "密码", label: "密码",
@ -211,6 +229,8 @@ export default {
videoStream: null, videoStream: null,
imageSrc: "", imageSrc: "",
canvasObj: {}, canvasObj: {},
zuobiInterval: null,
teacherEcharts: null,
}; };
}, },
watch: {}, watch: {},
@ -222,6 +242,11 @@ export default {
await this.init(); await this.init();
await this.initWebSocket() await this.initWebSocket()
await this.initCanvas() await this.initCanvas()
let echartsDom = document.getElementById('teacherZuobiEcharts');
this.teacherEcharts = echarts.init(echartsDom)
this.zuobiInterval = setInterval(async () => {
await this.getzuobiList()
}, 3000);
// getip((ip) => { console.log(777, ip) }) // getip((ip) => { console.log(777, ip) })
} }
}, },
@ -245,6 +270,8 @@ export default {
this.formDataLocal[element.prop] = "" this.formDataLocal[element.prop] = ""
} else if (["datetimerange"].indexOf(element.type) !== -1) { } else if (["datetimerange"].indexOf(element.type) !== -1) {
this.formDataLocal[element.prop] = [] this.formDataLocal[element.prop] = []
} else if (["img"].indexOf(element.type) !== -1) {
this.formDataLocal[element.prop] = ""
} }
} }
} }
@ -467,10 +494,78 @@ export default {
let blob = new Blob(byteArrays, { type: contentType }); let blob = new Blob(byteArrays, { type: contentType });
return blob; return blob;
}, },
//
async getzuobiList() {
let res = await getzuobi()
let zuobiList = res.list
let zuobiObj = _.groupBy(zuobiList, 'userId')
let allZuobi = {}
console.log(78444, zuobiObj)
let dataLocal = []
for (let key in zuobiObj) {
let value = zuobiObj[key]
let valueObj = _.groupBy(value, 'type')
let studentItem = _.find(this.tableData, o => o._id === key)
allZuobi[key] = valueObj
dataLocal.push({
value: value.length,
name: studentItem.xueshengxingming,
_id: key
})
}
let option = {
tooltip: {
trigger: 'item',
formatter: function (params) {
console.log(6887, params, allZuobi)
let msg = ""
for (let key1 in allZuobi[params.data._id]) {
let value1 = allZuobi[params.data._id][key1]
msg += `${key1}:${value1.length}<br>`
}
return `${params.name}<br>${msg}`;
}
},
legend: {
top: '1%',
left: 'center'
},
series: [
{
name: '作弊次数',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: 40,
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: dataLocal
}
]
};
this.teacherEcharts.setOption(option)
}
}, },
beforeUnmount() { beforeUnmount() {
this.stopVideoStream(); this.stopVideoStream();
this.closeWebSocket(); this.closeWebSocket();
clearInterval(this.zuobiInterval)
}, },
}; };
</script> </script>
@ -481,4 +576,9 @@ export default {
height: 390px; height: 390px;
background-color: aliceblue background-color: aliceblue
} }
#teacherZuobiEcharts {
width: 400px;
height: 400px;
}
</style> </style>

Loading…
Cancel
Save