From f02173c6674b350a1e544c710631fd94fdbad99f Mon Sep 17 00:00:00 2001 From: BBIT-Kai <2911862937@qq.com> Date: Wed, 18 Jun 2025 10:21:32 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ink/snowflake/server/Application.kt | 3 - .../ink/snowflake/server/database/ImageDao.kt | 1 - .../snowflake/server/route/ImageAnalytics.kt | 11 +- .../kotlin/ink/snowflake/server/route/Main.kt | 30 -- .../snowflake/server/route/VideoAnalytics.kt | 377 ++++++++---------- .../kotlin/ink/snowflake/server/utils/bin | 46 +++ 6 files changed, 219 insertions(+), 249 deletions(-) delete mode 100644 ktor/src/main/kotlin/ink/snowflake/server/route/Main.kt create mode 100644 ktor/src/main/kotlin/ink/snowflake/server/utils/bin diff --git a/ktor/src/main/kotlin/ink/snowflake/server/Application.kt b/ktor/src/main/kotlin/ink/snowflake/server/Application.kt index a6cf998..dee0e60 100644 --- a/ktor/src/main/kotlin/ink/snowflake/server/Application.kt +++ b/ktor/src/main/kotlin/ink/snowflake/server/Application.kt @@ -9,7 +9,6 @@ import ink.snowflake.server.route.ImageAnalytics import ink.snowflake.server.route.RemoteDebug import ink.snowflake.server.route.VideoAnalytics import ink.snowflake.server.route.VideoAnalyticsJetson -import ink.snowflake.server.route.mainFunc import ink.snowflake.server.utils.AppConfig import io.ktor.server.application.* import io.ktor.server.tomcat.jakarta.* @@ -54,8 +53,6 @@ fun Application.module() { // 设置-WebSocket configureSockets() - // 业务-首页导航 - mainFunc() // 业务-用户信息相关操作 User(appConfig) // 业务-聊天 diff --git a/ktor/src/main/kotlin/ink/snowflake/server/database/ImageDao.kt b/ktor/src/main/kotlin/ink/snowflake/server/database/ImageDao.kt index 761276f..210bce1 100644 --- a/ktor/src/main/kotlin/ink/snowflake/server/database/ImageDao.kt +++ b/ktor/src/main/kotlin/ink/snowflake/server/database/ImageDao.kt @@ -32,7 +32,6 @@ object ImageDao { } } - fun getImageList(name: String): List { return transaction { ImageTable.selectAll() diff --git a/ktor/src/main/kotlin/ink/snowflake/server/route/ImageAnalytics.kt b/ktor/src/main/kotlin/ink/snowflake/server/route/ImageAnalytics.kt index f8238a8..271ecd8 100644 --- a/ktor/src/main/kotlin/ink/snowflake/server/route/ImageAnalytics.kt +++ b/ktor/src/main/kotlin/ink/snowflake/server/route/ImageAnalytics.kt @@ -8,6 +8,7 @@ import io.ktor.server.auth.* import io.ktor.server.routing.* import io.ktor.server.request.* import io.ktor.server.response.* +import kotlin.text.isNullOrEmpty fun Application.ImageAnalytics() { routing { @@ -18,16 +19,6 @@ fun Application.ImageAnalytics() { call.respond(BaseResponse(data = ImageDao.insertImageAnalyticsData(request))) } authenticate { - // 拍照保存为图片 并且调用Python程序进行分析 - get("/takePhoto") { - val camera = call.request.queryParameters["cameraId"] - if (camera.isNullOrEmpty()) { - call.respond(BaseResponse(status = false, message = "摄像头名称不能为空", data = null)) - return@get - } - stopHLSStream(camera) - call.respond(BaseResponse(message = "摄像头流已停止", data = null)) - } // 获取已分析图片列表 get("/getImageList") { val name = call.parameters["name"] diff --git a/ktor/src/main/kotlin/ink/snowflake/server/route/Main.kt b/ktor/src/main/kotlin/ink/snowflake/server/route/Main.kt deleted file mode 100644 index f57e9b6..0000000 --- a/ktor/src/main/kotlin/ink/snowflake/server/route/Main.kt +++ /dev/null @@ -1,30 +0,0 @@ -package ink.snowflake.server.route - -import com.google.gson.Gson -import ink.snowflake.server.model.response.BaseResponse -import ink.snowflake.server.model.response.DeviceItem -import ink.snowflake.server.utils.runCommand -import io.ktor.client.* -import io.ktor.client.engine.cio.* -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import java.io.BufferedReader -import java.io.InputStreamReader -import io.ktor.client.plugins.auth.* -import io.ktor.client.plugins.auth.providers.* -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.ktor.server.auth.* -import io.ktor.server.http.content.* -import java.io.File - -fun Application.mainFunc() { - routing { - get("/") { -// call.respondFile(File("src/main/resources/page/html/login.html")) -// call.respondRedirect("html/login.html") - } - } -} diff --git a/ktor/src/main/kotlin/ink/snowflake/server/route/VideoAnalytics.kt b/ktor/src/main/kotlin/ink/snowflake/server/route/VideoAnalytics.kt index d03a8a6..01713d9 100644 --- a/ktor/src/main/kotlin/ink/snowflake/server/route/VideoAnalytics.kt +++ b/ktor/src/main/kotlin/ink/snowflake/server/route/VideoAnalytics.kt @@ -34,244 +34,211 @@ import java.time.format.DateTimeFormatter import java.util.* val clients = Collections.synchronizedList(ArrayList()) // 线程安全的客户端列表 -var aiState = "等待分析任务中" fun Application.VideoAnalytics() { routing { - // 实时发送AI状态 - webSocket("/handleState") { // WebSocket 路由 - clients.add(this) // 添加当前连接的客户端 - send(aiState) // 向客户端发送连接成功消息 - try { - incoming.consumeEach { frame -> // 持续接收消息 - when (frame) { - is Frame.Text -> { - aiState = frame.readText() // 更新状态 - broadcastMessage(aiState) // 使用封装的方法广播消息 - } - - is Frame.Close -> { - println("Closed") - clients.remove(this) - close() // 确保关闭 WebSocket 连接 - return@consumeEach - } - // 其他消息类型的处理 - is Frame.Binary -> TODO() // 处理二进制消息 - is Frame.Ping -> TODO() // 处理 Ping 消息 - is Frame.Pong -> TODO() // 处理 Pong 消息 - } - } - } catch (e: Exception) { - // 处理接收消息时的异常 - close(CloseReason(CloseReason.Codes.NORMAL, "Client disconnected")) - e.printStackTrace() - } finally { - clients.remove(this) // 确保在连接关闭时移除客户端 - } - } route("/api/iva") { - // 上传分析结果 - post("/saveVideoAnalyticsData") { - val request = call.receive() - // todo 上传这里未做测试 - call.respond(BaseResponse(data = VideoDao.insertVideoAnalyticsData(request))) - } - authenticate { - post("/createVideoTask") { - val multipart = call.receiveMultipart() //1G - // 确保 uploads 目录存在 - val uploadDir = File(VIDEO_INPUT_PATH) - if (!uploadDir.exists()) { - if (!uploadDir.mkdirs()) { - println("无法创建目录") - throw IOException("Failed to create upload directory.") - } + // 上传分析结果 + post("/saveVideoAnalyticsData") { + val request = call.receive() + // todo 上传这里未做测试 + call.respond(BaseResponse(data = VideoDao.insertVideoAnalyticsData(request))) + } + authenticate { + post("/createVideoTask") { + val multipart = call.receiveMultipart() //1G + // 确保 uploads 目录存在 + val uploadDir = File(VIDEO_INPUT_PATH) + if (!uploadDir.exists()) { + if (!uploadDir.mkdirs()) { + println("无法创建目录") + throw IOException("Failed to create upload directory.") } - var fileName = "" - var name = "" - var datetime = "" - broadcastMessage("正在上传数据") + } + var fileName = "" + var name = "" + var datetime = "" + broadcastMessage("正在上传数据") - withContext(Dispatchers.IO) { - multipart.forEachPart { part -> - when (part) { - is PartData.FileItem -> { - fileName = part.originalFileName ?: "unknown" - val file = File("$VIDEO_INPUT_PATH$fileName") // 保存路径 - //ktor3 + withContext(Dispatchers.IO) { + multipart.forEachPart { part -> + when (part) { + is PartData.FileItem -> { + fileName = part.originalFileName ?: "unknown" + val file = File("$VIDEO_INPUT_PATH$fileName") // 保存路径 + //ktor3 // file.outputStream().use { outputStream -> // val writableChannel = Channels.newChannel(outputStream) // part.provider().copyTo(writableChannel) // 复制到 WritableByteChannel // } - //ktor2 - part.streamProvider().use { inputStream -> - file.outputStream().buffered().use { outputStream -> - inputStream.copyTo(outputStream) - } + //ktor2 + part.streamProvider().use { inputStream -> + file.outputStream().buffered().use { outputStream -> + inputStream.copyTo(outputStream) } } - - is PartData.FormItem -> { - when (part.name) { - "projectName" -> name = part.value - "projectDatetime" -> datetime = part.value - } - } - - else -> part.dispose() } + + is PartData.FormItem -> { + when (part.name) { + "projectName" -> name = part.value + "projectDatetime" -> datetime = part.value + } + } + + else -> part.dispose() } } - call.respond(BaseResponse(message = "上传成功", data = null)) - broadcastMessage("上传完成,开始启动AI引擎") - val command = listOf( - "/usr/bin/python3", - "/home/xhcp/mine/IntelligentVideoAnalytics/AI_Project/DeepStream_Action_Recognition/core/final.py", - "$VIDEO_INPUT_PATH$fileName", - datetime, - name + } + call.respond(BaseResponse(message = "上传成功", data = null)) + broadcastMessage("上传完成,开始启动AI引擎") + val command = listOf( + "/usr/bin/python3", + "/home/xhcp/mine/IntelligentVideoAnalytics/AI_Project/DeepStream_Action_Recognition/core/final.py", + "$VIDEO_INPUT_PATH$fileName", + datetime, + name + ) + println("-----------------" + command.joinToString(" ")) + runCommand(command) { + println(it) + } + } + // 获取已分析视频列表 + get("/getVideoList") { + val name = call.parameters["name"] + val res = VideoDao.getVideoList(name) + call.respond(BaseResponse(data = res)) + } + // 获取某视频分析详情 + get("/getAnalyticsDetailByVideoId") { + // 获取 vId 参数 + val vIdParam = call.parameters["vId"] + val vId = vIdParam?.toIntOrNull() // 将 vId 转换为 Int,确保安全 + if (vId == null) { + call.respond(BaseResponse(status = true, message = "Invalid vId", data = null)) + return@get + } + // 查询视频信息 + val video = VideoDao.getAnalyticsDetailByVideoId(vId) + if (video == null) { + call.respond(BaseResponse(status = false, message = "不存在该视频", data = null)) + } else { + // 查询相关的分析详情 + val details = VideoDao.selectVideoDetailByVid(vId) + + // 用于返回的数据 + val yTotalData = mutableListOf>() // (时间, 总人数) + val yMaskedData = mutableListOf>() // (时间, 佩戴口罩人数) + val areaData = mutableListOf>() + val detailList = mutableListOf() + // 颜色映射 + val colors = mapOf( + "feed" to "rgba(0, 255, 0, 0.4)", // 淡绿色 + "disinfection" to "rgba(0, 0, 255, 0.4)", // 淡蓝色 + "other" to "rgba(255, 0, 0, 0.4)" // 淡红色 ) - println("-----------------" + command.joinToString(" ")) - runCommand(command) { - println(it) - } - } - // 获取已分析视频列表 - get("/getVideoList") { - val name = call.parameters["name"] - val res = VideoDao.getVideoList(name) - call.respond(BaseResponse(data = res)) - } - // 获取某视频分析详情 - get("/getAnalyticsDetailByVideoId") { - // 获取 vId 参数 - val vIdParam = call.parameters["vId"] - val vId = vIdParam?.toIntOrNull() // 将 vId 转换为 Int,确保安全 - if (vId == null) { - call.respond(BaseResponse(status = true, message = "Invalid vId", data = null)) - return@get - } - // 查询视频信息 - val video = VideoDao.getAnalyticsDetailByVideoId(vId) - if (video == null) { - call.respond(BaseResponse(status = false, message = "不存在该视频", data = null)) - } else { - // 查询相关的分析详情 - val details = VideoDao.selectVideoDetailByVid(vId) + // 生成 yTotalData 和 yMaskedData,同时计算 areaData + var lastAction: String? = null + var areaStartTime: String? = null + var currentColor: String? = null - // 用于返回的数据 - val yTotalData = mutableListOf>() // (时间, 总人数) - val yMaskedData = mutableListOf>() // (时间, 佩戴口罩人数) - val areaData = mutableListOf>() - val detailList = mutableListOf() - // 颜色映射 - val colors = mapOf( - "feed" to "rgba(0, 255, 0, 0.4)", // 淡绿色 - "disinfection" to "rgba(0, 0, 255, 0.4)", // 淡蓝色 - "other" to "rgba(255, 0, 0, 0.4)" // 淡红色 - ) - // 生成 yTotalData 和 yMaskedData,同时计算 areaData - var lastAction: String? = null - var areaStartTime: String? = null - var currentColor: String? = null + for (detail in details) { + // 获取视频开始时间 + val vStartDatetime: LocalDateTime = video[vStartDateTime]!! + val timeStr = vStartDatetime.toJavaLocalDateTime() + .plusSeconds(detail.a_time_stamp.toLong()) + .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) - for (detail in details) { - // 获取视频开始时间 - val vStartDatetime: LocalDateTime = video[vStartDateTime]!! - val timeStr = vStartDatetime.toJavaLocalDateTime() - .plusSeconds(detail.a_time_stamp.toLong()) - .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + // 添加总人数和口罩佩戴人数 + yTotalData.add(timeStr to detail.a_total_people) + yMaskedData.add(timeStr to detail.a_total_people_masked) - // 添加总人数和口罩佩戴人数 - yTotalData.add(timeStr to detail.a_total_people) - yMaskedData.add(timeStr to detail.a_total_people_masked) - - // 处理 areaData,根据 a_action 判断是否需要创建新的区域 - if (detail.a_action != "--") { // 只处理非 "--" 动作 - if (lastAction == null || lastAction != detail.a_action) { - // 如果上一个动作和当前动作不同,并且 areaStartTime 已经存在,创建新的区域 - if (areaStartTime != null) { - areaData.add( - listOf( - Area(areaStartTime, ItemStyle(currentColor ?: "#FF0000")), - Area(timeStr, ItemStyle(currentColor ?: "#FF0000")) - ) - ) - } - // 添加到 detailList,记录动作开始的时刻 - detailList.add( - DetailItem(getFriendlyActionName(detail.a_action), time = timeStr) - ) - - // 更新为新的动作 - lastAction = detail.a_action - areaStartTime = timeStr - currentColor = colors[detail.a_action] ?: "#FF0000" // 默认红色 - } - } else { - // 如果当前动作为 "--",则结束当前区域,并重置状态 + // 处理 areaData,根据 a_action 判断是否需要创建新的区域 + if (detail.a_action != "--") { // 只处理非 "--" 动作 + if (lastAction == null || lastAction != detail.a_action) { + // 如果上一个动作和当前动作不同,并且 areaStartTime 已经存在,创建新的区域 if (areaStartTime != null) { - // 添加到 detailList,记录动作开始的时刻 - detailList.add( - DetailItem(getFriendlyActionName(detail.a_action), timeStr) - ) - // 结束当前区域 areaData.add( listOf( Area(areaStartTime, ItemStyle(currentColor ?: "#FF0000")), Area(timeStr, ItemStyle(currentColor ?: "#FF0000")) ) ) - // 重置状态 - lastAction = null - areaStartTime = null - currentColor = null } + // 添加到 detailList,记录动作开始的时刻 + detailList.add( + DetailItem(getFriendlyActionName(detail.a_action), time = timeStr) + ) + + // 更新为新的动作 + lastAction = detail.a_action + areaStartTime = timeStr + currentColor = colors[detail.a_action] ?: "#FF0000" // 默认红色 + } + } else { + // 如果当前动作为 "--",则结束当前区域,并重置状态 + if (areaStartTime != null) { + // 添加到 detailList,记录动作开始的时刻 + detailList.add( + DetailItem(getFriendlyActionName(detail.a_action), timeStr) + ) + // 结束当前区域 + areaData.add( + listOf( + Area(areaStartTime, ItemStyle(currentColor ?: "#FF0000")), + Area(timeStr, ItemStyle(currentColor ?: "#FF0000")) + ) + ) + // 重置状态 + lastAction = null + areaStartTime = null + currentColor = null } } - // 处理最后一个区域的结束时间 - if (areaStartTime != null && lastAction != null) { - areaData.add( - listOf( - Area(areaStartTime, ItemStyle(currentColor ?: "#FF0000")), - Area(yTotalData.last().first, ItemStyle(currentColor ?: "#FF0000")) - ) - ) - } - val analyticsData = VideoAnalyticsData( - yTotalData = yTotalData, - yMaskedData = yMaskedData, - areaData = areaData - ) - // 返回数据 - call.respond( - BaseResponse( - data = VideoAnalyticsDetail( - v_id = vId, - v_name = video[VideoTable.vName], - v_video_play_path = "http://${SERVER_PATH_OSS}:9000/video/" + video[VideoTable.vObjectName], - v_file_name = video[VideoTable.vFileName], - v_duration = video[VideoTable.vDuration], - v_size = video[VideoTable.vSize], - v_start_datetime = video[vStartDateTime]?.toString() ?: "", - v_video_codec = video[VideoTable.vVideoCodec], - v_audio_codec = video[VideoTable.vAudioCodec], - v_overall_bit_rate = video[VideoTable.vOverallBitRate], - v_resolution = video[VideoTable.vResolution], - v_a_time = video[VideoTable.vATime].toString(), - v_a_total_people = video[vATotalPeople], // 总人数 - v_a_count_people = video[vACountPeople], // 佩戴口罩人数 - v_a_max_stay_time = video[vAMaxStayTime], // 最大停留时间 - v_a_max_action = getFriendlyActionName(video[vAMaxAction]), // 最大动作 - v_a_average_masked_ratio = video[vAAverageMaskedRatio], // 平均佩戴口罩比例 - v_a_details = analyticsData, // 这里是计算得来的 VideoAnalyticsData - v_details_list = detailList - ) + } + // 处理最后一个区域的结束时间 + if (areaStartTime != null && lastAction != null) { + areaData.add( + listOf( + Area(areaStartTime, ItemStyle(currentColor ?: "#FF0000")), + Area(yTotalData.last().first, ItemStyle(currentColor ?: "#FF0000")) ) ) } + val analyticsData = VideoAnalyticsData( + yTotalData = yTotalData, + yMaskedData = yMaskedData, + areaData = areaData + ) + // 返回数据 + call.respond( + BaseResponse( + data = VideoAnalyticsDetail( + v_id = vId, + v_name = video[VideoTable.vName], + v_video_play_path = "http://${SERVER_PATH_OSS}:9000/video/" + video[VideoTable.vObjectName], + v_file_name = video[VideoTable.vFileName], + v_duration = video[VideoTable.vDuration], + v_size = video[VideoTable.vSize], + v_start_datetime = video[vStartDateTime]?.toString() ?: "", + v_video_codec = video[VideoTable.vVideoCodec], + v_audio_codec = video[VideoTable.vAudioCodec], + v_overall_bit_rate = video[VideoTable.vOverallBitRate], + v_resolution = video[VideoTable.vResolution], + v_a_time = video[VideoTable.vATime].toString(), + v_a_total_people = video[vATotalPeople], // 总人数 + v_a_count_people = video[vACountPeople], // 佩戴口罩人数 + v_a_max_stay_time = video[vAMaxStayTime], // 最大停留时间 + v_a_max_action = getFriendlyActionName(video[vAMaxAction]), // 最大动作 + v_a_average_masked_ratio = video[vAAverageMaskedRatio], // 平均佩戴口罩比例 + v_a_details = analyticsData, // 这里是计算得来的 VideoAnalyticsData + v_details_list = detailList + ) + ) + ) } + } } } } diff --git a/ktor/src/main/kotlin/ink/snowflake/server/utils/bin b/ktor/src/main/kotlin/ink/snowflake/server/utils/bin new file mode 100644 index 0000000..e65eb11 --- /dev/null +++ b/ktor/src/main/kotlin/ink/snowflake/server/utils/bin @@ -0,0 +1,46 @@ + +var aiState = "等待分析任务中" + // 实时发送AI状态 + webSocket("/handleState") { // WebSocket 路由 + clients.add(this) // 添加当前连接的客户端 + send(aiState) // 向客户端发送连接成功消息 + try { + incoming.consumeEach { frame -> // 持续接收消息 + when (frame) { + is Frame.Text -> { + aiState = frame.readText() // 更新状态 + broadcastMessage(aiState) // 使用封装的方法广播消息 + } + + is Frame.Close -> { + println("Closed") + clients.remove(this) + close() // 确保关闭 WebSocket 连接 + return@consumeEach + } + // 其他消息类型的处理 + is Frame.Binary -> TODO() // 处理二进制消息 + is Frame.Ping -> TODO() // 处理 Ping 消息 + is Frame.Pong -> TODO() // 处理 Pong 消息 + } + } + } catch (e: Exception) { + // 处理接收消息时的异常 + close(CloseReason(CloseReason.Codes.NORMAL, "Client disconnected")) + e.printStackTrace() + } finally { + clients.remove(this) // 确保在连接关闭时移除客户端 + } + } + + +// 拍照保存为图片 并且调用Python程序进行分析 +//get("/takePhoto") { +// val camera = call.request.queryParameters["cameraId"] +// if (camera.isNullOrEmpty()) { +// call.respond(BaseResponse(status = false, message = "摄像头名称不能为空", data = null)) +// return@get +// } +// stopHLSStream(camera) +// call.respond(BaseResponse(message = "摄像头流已停止", data = null)) +//} \ No newline at end of file