Quellcode durchsuchen

隐患识别框选

KCY vor 1 Monat
Ursprung
Commit
ffb34a52d5

+ 8 - 4
shudao-chat-py/routers/chat.py

@@ -1111,12 +1111,14 @@ async def stream_chat_with_db(request: Request, data: StreamChatWithDBRequest):
                             if thinking_summary:
                                 prefix = f"思考过程:\n{thinking_summary}\n\n回答:\n"
                                 full_response += prefix
-                                yield f"data: {prefix.replace('\n', '\\n')}\n\n"
+                                escaped_prefix = prefix.replace('\n', '\\n')
+                                yield f"data: {escaped_prefix}\n\n"
 
                             answer_chunk = (pre_answer + buffer).lstrip()
                             if answer_chunk:
                                 full_response += answer_chunk
-                                yield f"data: {answer_chunk.replace('\n', '\\n')}\n\n"
+                                escaped_answer = answer_chunk.replace('\n', '\\n')
+                                yield f"data: {escaped_answer}\n\n"
 
                             pre_answer = ""
                             buffer = ""
@@ -1142,10 +1144,12 @@ async def stream_chat_with_db(request: Request, data: StreamChatWithDBRequest):
                 if thinking_summary:
                     prefix = f"思考过程:\n{thinking_summary}\n\n回答:\n"
                     full_response += prefix
-                    yield f"data: {prefix.replace('\n', '\\n')}\n\n"
+                    escaped_prefix = prefix.replace('\n', '\\n')
+                    yield f"data: {escaped_prefix}\n\n"
                 if pre_answer:
                     full_response += pre_answer
-                    yield f"data: {pre_answer.replace('\n', '\\n')}\n\n"
+                    escaped_pre_answer = pre_answer.replace('\n', '\\n')
+                    yield f"data: {escaped_pre_answer}\n\n"
 
             # 9. 更新 AI 消息内容
             if full_response:

+ 3 - 57
shudao-chat-py/routers/hazard.py

@@ -495,66 +495,12 @@ async def hazard(
         current_step = 1
         hazard_count = len(detection_results)
 
+        # 不再绘制检测框,直接使用原始图片
         logger.info(
-            "[hazard][%s] drawing boxes and watermark: user_name=%r, user_account=%r",
+            "[hazard][%s] skipping image annotation, using original image",
             request_id,
-            getattr(user, "name", "") or getattr(user, "username", "") or "",
-            getattr(user, "contactNumber", "") or getattr(user, "account", "") or "",
         )
-        result_image_bytes = await _draw_boxes_and_watermark(
-            image_bytes,
-            [
-                {
-                    "label": item["label"],
-                    "bbox": item["box"],
-                    "confidence": item["score"],
-                }
-                for item in detection_results
-            ],
-            user_name=getattr(user, "name", "") or getattr(user, "username", "") or "",
-            user_account=getattr(user, "contactNumber", "") or getattr(user, "account", "") or "",
-        )
-        logger.info(
-            "[hazard][%s] annotated image generated: bytes=%s",
-            request_id,
-            len(result_image_bytes),
-        )
-
-        utc_now = time.gmtime()
-        timestamp = int(time.time())
-        file_name = f"hazard_annotated/{time.strftime('%Y', utc_now)}/{time.strftime('%m%d', utc_now)}_{timestamp}.jpg"
-        logger.info(
-            "[hazard][%s] uploading annotated image to oss: file_name=%r, folder=%r",
-            request_id,
-            file_name,
-            "hazard_detection",
-        )
-
-        try:
-            result_url = oss_service.upload_image(
-                result_image_bytes,
-                file_name,
-            )
-            logger.info(
-                "[hazard][%s] oss upload success: result_url=%r",
-                request_id,
-                result_url,
-            )
-            recognition_image_url = _resolve_image_url(result_url)
-            logger.info(
-                "[hazard][%s] resolved recognition image url: raw=%r, resolved=%r",
-                request_id,
-                result_url,
-                recognition_image_url,
-            )
-        except Exception as e:
-            logger.error(
-                "[hazard][%s] 上传标注图片到OSS失败: error=%s, traceback=%s",
-                request_id,
-                e,
-                traceback.format_exc(),
-            )
-            return {"statusCode": 500, "msg": f"上传标注图片到OSS失败: {str(e)}"}
+        recognition_image_url = source_image_url
 
         try:
             labels_text = _remove_duplicate_labels([item["label"] for item in detection_results])

+ 49 - 3
shudao-chat-py/routers/scene.py

@@ -62,7 +62,7 @@ def _unique_ordered(items):
     return ordered
 
 
-def _build_record_view(record: RecognitionRecord):
+def _build_record_view(record: RecognitionRecord, db: Session = None):
     hazard_details = _load_hazard_details(record)
     derived_labels = _unique_ordered(
         [
@@ -88,6 +88,49 @@ def _build_record_view(record: RecognitionRecord):
         for item in hazard_details
     ]
 
+    # Rebuild element_hazards from database if db session is provided
+    element_hazards = {}
+    if db and display_labels:
+        try:
+            from models.scene import SecondScene, ThirdScene
+            
+            for label in display_labels:
+                # Find matching secondary scene
+                second_scene = (
+                    db.query(SecondScene)
+                    .filter(
+                        SecondScene.second_scene_name == label,
+                        SecondScene.is_deleted == 0,
+                    )
+                    .first()
+                )
+                
+                if second_scene:
+                    # Get all third scenes for this secondary scene
+                    third_scene_records = (
+                        db.query(ThirdScene)
+                        .filter(
+                            ThirdScene.second_scene_id == second_scene.id,
+                            ThirdScene.is_deleted == 0,
+                        )
+                        .all()
+                    )
+                    
+                    if third_scene_records:
+                        element_hazards[label] = [
+                            ts.third_scene_name for ts in third_scene_records
+                        ]
+                    else:
+                        element_hazards[label] = []
+                else:
+                    element_hazards[label] = []
+        except Exception as e:
+            logger.warning(
+                "_build_record_view failed to rebuild element_hazards: record_id=%s error=%s",
+                record.id,
+                e,
+            )
+
     return {
         "id": record.id,
         "title": record.title or "隐患提示记录",
@@ -97,6 +140,7 @@ def _build_record_view(record: RecognitionRecord):
         "labels": record.labels or ",".join(display_labels),
         "display_labels": display_labels,
         "third_scenes": third_scenes,
+        "element_hazards": element_hazards,
         "tag_type": record.tag_type or record.scene_type,
         "scene_type": record.scene_type,
         "effect_evaluation": record.effect_evaluation,
@@ -433,12 +477,13 @@ async def get_recognition_record_detail(
         logger.warning("get_recognition_record_detail not found: record_id=%s", record_id)
         return {"statusCode": 404, "msg": "记录不存在"}
 
-    record_view = _build_record_view(record)
+    record_view = _build_record_view(record, db)
     logger.info(
-        "get_recognition_record_detail success: record_id=%s user_id=%s scene_type=%s",
+        "get_recognition_record_detail success: record_id=%s user_id=%s scene_type=%s element_hazards_count=%s",
         record.id,
         record.user_id,
         record.scene_type,
+        len(record_view.get("element_hazards", {})),
     )
     return {
         "statusCode": 200,
@@ -453,6 +498,7 @@ async def get_recognition_record_detail(
             "labels": record_view["labels"],
             "display_labels": record_view["display_labels"],
             "third_scenes": record_view["third_scenes"],
+            "element_hazards": record_view["element_hazards"],
             "tag_type": record_view["tag_type"],
             "scene_type": record.scene_type,
             "scene_match": record.scene_match,

+ 149 - 3
shudao-vue-frontend/src/views/HazardDetection.vue

@@ -387,8 +387,8 @@
                             <div class="image-container" ref="imageContainerRef">
                                 <img
                                     ref="mainImageRef"
-                                    :src="showScanningEffect ? uploadedImageUrl : annotatedImageUrl"
-                                    :alt="showScanningEffect ? '用户上传图片' : '隐患提示图片'"
+                                    :src="annotatedImageUrl || uploadedImageUrl"
+                                    alt="隐患识别图片"
                                     class="main-image"
                                     @click="openImagePreview()"
                                     style="cursor: pointer; transform: none !important;"
@@ -396,6 +396,30 @@
                                     @error="handleMainImageError"
                                 />
 
+                                <!-- 动态检测框覆盖层 -->
+                                <div
+                                    v-if="!showScanningEffect && detectionResult?.detections"
+                                    class="detection-boxes-overlay"
+                                >
+                                    <div
+                                        v-for="(detection, index) in detectionResult.detections"
+                                        :key="index"
+                                        class="detection-box"
+                                        :class="{ 
+                                            'detection-box-selected': selectedKeyElement === normalizeLabel(detection.label),
+                                            'detection-box-hover': true
+                                        }"
+                                        :style="getDetectionBoxStyle(detection)"
+                                        @click.stop="handleDetectionBoxClick(detection)"
+                                        @mouseenter="handleDetectionBoxHover(detection, true)"
+                                        @mouseleave="handleDetectionBoxHover(detection, false)"
+                                    >
+                                        <div class="detection-label">
+                                            {{ normalizeLabel(detection.label) }}
+                                        </div>
+                                    </div>
+                                </div>
+
                                 <div
                                     v-if="
                                         !showScanningEffect &&
@@ -1569,6 +1593,11 @@ const handleHistoryItem = async (historyItem) => {
 
         if (detailResponse.statusCode === 200 || detailResponse.code === 200) {
             const detailData = detailResponse.data;
+            
+            // 确保 element_hazards 正确加载
+            const elementHazards = detailData.element_hazards || {};
+            console.log('[历史记录] element_hazards:', elementHazards);
+            
             detectionResult.value = {
                 scene_name: detailData.tag_type || "",
                 labels: detailData.labels,
@@ -1579,7 +1608,7 @@ const handleHistoryItem = async (historyItem) => {
                         : 0
                     : 0,
                 third_scenes: detailData.third_scenes || [],
-                element_hazards: detailData.element_hazards || {},
+                element_hazards: elementHazards,
                 detections: detailData.detections || [],
             };
             selectedScenario.value = detectionResult.value.scene_name || "";
@@ -1589,6 +1618,7 @@ const handleHistoryItem = async (historyItem) => {
                 detailData.recognition_image_url ||
                 detailData.original_image_url;
             annotatedImageUrl.value = newImageUrl;
+            uploadedImageUrl.value = detailData.original_image_url || newImageUrl;
             if (newImageUrl) {
                 await waitForImageLoad(newImageUrl);
             }
@@ -1596,7 +1626,16 @@ const handleHistoryItem = async (historyItem) => {
             const tagType =
                 detailData.tag_type || getTagTypeFromLabels(detailData.labels);
             historyItem.tagType = tagType;
+            
+            // 确保显示所有隐患卡片
             setVisibleHazardCards(detectionResult.value?.third_scenes || []);
+            
+            console.log('[历史记录] 加载完成:', {
+                scene: detectionResult.value.scene_name,
+                labels: detectionResult.value.display_labels,
+                hazards: detectionResult.value.third_scenes,
+                elementHazards: detectionResult.value.element_hazards
+            });
         } else {
             ElMessage.error("获取记录详情失败");
             detectionResult.value = {
@@ -1621,6 +1660,7 @@ const handleHistoryItem = async (historyItem) => {
             item.isActive = item.id === historyItem.id;
         });
     } catch (error) {
+        console.error('[历史记录] 加载失败:', error);
         ElMessage.error("获取记录详情失败");
         isDragOver.value = false;
     } finally {
@@ -2473,6 +2513,59 @@ watch(
     }
 );
 
+const getDetectionBoxStyle = (detection) => {
+    if (!detection || !Array.isArray(detection.box) || detection.box.length < 4) {
+        return { display: 'none' };
+    }
+
+    const container = imageContainerRef.value;
+    const imageElement = mainImageRef.value;
+
+    if (!container || !imageElement || !imageElement.naturalWidth || !imageElement.naturalHeight) {
+        return { display: 'none' };
+    }
+
+    const containerWidth = container.clientWidth;
+    const containerHeight = container.clientHeight;
+    const naturalWidth = imageElement.naturalWidth;
+    const naturalHeight = imageElement.naturalHeight;
+
+    const scale = Math.min(
+        containerWidth / naturalWidth,
+        containerHeight / naturalHeight
+    );
+    const renderedWidth = naturalWidth * scale;
+    const renderedHeight = naturalHeight * scale;
+    const offsetX = (containerWidth - renderedWidth) / 2;
+    const offsetY = (containerHeight - renderedHeight) / 2;
+
+    const [x1, y1, x2, y2] = detection.box.map((value) => Number(value) || 0);
+
+    const boxLeft = offsetX + x1 * scale;
+    const boxTop = offsetY + y1 * scale;
+    const boxWidth = (x2 - x1) * scale;
+    const boxHeight = (y2 - y1) * scale;
+
+    return {
+        position: 'absolute',
+        left: `${boxLeft}px`,
+        top: `${boxTop}px`,
+        width: `${boxWidth}px`,
+        height: `${boxHeight}px`,
+    };
+};
+
+const handleDetectionBoxClick = (detection) => {
+    const label = normalizeLabel(detection?.label || '');
+    if (label) {
+        toggleKeyElement(label);
+    }
+};
+
+const handleDetectionBoxHover = (detection, isHovering) => {
+    // 可以在这里添加悬停效果逻辑
+};
+
 const handleWindowResize = () => {
     updateElementOverlayPosition();
 };
@@ -3278,6 +3371,59 @@ onBeforeUnmount(() => {
     cursor: pointer;
 }
 
+.detail-view .detail-content .image-section .detection-boxes-overlay {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    pointer-events: none;
+}
+
+.detail-view .detail-content .image-section .detection-box {
+    pointer-events: auto;
+    border: 2px solid #3b82f6;
+    background: rgba(59, 130, 246, 0.1);
+    cursor: pointer;
+    transition: all 0.2s ease;
+}
+
+.detail-view .detail-content .image-section .detection-box:hover {
+    border-color: #2563eb;
+    background: rgba(37, 99, 235, 0.15);
+    box-shadow: 0 0 12px rgba(59, 130, 246, 0.4);
+}
+
+.detail-view .detail-content .image-section .detection-box-selected {
+    border-color: #ef4444;
+    background: rgba(239, 68, 68, 0.15);
+    border-width: 3px;
+}
+
+.detail-view .detail-content .image-section .detection-box-selected:hover {
+    border-color: #dc2626;
+    background: rgba(220, 38, 38, 0.2);
+    box-shadow: 0 0 16px rgba(239, 68, 68, 0.5);
+}
+
+.detail-view .detail-content .image-section .detection-label {
+    position: absolute;
+    top: -24px;
+    left: 0;
+    background: #3b82f6;
+    color: white;
+    padding: 2px 8px;
+    border-radius: 4px;
+    font-size: 12px;
+    font-weight: 500;
+    white-space: nowrap;
+    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+}
+
+.detail-view .detail-content .image-section .detection-box-selected .detection-label {
+    background: #ef4444;
+}
+
 .detail-view .detail-content .image-section .element-overlay-card {
     position: absolute;
     width: 260px;