analyze.py


import copy
import cv2
import os
import json
import gc
import numpy as np
  • copy:深拷贝检测框,避免污染原始结果
  • cv2 + numpy:图像解码、裁剪、保存
  • gc:内存回收(虽然后面没显式用)
  • json:结果结构使用 JSON 风格字典
from aionet import uploader
from config.logger_config import process_logger
from config import settings
  • uploader:上传 OBS(华为对象存储)
  • process_logger:进程级日志
  • settings:全局配置

整体功能概述

这段代码属于一个 图像检测 post-processing(后处理)模块,典型用于:

  • AI 检测模型返回结果的进一步过滤
  • 删除越界或无效的检测框
  • 冗余检测结果去重(例如多个相机拍摄到重复目标)
  • 对检测框进行裁剪生成子图
  • 将子图上传到 OBS(对象存储)
  • 最终合成一个包含所有子图结果的 JSON

整体结构如下:

模型检测结果 → 后处理
    ├─ check_error:检查模型返回是否包含 error_code
    ├─ remove_outside_boundingbox:移除超出原图宽度的框
    ├─ remove_approximate_result:去重(不同相机得到的、同一个部件的相近结果)
    ├─ get_short_image_results_middle:裁剪子图并上传
    └─ assemble_results:组装小图检测结构并返回

1. check_error(result)

检查返回结果是否含有 error_code。

def check_error(result):
    detect_info = result["result"]["faults"][0]
    if "error_code" in detect_info:
        process_logger.info(...)

1.1 作用:

  • 某些模型可能输出 error_code 代表检测失败。
  • 如果存在 error_code,则记录日志。

1.2 特点:

  • 无返回值,只用于日志监控。

2. remove_outside_boundingbox(result, width)

删除 “左 + 宽度 > 图像宽度” 的框

new_list = [rect for rect in fault_info if rect["left"] + rect["width"] <= width]

2.1 为什么?

检测模型可能输出越界框,裁剪子图会出错,因此先过滤。

2.2 步骤:

  1. fault_info 列表中每个元素是一个检测框:

    {"left": ..., "top": ..., "width": ..., "height": ...}
    
  2. 条件过滤:

    left + width <= 图像宽度
    
  3. 替换原 fault_info,再返回。

2.3 注意点:

  • 如果图像是宽图,left 可能很大(长图拼接)。
  • 返回 result 保持结构一致。

3. remove_approximate_result(items)

核心逻辑: 去重不同相机、不同角度拍到的 同一故障目标。

这是整个代码最复杂的函数。


3.1 预设去重规则 remove_task

remove_task = {
    "轮缘润滑装置":[["10", "11"],["14", "13"]],
    "橡胶盖":[["10","8"],["14","9"]]
}

含义:

  • 同一个故障场景由多个相机拍摄
  • 某些相机(如 10、11)会拍摄到同一个目标
  • 如果两个相机结果高度相似,应只保留一个

3.2 按车厢号分组

grouped_by_car[car_number].append((idx, item))
  • seq = 车厢号

  • 保留原始索引,方便后面删除

3.3 查找同一车厢 + 相机组 + 组件的检测项

camera = item["check"][0]["position"]
detections = item["check"][0]["detections"]

必须同时满足:

  • 相机在指定相机组中
  • 有检测结果
  • faultname 包含目标组件
  • other 中存在 x 坐标

items 中每个元素如:

{
  "seq": "06",
  "check": [
      {
         "position": "10", 
         "detections": [...]
      }
  ]
}

最终结构:

{
    "车厢号": [
        (原索引, item对象),
        ...
    ]
}

3.4 多相机、同部件、同车厢 → 按 x 坐标聚类

3.4.1 关键逻辑:

if abs(curr_x - prev_x) < settings.X_APPROXIMATE_THRESHOLD:
    同一簇

含义:

  • 如果检测结果横向位置非常接近(如相差 < 25 像素)
  • 可以认为是“同一个故障被不同相机拍到”

3.4.2 只保留每簇第一个,其余删除:

for i in range(1, len(cluster)):
    keep_indices.discard(cluster[i][0])

3.5 小结(remove_approximate_result)

功能:
在以下条件同时满足时,认为是重复项并去除:

  • 同一车厢
  • 同一部件(如轮缘润滑装置)
  • 同一个相机组(比如相机 10/11)
  • x 坐标非常近(小于阈值 settings.X_APPROXIMATE_THRESHOLD)

结果:返回不重复的检测列表。


4. get_short_image_results_middle(result)

核心任务:

  1. 读取一张 长图(长条监控图)
  2. 对每个检测框生成一个 以该故障为中心的正方形子图
  3. 修正该子图中的检测框坐标
  4. 将子图保存到本地,并上传到 OBS
  5. 返回所有子图的检测 JSON,用于进一步处理

4.1 初始化与读取输入

all_sub_detect_json = {}
detect_info = result["result"]["faults"][0]
  • result 是模型返回的整体检测结果结构
  • faults 是一个列表,每个 fault 对应一种部件,这里默认只取第一个
  • all_sub_detect_json 用于保存输出

4.2 获取长图路径 + 文件名拆分

long_img_path = detect_info["path"]
_, long_img_fname = os.path.split(long_img_path)
long_img_name, long_img_extension = os.path.splitext(long_img_fname)

例如:

20250706044153000_M15_15004_06_香江北路_1_走行部左.jpg

拆分后:

  • long_img_name = 没有扩展名的部分
  • long_img_extension = 文件扩展名(.jpg)

后面存子图时要用。


4.3 使用 imdecode 读取图片

long_img = cv2.imdecode(np.fromfile(long_img_path, dtype=np.uint8), -1)

4.3.1 为什么不用 cv2.imread?

因为中文路径会导致 cv2.imread 读取失败。
np.fromfile + imdecode 是处理中文路径的惯用方法。


4.4 获取原图尺寸

H, W = long_img.shape[:2]
  • H = 高度
  • W = 宽度

4.5 取出检测框信息

fault_info = detect_info["fault_info"]

fault_info 是一个列表,每个元素是:

{
  "left": xxx,
  "top": xxx,
  "width": xxx,
  "height": xxx,
  "defect_name": "xxxxxx",
  "defect_area": "xxxx"
}

4.6 设定裁剪子图宽度 cut_width

cut_width = H

裁剪的子图一定是正方形,宽=高=原图高度

理由:

  • 原图是长条图(横向长)
  • 故障框一般在中部
  • 每个子图裁剪为正方形便于后续网络处理/展示

4.7 遍历每个检测框 rect

for idx, rect in enumerate(fault_info):

4.7.1 跳过无需裁剪的类型

if any(x in rect["defect_name"] for x in [...]):
    continue

包含关键字的 defect_name 都属于:

  • 正常
  • 无法判断
  • 遮挡
  • 非真实故障

这些不需要生成故障子图,直接跳过。


4.8 深拷贝当前框,避免修改原框

sub_rect = copy.deepcopy(rect)

4.9 计算裁剪图的中心 mid_x(核心逻辑)

if rect["width"] > cut_width:
    mid_x = rect["left"]
else:
    mid_x = rect["left"] + rect["width"]//2

4.9.1 宽度非常大的框(> 子图宽度)

采用:

mid_x = 左边界

理由:框太宽,中心太偏,要以左边当中心,否则裁剪不合理。

4.9.2 普通框

采用:

中心 = 左 + 半宽

4.10 根据 mid_x 计算裁剪窗口左右边界

sub_img_left = mid_x - cut_width//2 if mid_x - cut_width//2 > 0 else 0
sub_img_right = sub_img_left + cut_width
  • mid_x - cut_width/2 是左边界
  • 不能 < 0,否则从 0 截起

4.11 如果右边越界,整体左移

offset_x = sub_img_right - W
if offset_x > 0:
    sub_img_left -= offset_x
    sub_img_right -= offset_x

作用:

  • 避免 sub_img_right > 图像宽度
  • 同时往左移,使得子图仍然是正方形

4.12 截取子图

sub_im = long_img[:, sub_img_left:sub_img_right]

子图是:

  • 高度:原图 H
  • 宽度:cut_width

4.13 修正子图内的检测框坐标

sub_rect["left"] -= sub_img_left

因为子图的 left = 原图的 left - 裁剪窗口左边界

特殊情况:框宽 > cut_width,需要截断

if rect["width"] > cut_width:
    sub_rect["width"] = cut_width//2

避免子图中过宽的框。


4.14 保存子图(本地)

sub_img_path = os.path.join(settings.ERROR_IMG_DIR, long_img_name+"_"+str(idx).zfill(3)+".jpg")
cv2.imencode('.jpg', sub_im)[1].tofile(sub_img_path)

4.15 准备 OBS 上传路径(文件名编码)

文件名解析:

pass_time, line, train_code, carriage_number, station, direction, camera = long_img_name.split("_")

生成如下结构:

pass_time_相机_车厢号_故障区域_故障名_序号.jpg

用于上传 OBS:

obs_sub_img_path = ...

4.16 上传 OBS

status, obs_path = uploader(...)

上传的是 未画框的子图


4.17 生成子图 JSON

sub_fault_info_json = {
    "result": {
        "faults":[
            {
                "path": sub_img_path,
                "fault_info":[sub_rect],
                "defect_count": 1,
                "part": detect_info["part"],
                "left": sub_img_left,
                "top": 0,
                "height": H,
                "width": cut_width
            }
        ]
    }
}

4.18 用子图的 left 作为 key 保存

all_sub_detect_json[sub_img_left] = sub_fault_info_json

这样可以按子图在原图中的顺序输出(按 left 排序)。


4.19 返回所有子图 JSON

return all_sub_detect_json

4.20 总结:核心逻辑图

原图
│
├─ 遍历每个检测框 rect
│    ├─ 跳过正常/无法判断类
│    ├─ 计算裁剪中心 mid_x
│    ├─ 计算子图左右边界
│    ├─ 截取子图
│    ├─ 修正子图中的框坐标
│    ├─ 保存本地 + OBS
│    ├─ 构建子图 JSON
│    └─ 插入结果
│
└─ 返回所有子图结果

5. assemble_results(sub_results, full_result, width, height)

最终组装所有子图结果

返回结构:

{
  "path": 原长图路径,
  "width": 长图宽度,
  "height": 长图高度,
  "short_result": [
      {
          "path": 子图路径,
          "left": 子图在原图中的位置,
          "top": ...
          "width": ...
          "height": ...
          "result": 子图的检测信息
      }
  ]
}

用于输出给上一级系统。


6.整体流程总结图

AI检测结果(result)
       ↓
[1] check_error ——日志检查错误
       ↓
[2] remove_outside_boundingbox ——过滤越界框
       ↓
[3] remove_approximate_result ——去除重复结果
       ↓
[4] get_short_image_results_middle ——生成小图 + 上传OBS + 返回JSON
       ↓
[5] assemble_results ——组装最终返回值

posted @ 2026-01-05 08:54  做梦当财神  阅读(5)  评论(0)    收藏  举报