import asyncio
from concurrent.futures import ThreadPoolExecutor
import os
import time
import numpy as np
import traceback
import cv2
import torch
import csv
from utils.general import (
check_img_size, non_max_suppression, apply_classifier, scale_coords,
xyxy2xywh, strip_optimizer, set_logging)
from models.common import DetectMultiBackend
from utils.torch_utils import select_device
from algo_config.common_config_data import *
from algo_utils.code_filter import CodeFilter
from algo_utils.code_identifier import CodeIdentifier
# from algo_utils.uk_detector import UKDetector
import json
from math import ceil
# from PIL import Image
import logging
import xml.etree.ElementTree as ET
import xml.dom.minidom
from utils.plots import Annotator, colors, save_one_box
# 同类型code不同级别时,进行合并,key为主code,可考虑更新为可配置
BBOX_MERGE_CLS_INFO = {3: [2]}
START_CUT_OFFX = 0
END_CUT_OFFX = 0
logger = logging.getLogger(__name__)
def calc_file_size(file_path):
if os.path.exists(file_path):
file_size = os.path.getsize(file_path)
else:
file_size = -1
return file_size
class Model():
def __init__(self, color_spec, device="", gray_spec=None, common_spec=None, conf_thres=0.45):
from ultralytics import YOLO
self.color_spec = color_spec
self.gray_spec = gray_spec
self.common_spec = common_spec
self.augment = True
self.conf_thres = conf_thres # 0.2
self.iou_thres = 0.5
self.agnostic_nms = False
self.classes = None
# 是否存储xml和img绘制,只有开启,绘制结果的参数才会生效
self.save_result = False
# 是否绘制
self.draw_result_on_img = False
imgsz = 1280
self.device_num = "cuda:" + str(device)
self.device = select_device(self.device_num)
# Initialize
set_logging()
# model = DetectMultiBackend(weights, device=self.device, dnn=False)
model_s = YOLO(self.color_spec['split_model'])
model_f = YOLO(self.color_spec['full_model'])
self.model_s = model_s
self.model_f = model_f
self.imgsz = imgsz
self.half = False
# 获取模型中的标注code名称
self.code_name = list(self.model_s.names.values())+list(self.model_f.names.values())
# 增加unkown code,确保SD1UK始终在最后一个
# profile_preset.json的路径
self.profile_preset_path = self.color_spec['profile_preset_path']
# code过滤
self.code_filter = CodeFilter()
# code区分合并
self.code_identifier = CodeIdentifier(self.code_name)
# 未知缺陷定位检测
# self.u3_id = self.code_name.index(SD1U3) if SD1U3 in self.code_name else -1
# self.u4_id = self.code_name.index(SD1U4) if SD1U4 in self.code_name else -1
# self.ot_id = self.code_name.index(SD1OT) if SD1OT in self.code_name else -1
# self.uk_detector = UKDetector()
def get_code_idx(self, code):
return self.code_name.index(code) if code in self.code_name else -1
def update_save_draw_action(self, save_result, draw_result_on_img):
"""
更新存储和绘制图像参数
"""
self.save_result = save_result
self.draw_result_on_img = draw_result_on_img
def create_result_dict(self):
"""
创建算法响应消息主体
:return:
"""
result = {}
result.setdefault("status", 200)
result.setdefault("message", "Success")
result.setdefault("result", [])
return result.copy()
def dump_error_result(self, code, message):
"""
异常代码
:param code:
:param message:
:return:
"""
self.result["status"] = code
if not isinstance(message, str):
message = repr(message)
self.result["message"] = message
def convert_box(self, box):
"""
左上、右下转为左上、右上、右下、左下
:param box
:return:
"""
new_box_list = []
for each_box in box:
x1, y1, x2, y2 = each_box
new_box_list.append([x1, y1, x2, y1, x2, y2, x1, y2])
return new_box_list
def convert_code_name(self, cls_list):
"""Code转换 """
code_name_list = []
for cl in cls_list:
code_name_list.append(self.code_name[int(cl)])
return code_name_list
def merge_contours(self, contours, img_shape, padding=0):
"""
合并多个轮廓的边界框
参数:
contours: 轮廓列表
img_shape: 图像形状(H,W)
padding: 在合并后的边界框周围添加的额外像素
返回:
合并后的边界框坐标(x, y, w, h)
"""
if not contours:
return 0, 0, img_shape[1], img_shape[0]
# 初始化边界框坐标
x_min = img_shape[1]
y_min = img_shape[0]
x_max = 0
y_max = 0
# 遍历所有轮廓,找到最大边界
for contour in contours:
x, y, w, h = cv2.boundingRect(contour)
x_min = min(x_min, x)
y_min = min(y_min, y)
x_max = max(x_max, x + w)
y_max = max(y_max, y + h)
# 添加padding
x_min = max(0, x_min - padding)
y_min = max(0, y_min - padding)
x_max = min(img_shape[1], x_max + padding)
y_max = min(img_shape[0], y_max + padding)
return x_min, y_min, x_max - x_min, y_max - y_min
def crop_black_borders(self, img, threshold=10, min_area=100000, padding=0):
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 二值化处理
_, thresh = cv2.threshold(gray, threshold, 255, cv2.THRESH_BINARY)
# 找到非黑色区域的轮廓
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 过滤掉太小的轮廓
filtered_contours = [c for c in contours if cv2.contourArea(c) > min_area]
if not filtered_contours:
return img # 如果没有找到符合条件的轮廓,返回原图
x, y, w, h = self.merge_contours(filtered_contours, gray.shape, padding)
return x, y, w, h
def merge_result(self, xyxy_rst, conf_rst, cls_rst):
xyxy_results = []
conf_results = []
cls_results = []
# 类别数组
# cls_list = cls_rst.unique()
cls_list = np.unique(cls_rst)
cls_num = len(cls_list)
xyxy_unique = [[] for j in range(cls_num)]
conf_unique = [[] for j in range(cls_num)]
cls_unique = [[] for j in range(cls_num)]
# 按类别分组
for c in range(cls_num):
for i in range(len(cls_rst)):
if cls_rst[i] == cls_list[c]:
xyxy_unique[c].append(xyxy_rst[i])
conf_unique[c].append(conf_rst[i])
cls_unique[c].append(cls_rst[i])
# 按类别合并: 计算iou
for i in range(cls_num):
box_result, conf_result, cls_result = self.custom_nms(xyxy_unique[i], conf_unique[i], cls_unique[i], 0.15)
xyxy_results.append(box_result)
conf_results.append(conf_result)
cls_results.append(cls_result)
# 将数据展平
xyxy_results_ = [element for sublist in xyxy_results for element in sublist]
conf_results_ = [element for sublist in conf_results for element in sublist]
cls_results_ = [element for sublist in cls_results for element in sublist]
return xyxy_results_, conf_results_, cls_results_
def augment_image(self, img):
clipLimit = 10
tileGridSize = (8, 8)
if len(img.shape) == 2: # 单通道灰度图像
clahe = cv2.createCLAHE(clipLimit=clipLimit, tileGridSize=tileGridSize)
enhanced_img = clahe.apply(img)
enhanced_img = cv2.cvtCOLOR(enhanced_img, cv2.COLOR_GRAY2BGR)
elif len(img.shape) == 3: # 三通道彩色图像
# 分离通道
b, g, r = cv2.split(img)
# 对每个通道分别应用 CLAHE
clahe = cv2.createCLAHE(clipLimit=clipLimit, tileGridSize=tileGridSize)
enhanced_b = clahe.apply(b)
enhanced_g = clahe.apply(g)
enhanced_r = clahe.apply(r)
# 合并通道
enhanced_img = cv2.merge((enhanced_b, enhanced_g, enhanced_r))
return enhanced_img
def bbox_merge_cls(self, xyxy_results, conf_results, cls_results):
'''
同类型code合并框,以merge_info为配置进行合并
'''
def is_overlap(box1, box2):
return not (box2[0] > box1[2] or box2[2] < box1[0] or box2[1] > box1[3] or box2[3] < box1[1])
def merge_bboxes(boxes):
return [min(box[0] for box in boxes),
min(box[1] for box in boxes),
max(box[2] for box in boxes),
max(box[3] for box in boxes)]
to_delete = set()
merged_boxes = []
merged_confidences = []
merged_codes = []
for key, vals in BBOX_MERGE_CLS_INFO.items():
# 找到需要过滤合并的code
code_sub_indices = [i for i in range(len(cls_results)) if cls_results[i] in vals]
code_main_indices = [i for i in range(len(cls_results)) if cls_results[i] == key]
for j in code_main_indices:
inter_boxes = [xyxy_results[j]]
inter_confs = [conf_results[j]]
for i in code_sub_indices:
if is_overlap(xyxy_results[i], xyxy_results[j]):
inter_boxes.append(xyxy_results[i])
inter_confs.append(conf_results[i])
to_delete.add(i)
if len(inter_boxes) > 1:
merged_box = merge_bboxes(inter_boxes)
merged_boxes.append(merged_box)
merged_confidences.append(conf_results[j])
merged_codes.append(key)
to_delete.add(j)
# 准备结果列表
new_boxes = []
new_confs = []
new_codes = []
# 添加未删除的原始框
for i in range(len(xyxy_results)):
if i not in to_delete:
new_boxes.append(xyxy_results[i])
new_confs.append(conf_results[i])
new_codes.append(cls_results[i])
# 添加合并后的框
new_boxes.extend(merged_boxes)
new_confs.extend(merged_confidences)
new_codes.extend(merged_codes)
return new_boxes, new_confs, new_codes
def custom_nms(self, boxes, confs, cls, iou_thresh):
boxes = np.array(boxes)
x1 = boxes[:, 0]
y1 = boxes[:, 1]
x2 = boxes[:, 2]
y2 = boxes[:, 3]
scores = np.array(confs)
areas = (y2 - y1 + 1) * (x2 - x1 + 1)
keep_boxes = []
index = scores.argsort()[::-1]
box_result = []
conf_result = []
cls_result = []
while len(index) > 0:
i = index[0]
keep_boxes.append(i)
x1_overlap = np.maximum(x1[i], x1[index[1:]])
y1_overlap = np.maximum(y1[i], y1[index[1:]])
x2_overlap = np.minimum(x2[i], x2[index[1:]])
y2_overlap = np.minimum(y2[i], y2[index[1:]])
# 计算重叠部分的面积,若没有不重叠部分则面积为 0
w = np.maximum(0, x2_overlap - x1_overlap + 1)
h = np.maximum(0, y2_overlap - y1_overlap + 1)
overlap_area = w * h
ious = overlap_area / (areas[i] + areas[index[1:]] - overlap_area)
idx = np.where(ious <= iou_thresh)[0]
# 合并大框
big_idx = np.where(ious > iou_thresh)[0]
union_index = index[big_idx + 1]
if union_index.size == 1:
lt_x = np.minimum(boxes[i][0], boxes[:, 0][union_index])[0]
lt_y = np.minimum(boxes[i][1], boxes[:, 1][union_index])[0]
rb_x = np.maximum(boxes[i][2], boxes[:, 2][union_index])[0]
rb_y = np.maximum(boxes[i][3], boxes[:, 3][union_index])[0]
elif union_index.size > 1:
lt_x = np.min(np.minimum(boxes[i][0], boxes[:, 0][union_index]))
lt_y = np.min(np.minimum(boxes[i][1], boxes[:, 1][union_index]))
rb_x = np.max(np.maximum(boxes[i][2], boxes[:, 2][union_index]))
rb_y = np.max(np.maximum(boxes[i][3], boxes[:, 3][union_index]))
else:
lt_x = boxes[i][0]
lt_y = boxes[i][1]
rb_x = boxes[i][2]
rb_y = boxes[i][3]
box_result.append([lt_x, lt_y, rb_x, rb_y])
conf_result.append(scores[i])
cls_result.append(cls[i])
index = index[idx + 1]
return box_result, conf_result, cls_result
# 执行推理
def pytorch_infer(self, im):
visualize = False
augment = False
# pred = self.model(im, augment=augment, visualize=visualize)
pred = self.model.predict(im, save=False)
# NMS
conf_thres = 0.15
iou_thres = 0.6
max_det = 100
classes = None
agnostic_nms = False
# pred = non_max_suppression(pred, conf_thres, iou_thres, classes, agnostic_nms, max_det=max_det)
return pred
def calc_code_conf_threshold(self, profile):
code_id = []
code_conf_thre = []
code_pr_thre = []
if len(profile) != 0:
for key, code in profile.items():
if not key in self.code_name:
continue
code_id.append(self.code_name.index(key))
code_conf_thre.append(code["th"])
code_pr_thre.append(code["pr"])
return code_id, code_conf_thre, code_pr_thre
def transform_bbox(self, bbox_resized, origin_size, resized_size, is_need_rotate):
W_origin, H_origin = origin_size
resized_width, resized_height = resized_size
# 缩放因子
if is_need_rotate:
scale_x = H_origin / resized_width
scale_y = W_origin / resized_height
else:
scale_y = H_origin / resized_width
scale_x = W_origin / resized_height
# 提取resized bbox坐标
xmin_r, ymin_r, xmax_r, ymax_r = bbox_resized
# 转换到旋转后的坐标系
x_rot_min = xmin_r * scale_x
y_rot_min = ymin_r * scale_y
x_rot_max = xmax_r * scale_x
y_rot_max = ymax_r * scale_y
if is_need_rotate:
# 限制坐标范围
x_rot_min = max(0, min(x_rot_min, H_origin - 1))
y_rot_min = max(0, min(y_rot_min, W_origin - 1))
x_rot_max = max(0, min(x_rot_max, H_origin - 1))
y_rot_max = max(0, min(y_rot_max, W_origin - 1))
# 逆旋转到原始坐标系
x_origin_left = W_origin - 1 - y_rot_max
x_origin_right = W_origin - 1 - y_rot_min
ymin_origin = x_rot_min
ymax_origin = x_rot_max
# 确定min和max
xmin_origin = min(x_origin_left, x_origin_right)
xmax_origin = max(x_origin_left, x_origin_right)
ymin_origin = min(ymin_origin, ymax_origin)
ymax_origin = max(ymin_origin, ymax_origin)
# 确保不超出原始图像范围
xmin_origin = max(0, min(xmin_origin, W_origin - 1))
xmax_origin = max(0, min(xmax_origin, W_origin - 1))
ymin_origin = max(0, min(ymin_origin, H_origin - 1))
ymax_origin = max(0, min(ymax_origin, H_origin - 1))
else:
# 限制坐标范围
x_rot_min = max(0, min(x_rot_min, W_origin - 1))
y_rot_min = max(0, min(y_rot_min, H_origin - 1))
x_rot_max = max(0, min(x_rot_max, W_origin - 1))
y_rot_max = max(0, min(y_rot_max, H_origin - 1))
xmin_origin = x_rot_min
xmax_origin = x_rot_max
ymin_origin = y_rot_min
ymax_origin = y_rot_max
return (int(xmin_origin), int(ymin_origin), int(xmax_origin), int(ymax_origin))
def infer_cv_detect(self, origin_image):
''' 检测黑框等CV直接可检出缺陷
'''
return self.code_identifier.identify_black_rect_as_OT(origin_image)
def temp_filter_code(self, img_path, pred_cls):
"""临时过滤,如果有对应code则不输出
"""
for k, temp_code_list in TEMP_FILTER_DICT.items():
if os.path.basename(img_path).startswith(k):
for temp_code in temp_code_list:
temp_idx = self.get_code_idx(temp_code)
if temp_idx == pred_cls:
return True
return False
def infer_od(self, origin_image, origin_image_before_rot, source, code_id, code_conf_thre, code_pr_thre,
Image_Width, Image_Height, is_need_rotate, target_size=(1280, 1280)):
"""目标检测推理
传入是resize +(可能旋转)的图像,所以需要传入原始长宽
可能多个处理均需要旋转,所以传入旋转后的图,只在外部旋转一次
"""
# Image_Width = origin_image.shape[1]
# Image_Height = origin_image.shape[0]
xyxy_results = []
conf_results = []
cls_results = []
# 图像
image = origin_image.copy()
infer_full = True
if infer_full:
# Convert
image = cv2.resize(image, target_size, interpolation=cv2.INTER_LINEAR)
img = image.transpose((2, 0, 1))[::-1] # HWC to CHW, BGR to RGB
img = np.ascontiguousarray(img)
im = torch.from_numpy(img).to(self.device)
im = im.half() if self.half else im.float() # uint8 to fp16/32
im /= 255 # 0 - 255 to 0.0 - 1.0
if len(im.shape) == 3:
im = im[None] # expand for batch dim
# 执行推理
pred = self.pytorch_infer(im)
for i, det in enumerate(pred):
for *xyxy, conf, cls in zip(det.boxes.xyxy, det.boxes.conf, det.boxes.cls):
temp_xyxy = []
# 判断检出的code是否满足置信度阈值,以及是否过滤项
if len(code_id) > 0:
clsID_pred = int(cls.cpu().detach())
# 过滤特定产品code
pr_threshold = code_pr_thre[code_id.index(clsID_pred)]
# 设置为10000的为不需要检出的code,通常是用于抑制过检的OK Code
if pr_threshold == 10000:
continue
if TEMP_FILTER_ENABLE and self.temp_filter_code(source, clsID_pred):
continue
conf_pred = float(conf.cpu().detach())
conf_threshold = code_conf_thre[code_id.index(clsID_pred)]
if conf_pred < conf_threshold:
continue
else:
conf_pred = float(conf.cpu().detach())
clsID_pred = int(cls.cpu().detach())
for _xyxy in xyxy:
temp_xyxy = [int(ts.item()) for ts in _xyxy.cpu().detach()]
# temp_xyxy.append(int(_xyxy.cpu().detach()))
# 图像转换回bbox
cur_xyxy = self.transform_bbox(temp_xyxy, (Image_Width, Image_Height), target_size, is_need_rotate)
xyxy_results.append(cur_xyxy)
conf_results.append(round(conf_pred, 4))
cls_results.append(clsID_pred)
# 合并相同code相交的情况
xyxy_results, conf_results, cls_results = self.code_identifier.merge_codes_bbox(xyxy_results, conf_results,
cls_results)
# 合并不同code重叠的情况
if len(xyxy_results) > 0:
xyxy_results, conf_results, cls_results = self.code_identifier.merge_overlap_codes(xyxy_results,
conf_results,
cls_results,
MERGE_DIFF_CODE_BBOX_IOU_THRESHOLD)
# 更新P2/P3 code,此处的code需要提取图像,且bbox是经过transform之后的
cls_results, p2_max_bboxes = self.code_identifier.identify_P2P3(origin_image_before_rot, xyxy_results,
cls_results)
# exists_u3u4 = self.u3_id in cls_results or self.u4_id in cls_results
# 此处的坐标已变化为原始图,所以传入的长宽也需要变换之前的
xyxy_results, conf_results, cls_results = self.code_filter.filter_edge_code(xyxy_results, conf_results,
cls_results, Image_Width,
Image_Height)
return xyxy_results, conf_results, cls_results
def infer_cut(self, origin_image, code_id, code_conf_thre, code_pr_thre):
def post_processing(pred):
one_xyxy, one_conf, one_cls = [], [], []
for _, det in enumerate(pred):
for *xyxy, conf, cls in zip(det.boxes.xyxy, det.boxes.conf, det.boxes.cls):
# 判断检出的code是否满足置信度阈值
if len(code_id) > 0:
clsID_pred = int(cls.cpu().detach())
if not clsID_pred in code_id:
# 没有在待处理的id中
continue
pr_threshold = code_pr_thre[code_id.index(clsID_pred)]
conf_pred = float(conf.cpu().detach())
conf_threshold = code_conf_thre[code_id.index(clsID_pred)]
if conf_pred < conf_threshold:
continue
else:
conf_pred = float(conf.cpu().detach())
clsID_pred = int(cls.cpu().detach())
# 写入筛选后的结果
temp_xyxy = []
temp_xyxy.append(int(xyxy[0][0].cpu().detach()))
temp_xyxy.append(int(xyxy[0][1].cpu().detach()))
temp_xyxy.append(int(xyxy[0][2].cpu().detach()))
temp_xyxy.append(int(xyxy[0][3].cpu().detach()))
# 各类边缘缺陷过滤
# if self.filter_od_edge_code(edge_code_filter, clsID_pred, temp_xyxy):
# continue
if round(conf_pred, 4) > self.conf_thres:
one_xyxy.append(temp_xyxy)
one_conf.append(round(conf_pred, 4))
one_cls.append(clsID_pred)
return one_xyxy, one_conf, one_cls
# 图像增强
image = self.augment_image(origin_image)
model_size = self.imgsz
overlap = 0.25
overlap_size = int(overlap / 2.0 * model_size)
valid_lt_x, valid_lt_y, w, h = self.crop_black_borders(origin_image)
valid_rb_x, valid_rb_y = valid_lt_x+w, valid_lt_y+h
# 实际切图,第一张保持切图size,其他均回退overlap_size,所以除第一张外,
# 实际坐标位移变化为model_size-overlap_size
h_index = ceil((h - model_size) / (model_size - overlap_size)) + 1
w_index = ceil((w - model_size) / (model_size - overlap_size)) + 1
xyxy_rst = []
conf_rst = []
cls_rst = []
w_with_end_offx = w - END_CUT_OFFX
for i in range(h_index):
for j in range(w_index):
start_y = max(int(i * model_size - overlap_size * i) + valid_lt_y, 0)
start_x = max(int(j * model_size - overlap_size * j) + valid_lt_x, 0)
end_x = start_x + model_size
end_y = start_y + model_size
# 超限位置
if end_x > valid_rb_x:
over_size = end_x - w_with_end_offx
end_x = w_with_end_offx
start_x = start_x - over_size
if end_y > valid_rb_y:
over_size = end_y - h
end_y = h
start_y = start_y - over_size
# 截图
img_tailor = image[int(start_y):int(end_y), int(start_x):int(end_x)]
img = img_tailor.transpose((2, 0, 1))[::-1] # HWC to CHW, BGR to RGB
img = np.ascontiguousarray(img)
im = torch.from_numpy(img).to(self.device)
im = im.half() if self.half else im.float() # uint8 to fp16/32
im /= 255 # 0 - 255 to 0.0 - 1.0
if len(im.shape) == 3:
im = im[None] # expand for batch dim
# 执行推理
pred = self.model_s.predict(im, save=False)
one_xyxy, one_conf, one_cls = post_processing(pred)
one_xyxy = [[xyxy[0]+start_x, xyxy[1]+start_y, xyxy[2]+start_x, xyxy[3]+start_y] for xyxy in one_xyxy]
# if self.draw_result_on_img:
# img_name = str(i) + '_' + str(j) + '.jpg'
# img_tailor = np.ascontiguousarray(img_tailor)
# self.draw_result(img_tailor, one_xyxy, one_conf, one_cls, img_name, self.save_root+'/CUT')
xyxy_rst += one_xyxy
conf_rst += one_conf
cls_rst += one_cls
im_full = cv2.resize(image, (self.imgsz, self.imgsz), interpolation=cv2.INTER_LINEAR)
im_full = im_full.transpose((2, 0, 1))[::-1] # HWC to CHW, BGR to RGB
im_full = np.ascontiguousarray(im_full)
im = torch.from_numpy(im_full).to(self.device)
im = im.half() if self.half else im.float() # uint8 to fp16/32
im /= 255 # 0 - 255 to 0.0 - 1.0
if len(im.shape) == 3:
im = im[None] # expand for batch dim
# 执行推理
pred = self.model_f.predict(im, save=False)
one_xyxy, one_conf, one_cls = post_processing(pred)
one_cls = [cls+len(self.model_s.names) for cls in one_cls]
one_xyxy = [[xyxy[0]/self.imgsz*w, xyxy[1]/self.imgsz*h, xyxy[2]/self.imgsz*w, xyxy[3]/self.imgsz*h] for xyxy in one_xyxy]
xyxy_rst += one_xyxy
conf_rst += one_conf
cls_rst += one_cls
# 缺陷结果合并
xyxy_results, conf_results, cls_results = self.merge_result(xyxy_rst, conf_rst, cls_rst)
# 合并相交的STM0/1
xyxy_results, conf_results, cls_results = self.bbox_merge_cls(xyxy_results, conf_results, cls_results)
pred_sorted, pred_unique, cls_list = self.sort_by_conf(xyxy_results, conf_results, cls_results)
pred_sorted[0] = self.convert_box(pred_sorted[0])
pred_sorted[2] = self.convert_code_name(pred_sorted[2])
return pred_sorted, pred_unique, cls_list, xyxy_results, conf_results, cls_results
def split_get_result(self, source, origin_image, profile):
"""
单张图片推理,加载图片并且执行前向传播,得到该张图片预测结果
:param source:
:return:
"""
# 解析code阈值
code_id, code_conf_thre, code_pr_thre = self.calc_code_conf_threshold(profile)
xyxy_results = []
conf_results = []
cls_results = []
# origin_image = cv2.imdecode(np.fromfile(source, dtype=np.uint8), cv2.IMREAD_COLOR)
Image_Width = origin_image.shape[1]
Image_Height = origin_image.shape[0]
origin_image_before_rot = origin_image.copy()
t1 = time.time()
# 是否需要旋转处理的图像
is_need_rotate = Image_Width < Image_Height
if is_need_rotate:
origin_image = cv2.rotate(origin_image, cv2.ROTATE_90_COUNTERCLOCKWISE)
with ThreadPoolExecutor(max_workers=2) as executor:
future1 = executor.submit(self.infer_od, origin_image, origin_image_before_rot, source, code_id, code_conf_thre, code_pr_thre, Image_Width, Image_Height, is_need_rotate)
od_result = future1.result()
xyxy_results, conf_results, cls_results = od_result
pred_sorted, pred_unique, cls_list = self.sort_by_conf(xyxy_results, conf_results, cls_results)
pred_sorted[0] = self.convert_box(pred_sorted[0])
pred_sorted[2] = self.convert_code_name(pred_sorted[2])
# 是否保存
if self.save_result:
# 绘制存储
if len(cls_results) == 0:
concat_save_path = f"{self.save_root}/OK"
else:
concat_save_path = f"{self.save_root}/NG"
xml_img_folder = os.path.dirname(source)
img_name = os.path.basename(source)
xml_img_filename = img_name
if self.draw_result_on_img:
self.draw_result(origin_image_before_rot, xyxy_results, conf_results, cls_results, img_name,
concat_save_path)
# 存储标注信息
self.dump_xml(xml_img_folder, xml_img_filename, origin_image_before_rot.shape, xyxy_results,
conf_results, cls_results, concat_save_path)
# 如果没有code
if len(conf_results) < 1:
# SD1F1为无缺陷
cls_results = ['difficult']
conf_results = ['1.0']
m_H = origin_image_before_rot.shape[0]
m_W = origin_image_before_rot.shape[1]
xyxy_results = [[0, 0, int(m_W), 0, int(m_W), int(m_H), 0, int(m_H)]]
pred_sorted = [xyxy_results, conf_results, cls_results]
pred_unique = []
cls_list = []
print("cost:", time.time() - t1)
return pred_sorted, pred_unique, cls_list, Image_Width, Image_Height
def get_result(self, source, origin_image, profile):
"""
单张图片推理,加载图片并且执行前向传播,得到该张图片预测结果
:param source:
:return:
"""
# 解析code阈值
code_id, code_conf_thre, code_pr_thre = self.calc_code_conf_threshold(profile)
xyxy_results = []
conf_results = []
cls_results = []
# origin_image = cv2.imdecode(np.fromfile(source, dtype=np.uint8), cv2.IMREAD_COLOR)
Image_Width = origin_image.shape[1]
Image_Height = origin_image.shape[0]
origin_image_before_rot = origin_image.copy()
t1 = time.time()
# 是否需要旋转处理的图像
# is_need_rotate = Image_Width < Image_Height
# if is_need_rotate:
# origin_image = cv2.rotate(origin_image, cv2.ROTATE_90_COUNTERCLOCKWISE)
# with ThreadPoolExecutor(max_workers=8) as executor:
# future1 = executor.submit(self.infer_cut, origin_image, code_id, code_conf_thre, code_pr_thre)
# cut_result = future1.result()
cut_result = self.infer_cut(origin_image, code_id, code_conf_thre, code_pr_thre)
pred_sorted, pred_unique, cls_list, xyxy_results, conf_results, cls_results = cut_result
# 如果没有code
if len(conf_results) < 1:
# SD1F1为无缺陷
cls_results = ['OTHER']
conf_results = [0.9999]
xyxy_results = [[0, 0, Image_Width, Image_Height]]
pred_sorted = [xyxy_results, conf_results, cls_results]
pred_unique = []
cls_list = []
# 是否保存
if self.save_result:
# 绘制存储
concat_save_path = f"{self.save_root}/RES"
xml_img_folder = os.path.dirname(source)
img_name = os.path.basename(source)
xml_img_filename = img_name
if self.draw_result_on_img:
self.draw_result(origin_image_before_rot, xyxy_results, conf_results, cls_results, img_name,
concat_save_path)
# 存储标注信息
self.dump_xml(xml_img_folder, xml_img_filename, origin_image_before_rot.shape, xyxy_results,
conf_results, cls_results, concat_save_path)
print("cost:", time.time() - t1)
return pred_sorted, pred_unique, cls_list, Image_Width, Image_Height
def draw_result(self, image, xyxy_rst, conf_rst, cls_rst, img_name, concat_save_path):
save_path = concat_save_path
if not os.path.exists(save_path):
os.makedirs(save_path)
image_save_path = f"{save_path}/{img_name}"
line_thickness = 1
annotator = Annotator(image, line_width=line_thickness, example=str(self.code_name))
for i in range(len(cls_rst)):
if cls_rst[i] == 'OTHER':
c = len(self.code_name)
label = f'{'OTHER'} {conf_rst[i]:.2f}'
else:
c = int(cls_rst[i]) # integer class
label = f'{self.code_name[c]} {conf_rst[i]:.2f}'
annotator.box_label(xyxy_rst[i], label, color=colors(c, True))
image = annotator.result()
cv2.imencode('.jpg', image)[1].tofile(image_save_path)
def dump_xml(self, xml_folder, xml_filename, shape, xyxy_results, conf_results, cls_results, concat_save_path):
annotation = ET.Element("annotation")
# 添加子元素
folder = ET.SubElement(annotation, "folder")
folder.text = xml_folder
filename = ET.SubElement(annotation, "filename")
filename.text = xml_filename
segmented = ET.SubElement(annotation, "segmented")
segmented.text = str(0)
source = ET.SubElement(annotation, "source")
database = ET.SubElement(source, "database")
database.text = 'Unknown'
size = ET.SubElement(annotation, "size")
width = ET.SubElement(size, "width")
width.text = str(shape[1])
height = ET.SubElement(size, "height")
height.text = str(shape[0])
depth = ET.SubElement(size, "depth")
depth.text = str(shape[2])
# 填充标注信息
for i in range(len(cls_results)):
if cls_results[i] == 'OTHER':
label = 'OTHER'
else:
label = self.code_name[cls_results[i]]
m_name = label
# m_difficult = label[-1]
m_subConf = round(conf_results[i], 3)
m_xmin = xyxy_results[i][0]
m_ymin = xyxy_results[i][1]
m_xmax = xyxy_results[i][2]
m_ymax = xyxy_results[i][3]
m_object = ET.SubElement(annotation, "object")
name = ET.SubElement(m_object, "name")
name.text = m_name
pose = ET.SubElement(m_object, "pose")
pose.text = 'Unspecifed'
truncated = ET.SubElement(m_object, "truncated")
truncated.text = str(0)
difficult = ET.SubElement(m_object, "difficult")
difficult.text = str(0)
contrast = ET.SubElement(m_object, "contrast")
contrast.text = str(0)
luminance = ET.SubElement(m_object, "luminance")
luminance.text = str(0)
subConf = ET.SubElement(m_object, "subConf")
subConf.text = str(m_subConf)
bndbox = ET.SubElement(m_object, "bndbox")
xmin = ET.SubElement(bndbox, "xmin")
xmin.text = str(int(m_xmin))
ymin = ET.SubElement(bndbox, "ymin")
ymin.text = str(int(m_ymin))
xmax = ET.SubElement(bndbox, "xmax")
xmax.text = str(int(m_xmax))
ymax = ET.SubElement(bndbox, "ymax")
ymax.text = str(int(m_ymax))
# 将 XML 结构保存为文件
save_path = concat_save_path
if not os.path.exists(save_path):
os.makedirs(save_path)
xml_file = xml_filename.split('.')[0]
xml_filepath = f"{save_path}/{xml_file}.xml"
tree = ET.ElementTree(annotation)
tree.write(xml_filepath, encoding="utf-8", xml_declaration=True)
# 使用 xml.dom.minidom 格式化 XML 文件
dom = xml.dom.minidom.parse(xml_filepath)
with open(xml_filepath, "w", encoding="utf-8") as f:
f.write(dom.toprettyxml(indent=" ")) # 使用四个空格作为缩进
def get_online_anomaly_conf_threshold(self, profile, label):
online_conf_thr = 0.1
try:
if label in profile and "th" in profile[label]:
online_conf_thr = profile[label]["th"]
except Exception as e:
print(f"Get anomaly conf threshold error:{e}")
pass
return online_conf_thr
def sort_by_priority(self, profile, pred_unique, cls_list):
# 判断优先级文档是否存在\内容是否不全
if len(profile) == 0:
box_result = [[0, 0, 20, 0, 20, 20, 0, 20]]
conf_result = [1.0]
cls_result = ['ISSUE']
return box_result, conf_result, cls_result
xyxy_unique = pred_unique[0]
conf_unique = pred_unique[1]
cls_unique = pred_unique[2]
# 将code_list转换为code_name_list
if not isinstance(cls_list[0], str):
code_name_list = self.convert_code_name(cls_list)
# 根据优先级,返回优先级最高的code
box_result = []
conf_result = []
cls_result = []
for key, value in profile.items():
if key not in code_name_list:
continue
index = code_name_list.index(key)
temp_box = [xyxy_unique[index][0]]
if len(temp_box[0]) == 4:
box_result = self.convert_box(temp_box)
else:
box_result = temp_box[0]
conf_result.append(conf_unique[index][0])
cls_result.append(key)
break
return box_result, conf_result, cls_result
def sort_by_conf(self, xyxy_results, conf_results, cls_results):
'''
1、先计算code_list
2、再对每一类code按照conf排序
3、返回排序后的结果和code_list
'''
# 类别数组
# cls_list = cls_rst.unique()
cls_list = np.unique(cls_results)
cls_num = len(cls_list)
xyxy_unique = [[] for j in range(cls_num)]
conf_unique = [[] for j in range(cls_num)]
cls_unique = [[] for j in range(cls_num)]
# 按类别分组
for c in range(cls_num):
temp_xyxy = []
temp_conf = []
temp_cls = []
for i in range(len(cls_results)):
if cls_results[i] == cls_list[c]:
temp_xyxy.append(xyxy_results[i])
temp_conf.append(conf_results[i])
temp_cls.append(cls_results[i])
sorted_list = sorted(temp_conf, reverse=True)
indexes = [temp_conf.index(x) for x in sorted_list]
for index in indexes:
xyxy_unique[c].append(temp_xyxy[index])
conf_unique[c].append(temp_conf[index])
cls_unique[c].append(float(temp_cls[index]))
# 也可以将unique中的元素展平
xyxy_results_ = [element for sublist in xyxy_unique for element in sublist]
conf_results_ = [element for sublist in conf_unique for element in sublist]
cls_results_ = [element for sublist in cls_unique for element in sublist]
return [xyxy_results_, conf_results_, cls_results_], [xyxy_unique, conf_unique, cls_unique], cls_list.tolist()
def get_final_result(self, pred_result):
"""
计算该张图片的最终结果
(接口测试阶段,默认取第一个结果为最终结果)
:param pred_result:
:return:
"""
box_list, conf_list, code_list = pred_result
final_result_dic = {}
final_result_dic.setdefault("img_cls", [code_list[0]])
final_result_dic.setdefault("img_box", [box_list[0]])
final_result_dic.setdefault("img_score", [conf_list[0]])
return final_result_dic
def get_group_final(self, pred_result, gid, savepath):
"""
计算该批次任务的最终结果
(接口测试阶段,默认取最后一张图片的第一个结果作为最终结果)
:param pred_result:
:param gid:
:param savepath:
:return:
"""
box_list, conf_list, code_list = pred_result
group_final_dic = {}
group_final_dic.setdefault("img_cls", [code_list[0]])
if len(box_list[0]) == 1:
group_final_dic.setdefault("img_box", box_list[0])
else:
group_final_dic.setdefault("img_box", [box_list[0]])
group_final_dic.setdefault("img_score", [conf_list[0]])
group_final_dic.setdefault("gid", gid)
group_final_dic.setdefault("defect", len(code_list))
group_final_dic.setdefault("type", "Final")
group_final_dic.setdefault("savepath", savepath)
return group_final_dic
def check_files(self, img_json, retry_times=1):
"""
执行推理任务前检查必要的文件是否存在
:param img_json:
:return:
"""
cur_file_path = ""
while retry_times >= 0:
try:
img_info_list = img_json["image"]
# 实际任务单次只会有一张,所以可以读图后返回
for each in img_info_list:
img_path = each["path"]
cur_file_path = img_path
raw_img = cv2.imdecode(np.fromfile(img_path, dtype=np.uint8), cv2.IMREAD_COLOR)
return raw_img
except Exception as e:
retry_times -= 1
stack_trace = traceback.format_exc()
file_size = calc_file_size(cur_file_path)
# 读图失败优先重试,达到重试次数再报错
if retry_times >= 0:
logger.info(
f"==SD1 Algo==: Image read error, Retry!!:{cur_file_path}, size:{file_size}, {stack_trace}")
time.sleep(RETRY_CHECK_SLEEP_TIME)
else:
self.dump_error_result(610, 'Image read error')
logger.error(f"==SD1 Algo==: Image read error:{stack_trace}")
logger.info(f"==SD1 Algo==: {cur_file_path}: size:{file_size}")
return []
def check_json(self, profile_preset_path):
"""
执行推理任务前检查必要的文件是否存在
:param img_json:
:return:
"""
try:
with open(profile_preset_path, 'r', encoding='utf-8') as file:
profile_preset = json.load(file)
except:
profile_preset = {}
self.dump_error_result(911, 'Lack of profile_preset.jsom')
return profile_preset
def parse_code_priority(self, img_json, profile_preset):
'''
如果csv完整,就使用csv信息
如果csv信息不完整,就使用json信息补充
如果csv为空,就直接使用json配置
'''
profile = {}
try:
profile_path = img_json["info"]["profile_path"]
with open(profile_path, 'r', encoding="utf-8") as f:
reader = csv.reader(f)
csv_code = []
for row in reader:
if row[0] != 'DEFECT_SIZE':
priority = row[1]
confidence = row[2]
defcet_name = row[3]
# 有code名,缺失优先级、置信度,且在预置的json中,则使用预置的值
if priority == '':
# csv优先级为空,使用默认值,如果默认值没有,则设置为666
if len(profile_preset) > 0 and defcet_name in profile_preset.keys():
priority = profile_preset[defcet_name]["PRIORITY"]
else:
priority = 666
if confidence == '':
if len(profile_preset) > 0 and defcet_name in profile_preset.keys():
confidence = profile_preset[defcet_name]["CONFIDENCE"]
else:
confidence = 0.2
profile[defcet_name] = {}
profile[defcet_name].setdefault('th', float(confidence))
profile[defcet_name].setdefault('pr', int(priority))
# csv信息不完整,使用json信息填充
if len(profile) < len(profile_preset):
for key, value in profile_preset.items():
if key not in profile.keys():
profile[key] = {}
profile[key].setdefault('th', float(value['CONFIDENCE']))
profile[key].setdefault('pr', int(value['PRIORITY']))
elif len(profile_preset) < 0:
# 如果没有json文件
if len(profile) < len(self.code_name):
for c_name in self.code_name:
if c_name not in profile.keys():
profile[c_name] = {}
profile[c_name].setdefault('th', 0.15)
profile[c_name].setdefault('pr', 666)
except Exception as e:
if len(profile_preset) > 0:
for key, value in profile_preset.items():
profile[key] = {}
profile[key].setdefault('th', float(value['CONFIDENCE']))
profile[key].setdefault('pr', int(value['PRIORITY']))
else:
for c_name in self.code_name:
if c_name not in profile.keys():
profile[c_name] = {}
profile[c_name].setdefault('th', 0.15)
profile[c_name].setdefault('pr', 1)
# 按照pr等级排序
profile = dict(sorted(profile.items(), key=lambda x: x[1]['pr']))
return profile
def infer(self, img_json, testing=False):
"""
infrence main program
:param img_json:
:return: reulst(有固定格式,按照接口规范返回)
"""
# 创建推理响应主体
self.result = self.create_result_dict()
pattern_results_list = []
img_info_list = img_json['image']
self.save_root = img_json["info"]["saveROOT_PATH"]
# 检查图片及配置文件是否存在
origin_image = self.check_files(img_json, RETRY_TIMES)
# 检查profile_preset.json文件是否存在
profile_preset = self.check_json(self.profile_preset_path)
if self.result['status'] == 200:
# 读取用户配置文件
# 获取code优先级
profile = self.parse_code_priority(img_json, profile_preset)
p_box_list = []
p_conf_list = []
p_cls_list = []
p_code_list = []
for each in img_info_list:
img_path = each["path"]
# 预测结果
# if not testing:
# pred_result, pred_unique, cls_list, Image_Width, Image_Height = self.get_result(img_path, origin_image, profile)
# else:
# pred_result, pred_unique, cls_list, Image_Width, Image_Height = self.split_get_result(img_path, origin_image, profile)
pred_result, pred_unique, cls_list, Image_Width, Image_Height = self.get_result(img_path, origin_image, profile)
# 结果优先级排序
if len(cls_list) > 0:
priority_result = self.sort_by_priority(profile, pred_unique, cls_list)
else:
priority_result = pred_result
final_result = self.get_final_result(priority_result)
# 单张图片的推理结果
pattern_results = {}
pattern_results.setdefault("img_cls", pred_result[2])
pattern_results.setdefault("img_box", pred_result[0])
pattern_results.setdefault("img_score", pred_result[1])
if "uid" in each:
pattern_results.setdefault("uid", each["uid"])
if "gid" in each:
pattern_results.setdefault("gid", each["gid"])
pattern_results.setdefault("defect", len(pred_result[2]))
if "type" in each:
pattern_results.setdefault("type", each["type"])
pattern_results.setdefault("savepath", img_json["info"]["saveROOT_PATH"])
pattern_results.setdefault("final", final_result)
# 业务特殊需求返回
ATTR = {"IMAGE_PIXEL_WIDTH": Image_Width, "IMAGE_PIXEL_HEIGHT": Image_Height}
pattern_results.setdefault("ATTR", ATTR)
pattern_results_list.append(pattern_results)
# 用于group寻优
if len(cls_list) > 0 and priority_result[2][0] != 'ISSUE':
p_box_list.append([priority_result[0]])
p_conf_list.append(priority_result[1])
p_cls_list.append(priority_result[2])
p_code_list.append(self.code_name.index(priority_result[2][0]))
else:
p_box_list.append([priority_result[0]])
p_conf_list.append(priority_result[1])
p_cls_list.append(priority_result[2])
p_code_list.append(-1)
# 多张图最优结果中再次取最优
if len(np.unique(p_code_list)) == 1 and np.unique(p_code_list) == -1:
priority_result = [p_box_list[0], p_conf_list[0], p_cls_list[0]]
p_code_list = []
else:
length = len(p_code_list)
del_index = 0
while del_index < length:
if p_code_list[del_index] == -1:
del p_box_list[del_index]
del p_conf_list[del_index]
del p_cls_list[del_index]
del p_code_list[del_index]
length -= 1
else:
del_index += 1
if len(p_code_list) > 0:
p_pred_unique = [p_box_list, p_conf_list, p_cls_list]
priority_result = self.sort_by_priority(profile, p_pred_unique, p_code_list)
if 'gid' in img_json['image'][0]:
group_result = self.get_group_final(priority_result, img_json['image'][0]['gid'], img_json['info']['saveROOT_PATH'])
pattern_results_list.append(group_result)
self.result["result"] = pattern_results_list
return self.result
这段代码详细解释一下
最新发布