From 551555d526b31c8efae50833ec0697edeb60f879 Mon Sep 17 00:00:00 2001 From: CaoWangrenbo Date: Sun, 26 Oct 2025 15:34:58 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E6=A0=87=E6=B3=A8?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + cam_web.py | 224 +++++++++-- templates/list_images.html | 135 +++---- templates/manual_annotation.html | 627 +++++++++++++++++++++++++++++++ 4 files changed, 882 insertions(+), 106 deletions(-) create mode 100644 templates/manual_annotation.html diff --git a/.gitignore b/.gitignore index 9cf3ce4..67e52d4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ __pycache__/ /static/received/left/ /static/received/right/ +/static/received/left_marked/ +/static/received/right_marked/ # 数据文件及数据库 *.zip diff --git a/cam_web.py b/cam_web.py index ac5e773..05e3639 100644 --- a/cam_web.py +++ b/cam_web.py @@ -141,8 +141,23 @@ def get_images_api(): """API: 获取图片列表 (JSON 格式)""" conn = sqlite3.connect(DATABASE_PATH) cursor = conn.cursor() - # 按时间倒序排列,包含标注图片字段 - cursor.execute("SELECT id, left_filename, right_filename, left_marked_filename, right_marked_filename, timestamp, metadata, comment, created_at FROM images ORDER BY timestamp DESC") + # 按时间倒序排列,包含标注图片字段和人工标注字段 + try: + cursor.execute(""" + SELECT id, left_filename, right_filename, left_marked_filename, right_marked_filename, + timestamp, metadata, comment, created_at, manual_detections, is_manual_labeled + FROM images + ORDER BY timestamp DESC + """) + except sqlite3.OperationalError: + # 如果字段不存在,使用基本查询 + cursor.execute(""" + SELECT id, left_filename, right_filename, left_marked_filename, right_marked_filename, + timestamp, metadata, comment, created_at, NULL as manual_detections, 0 as is_manual_labeled + FROM images + ORDER BY timestamp DESC + """) + rows = cursor.fetchall() conn.close() @@ -157,7 +172,9 @@ def get_images_api(): "timestamp": row[5], "metadata": row[6], "comment": row[7] or "", # 如果没有 comment 则显示空字符串 - "created_at": row[8] + "created_at": row[8], + "manual_detections": row[9] or "[]", # 人工标注检测框结果 + "is_manual_labeled": bool(row[10]) if row[10] is not None else False # 是否已完成人工标注 }) return jsonify(images) @@ -323,31 +340,41 @@ def upload_images(): right_marked_path = None try: - with VisionAPIClient() as client: - # 处理左图 - left_task_id = client.submit_task(image_id=1, image=img_left) - # 处理右图 - right_task_id = client.submit_task(image_id=2, image=img_right) + # with VisionAPIClient() as client: + # # 处理左图 + # left_task_id = client.submit_task(image_id=1, image=img_left) + # # 处理右图 + # right_task_id = client.submit_task(image_id=2, image=img_right) - # 等待任务完成 - client.task_queue.join() + # # 等待任务完成 + # client.task_queue.join() - # 获取处理结果 - left_result = client.get_result(left_task_id) - right_result = client.get_result(right_task_id) + # # 获取处理结果 + # left_result = client.get_result(left_task_id) + # right_result = client.get_result(right_task_id) - # 生成标注图片 - if left_result and left_result.success: - marked_left_img = draw_detections_on_image(img_left, left_result.detections) - left_marked_path = os.path.join(SAVE_PATH_LEFT_MARKED, left_marked_filename) - cv2.imwrite(left_marked_path, marked_left_img) - logger.info(f"Saved marked left image: {left_marked_path}") + # # 生成标注图片 + # if left_result and left_result.success: + # marked_left_img = draw_detections_on_image(img_left, left_result.detections) + # left_marked_path = os.path.join(SAVE_PATH_LEFT_MARKED, left_marked_filename) + # cv2.imwrite(left_marked_path, marked_left_img) + # logger.info(f"Saved marked left image: {left_marked_path}") - if right_result and right_result.success: - marked_right_img = draw_detections_on_image(img_right, right_result.detections) - right_marked_path = os.path.join(SAVE_PATH_RIGHT_MARKED, right_marked_filename) - cv2.imwrite(right_marked_path, marked_right_img) - logger.info(f"Saved marked right image: {right_marked_path}") + # if right_result and right_result.success: + # marked_right_img = draw_detections_on_image(img_right, right_result.detections) + # right_marked_path = os.path.join(SAVE_PATH_RIGHT_MARKED, right_marked_filename) + # cv2.imwrite(right_marked_path, marked_right_img) + # logger.info(f"Saved marked right image: {right_marked_path}") + + # Debug 直接原图覆盖 + left_marked_path = os.path.join(SAVE_PATH_LEFT_MARKED, left_marked_filename) + cv2.imwrite(left_marked_path, img_left) + logger.info(f"Saved marked left image: {left_marked_path}") + + right_marked_path = os.path.join(SAVE_PATH_RIGHT_MARKED, right_marked_filename) + cv2.imwrite(right_marked_path, img_right) + logger.info(f"Saved marked right image: {right_marked_path}") + except Exception as e: logger.error(f"Error processing images with VisionAPIClient: {e}") # 即使处理失败,也继续保存原图 @@ -418,6 +445,155 @@ def status(): timestamp = latest_timestamp if has_frames else "N/A" return jsonify({"has_frames": has_frames, "latest_timestamp": timestamp}) +@app.route('/api/images/manual-detections', methods=['PUT']) +def update_manual_detections(): + """API: 更新图片的人工标注检测框结果,支持左右图像分别标注""" + data = request.json + image_id = data.get('id') + side = data.get('side', 'left') # 获取side参数,默认为左侧 + detections = data.get('detections') + + if not image_id or detections is None: + return jsonify({"error": "Image ID and detections are required"}), 400 + + # 验证检测数据格式 + if not isinstance(detections, list): + return jsonify({"error": "Detections must be a list"}), 400 + + for detection in detections: + if not isinstance(detection, dict): + return jsonify({"error": "Each detection must be a dictionary"}), 400 + + required_keys = ['id', 'label', 'bbox'] + for key in required_keys: + if key not in detection: + return jsonify({"error": f"Missing required key '{key}' in detection"}), 400 + + # 验证ID + if not isinstance(detection['id'], int) or detection['id'] not in [1, 2, 3, 4]: + return jsonify({"error": f"Invalid ID in detection: {detection['id']}"}), 400 + + # 验证标签 + valid_labels = ['caisson', 'soldier', 'gun', 'number'] + if detection['label'] not in valid_labels: + return jsonify({"error": f"Invalid label in detection: {detection['label']}"}), 400 + + # 验证边界框 + bbox = detection['bbox'] + if not isinstance(bbox, list) or len(bbox) != 4: + return jsonify({"error": f"Invalid bbox format in detection"}), 400 + + for coord in bbox: + if not isinstance(coord, int) or not (0 <= coord <= 999): + return jsonify({"error": f"Invalid bbox coordinate: {coord}"}), 400 + + conn = sqlite3.connect(DATABASE_PATH) + cursor = conn.cursor() + + cursor.execute("SELECT id FROM images WHERE id = ?", (image_id,)) + if not cursor.fetchone(): + conn.close() + return jsonify({"error": "Image not found"}), 404 + + # 添加人工标注字段(如果不存在) + try: + cursor.execute(""" + ALTER TABLE images ADD COLUMN manual_detections_left TEXT + """) + except sqlite3.OperationalError: + pass + + try: + cursor.execute(""" + ALTER TABLE images ADD COLUMN manual_detections_right TEXT + """) + except sqlite3.OperationalError: + pass + + try: + cursor.execute(""" + ALTER TABLE images ADD COLUMN is_manual_labeled_left INTEGER DEFAULT 0 + """) + except sqlite3.OperationalError: + pass + + try: + cursor.execute(""" + ALTER TABLE images ADD COLUMN is_manual_labeled_right INTEGER DEFAULT 0 + """) + except sqlite3.OperationalError: + pass + + # 根据side参数更新对应的人工标注结果 + if side == 'left': + cursor.execute(""" + UPDATE images + SET manual_detections_left = ?, is_manual_labeled_left = 1 + WHERE id = ? + """, (json.dumps(detections), image_id)) + else: # side == 'right' + cursor.execute(""" + UPDATE images + SET manual_detections_right = ?, is_manual_labeled_right = 1 + WHERE id = ? + """, (json.dumps(detections), image_id)) + + conn.commit() + conn.close() + + # 重新生成标注图片 + try: + regenerate_marked_images(image_id, detections, side) + return jsonify({"message": f"Manual detections for image {image_id} ({side}) updated successfully and marked images regenerated"}) + except Exception as e: + return jsonify({"message": f"Manual detections for image {image_id} ({side}) updated successfully but failed to regenerate marked images: {str(e)}"}), 500 + +# 添加重新生成标注图片的函数 +def regenerate_marked_images(image_id, detections, side): + """重新生成标注图片,支持左右图像分别处理""" + conn = sqlite3.connect(DATABASE_PATH) + cursor = conn.cursor() + cursor.execute(""" + SELECT left_filename, right_filename, left_marked_filename, right_marked_filename + FROM images + WHERE id = ? + """, (image_id,)) + row = cursor.fetchone() + conn.close() + + if not row: + raise Exception("Image not found") + + left_filename, right_filename, left_marked_filename, right_marked_filename = row + + # 根据指定的side重新生成对应的标注图片 + if side == 'left' and left_marked_filename: + left_path = os.path.join(SAVE_PATH_LEFT, left_filename) + left_marked_path = os.path.join(SAVE_PATH_LEFT_MARKED, left_marked_filename) + + if os.path.exists(left_path): + img_left = cv2.imread(left_path) + if img_left is not None: + marked_left_img = draw_detections_on_image(img_left, detections) + cv2.imwrite(left_marked_path, marked_left_img) + + elif side == 'right' and right_marked_filename: + right_path = os.path.join(SAVE_PATH_RIGHT, right_filename) + right_marked_path = os.path.join(SAVE_PATH_RIGHT_MARKED, right_marked_filename) + + if os.path.exists(right_path): + img_right = cv2.imread(right_path) + if img_right is not None: + marked_right_img = draw_detections_on_image(img_right, detections) + cv2.imwrite(right_marked_path, marked_right_img) + +# 添加人工标注页面路由 +@app.route('/manual-annotation') +def manual_annotation(): + """人工标注页面""" + return render_template('manual_annotation.html') + + # --- SocketIO 事件处理程序 --- @socketio.event def connect(): diff --git a/templates/list_images.html b/templates/list_images.html index 214b6d3..2cc07c3 100644 --- a/templates/list_images.html +++ b/templates/list_images.html @@ -80,7 +80,6 @@

Saved Images

-

Back to Live Feed

Loading images...
@@ -88,7 +87,7 @@
- + @@ -98,10 +97,8 @@ style="display: inline-block; margin-left: 5px; cursor: pointer;">Select All - - - - + + @@ -114,9 +111,8 @@ diff --git a/templates/manual_annotation.html b/templates/manual_annotation.html new file mode 100644 index 0000000..dbdbdcb --- /dev/null +++ b/templates/manual_annotation.html @@ -0,0 +1,627 @@ + + + + + + + 人工标注 + + + + + +
+
+

人工标注

+ 返回图片列表 +
+ +
+

图像信息

+

图像ID:

+

时间戳:

+

备注:

+
+ + + +
+
+ +
+
+ +
+

目标类别

+
+ + + + +
+ +
+

当前状态: 请选择一个目标类别开始标注

+

当前选中类别:

+
+ + + +
+ +
+

已标注目标 (0)

+
+
+
+ + + + + \ No newline at end of file
IDLeft ImageRight ImageLeft Marked Image Right Marked Image Left Marked ImageRight Marked Image Timestamp Comment Actions