238 lines
9.5 KiB
Python
238 lines
9.5 KiB
Python
|
#!/usr/bin/env python3
|
|||
|
# -*- coding: utf-8 -*-
|
|||
|
"""
|
|||
|
car_measure.py
|
|||
|
--------------
|
|||
|
1. 读取显著性图 -> 阈值化生成纯白掩模
|
|||
|
2. 形态学闭运算 -> 去噪 & 填孔
|
|||
|
3. 计算 + 绘制外接矩形 (显示宽、高像素)
|
|||
|
4. 霍夫圆检测 -> 仅画圆心 & 连线 + 距离标注
|
|||
|
所有可视化与结果文件统一写到 out_dir
|
|||
|
"""
|
|||
|
|
|||
|
import os
|
|||
|
import cv2
|
|||
|
import numpy as np
|
|||
|
import matplotlib.pyplot as plt
|
|||
|
from PIL import Image, ImageDraw, ImageFont
|
|||
|
from u2net_saliency import generate_saliency_map
|
|||
|
|
|||
|
# ----------------------------------------------------------------------
|
|||
|
# -------------- 辅助:显著性图增强 & 调试可视化(可选) -------------------
|
|||
|
# ----------------------------------------------------------------------
|
|||
|
def enhance_saliency_map(saliency_map):
|
|||
|
"""对显著性图做对比度增强、CLAHE、双边滤波——调参用,可删"""
|
|||
|
saliency_map = cv2.normalize(saliency_map, None, 0, 255, cv2.NORM_MINMAX)
|
|||
|
clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8))
|
|||
|
saliency_map = clahe.apply(saliency_map)
|
|||
|
saliency_map = cv2.bilateralFilter(saliency_map, 9, 75, 75)
|
|||
|
return saliency_map
|
|||
|
|
|||
|
# ----------------------------------------------------------------------
|
|||
|
# ------------------------- 圆心检测 & 距离标注 -------------------------
|
|||
|
# ----------------------------------------------------------------------
|
|||
|
import os
|
|||
|
import cv2
|
|||
|
import numpy as np
|
|||
|
|
|||
|
def detect_and_draw_circles(salient_path, original_path, output_dir):
|
|||
|
"""
|
|||
|
霍夫圆检测:
|
|||
|
- 在原图上:画圆心、连线、标注距离
|
|||
|
- 在显著图上:画完整圆 和 圆心
|
|||
|
—— 输出两张图:
|
|||
|
- detected_centers_salient.png
|
|||
|
- detected_centers_original.png
|
|||
|
"""
|
|||
|
salient_img = cv2.imread(salient_path, cv2.IMREAD_GRAYSCALE)
|
|||
|
original_img = cv2.imread(original_path)
|
|||
|
if salient_img is None or original_img is None:
|
|||
|
raise FileNotFoundError("Salient 或 original 图片路径有误")
|
|||
|
|
|||
|
# 模糊+圆检测
|
|||
|
blurred = cv2.GaussianBlur(salient_img, (9, 9), 2)
|
|||
|
circles = cv2.HoughCircles(
|
|||
|
blurred, cv2.HOUGH_GRADIENT,
|
|||
|
dp=1.2, minDist=290,
|
|||
|
param1=50, param2=17,
|
|||
|
minRadius=85, maxRadius=95
|
|||
|
)
|
|||
|
|
|||
|
output_salient = cv2.cvtColor(salient_img, cv2.COLOR_GRAY2BGR)
|
|||
|
output_original = original_img.copy()
|
|||
|
|
|||
|
if circles is not None:
|
|||
|
circles = np.uint16(np.around(circles))
|
|||
|
centers = sorted([(c[0], c[1], c[2]) for c in circles[0]],
|
|||
|
key=lambda p: p[1], reverse=True)[:2]
|
|||
|
|
|||
|
# 显著图上:画完整圆 + 圆心
|
|||
|
for (x, y, r) in centers:
|
|||
|
cv2.circle(output_salient, (x, y), r, (0, 255, 0), 2) # 画圆边
|
|||
|
cv2.circle(output_salient, (x, y), 3, (0, 0, 255), -1) # 画圆心
|
|||
|
|
|||
|
# 原图上:画圆心 + 连线 + 距离
|
|||
|
if len(centers) >= 2:
|
|||
|
(x1, y1, _), (x2, y2, _) = centers
|
|||
|
cv2.circle(output_original, (x1, y1), 3, (0, 255, 0), -1)
|
|||
|
cv2.circle(output_original, (x2, y2), 3, (0, 255, 0), -1)
|
|||
|
cv2.line(output_original, (x1, y1), (x2, y2), (0, 0, 255), 2)
|
|||
|
|
|||
|
dist = np.hypot(x1 - x2, y1 - y2)
|
|||
|
mid_pt = (int((x1 + x2) / 2), int((y1 + y2) / 2) - 10)
|
|||
|
cv2.putText(output_original, f"{dist:.1f}px", mid_pt,
|
|||
|
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 0, 0), 2)
|
|||
|
|
|||
|
print(f"[Circle] 两圆心距离:{dist:.2f} px")
|
|||
|
else:
|
|||
|
print("[Circle] 检测到的圆少于 2 个")
|
|||
|
else:
|
|||
|
print("[Circle] 未检测到圆")
|
|||
|
|
|||
|
os.makedirs(output_dir, exist_ok=True)
|
|||
|
cv2.imwrite(os.path.join(output_dir, 'detected_centers_salient.png'), output_salient)
|
|||
|
cv2.imwrite(os.path.join(output_dir, 'detected_centers_original.png'), output_original)
|
|||
|
|
|||
|
|
|||
|
# ----------------------------------------------------------------------
|
|||
|
# ----------------------- 外接矩形 & 像素尺寸 ---------------------------
|
|||
|
# ----------------------------------------------------------------------
|
|||
|
def _get_font(size):
|
|||
|
"""跨平台字体加载"""
|
|||
|
for path in ("/usr/share/fonts/truetype/ubuntu/Ubuntu-B.ttf", "arial.ttf"):
|
|||
|
try:
|
|||
|
return ImageFont.truetype(path, size)
|
|||
|
except IOError:
|
|||
|
continue
|
|||
|
return ImageFont.load_default()
|
|||
|
|
|||
|
def calculate_and_draw_bbox(mask_path,
|
|||
|
output_mask_path,
|
|||
|
original_path=None,
|
|||
|
output_original_path=None,
|
|||
|
display_width=None,
|
|||
|
display_height=None):
|
|||
|
"""
|
|||
|
仅画一条顶边宽度线、一条右边高度线,并标注像素尺寸
|
|||
|
"""
|
|||
|
# ---------- 获取外接框 ----------
|
|||
|
mask_img = Image.open(mask_path).convert("L")
|
|||
|
arr = np.array(mask_img)
|
|||
|
coords = np.argwhere(arr > 0)
|
|||
|
if coords.size == 0:
|
|||
|
raise RuntimeError("掩模为空,无法测量尺寸")
|
|||
|
|
|||
|
ymin, xmin = coords.min(axis=0)
|
|||
|
ymax, xmax = coords.max(axis=0)
|
|||
|
w_px = xmax - xmin + 1
|
|||
|
h_px = ymax - ymin + 1
|
|||
|
|
|||
|
show_w = w_px if display_width is None else display_width
|
|||
|
show_h = h_px if display_height is None else display_height
|
|||
|
font = _get_font(34)
|
|||
|
|
|||
|
# ---------- 在掩模图上绘制 ----------
|
|||
|
vis_mask = mask_img.convert("RGB")
|
|||
|
draw_m = ImageDraw.Draw(vis_mask)
|
|||
|
|
|||
|
# 顶边水平线
|
|||
|
draw_m.line([(xmin, ymin), (xmax, ymin)], fill="red", width=4)
|
|||
|
# 右边垂直线
|
|||
|
draw_m.line([(xmax, ymin), (xmax, ymax)], fill="red", width=4)
|
|||
|
|
|||
|
# 文本位置计算
|
|||
|
# 宽度文字:顶边中点偏上 10px;若超出图片则放到线下方 10px
|
|||
|
tx_w = int((xmin + xmax) / 2) - 40
|
|||
|
ty_w = ymin - 40
|
|||
|
if ty_w < 0:
|
|||
|
ty_w = ymin + 10
|
|||
|
w_text = f"W:{int(round(show_w))}px"
|
|||
|
draw_m.text((tx_w, ty_w), w_text, fill="yellow", font=font)
|
|||
|
|
|||
|
# 高度文字:右边线中点偏右 10px
|
|||
|
tx_h = xmax + 10
|
|||
|
ty_h = int((ymin + ymax) / 2) - 20
|
|||
|
h_text = f"H:{int(round(show_h))}px"
|
|||
|
draw_m.text((tx_h, ty_h), h_text, fill="yellow", font=font)
|
|||
|
|
|||
|
vis_mask.save(output_mask_path)
|
|||
|
print(f"[Size] 掩模可视化已保存: {output_mask_path}")
|
|||
|
|
|||
|
# ---------- 同步绘制到原图 ----------
|
|||
|
if original_path and output_original_path:
|
|||
|
orig = Image.open(original_path).convert("RGB")
|
|||
|
draw_o = ImageDraw.Draw(orig)
|
|||
|
|
|||
|
draw_o.line([(xmin, ymin), (xmax, ymin)], fill="red", width=4)
|
|||
|
draw_o.line([(xmax, ymin), (xmax, ymax)], fill="red", width=4)
|
|||
|
draw_o.text((tx_w, ty_w), w_text, fill="yellow", font=font)
|
|||
|
draw_o.text((tx_h, ty_h), h_text, fill="yellow", font=font)
|
|||
|
|
|||
|
orig.save(output_original_path)
|
|||
|
print(f"[Size] 原图可视化已保存: {output_original_path}")
|
|||
|
|
|||
|
return w_px, h_px
|
|||
|
|
|||
|
|
|||
|
|
|||
|
# ----------------------------------------------------------------------
|
|||
|
# ------------------------------- 主程序 -------------------------------
|
|||
|
# ----------------------------------------------------------------------
|
|||
|
if __name__ == '__main__':
|
|||
|
# ======================= 路径配置 =======================
|
|||
|
triplets = [
|
|||
|
# (标签, 原图路径, 显著 / 掩模 路径)
|
|||
|
('front', './image/front_2.jpg', './saliency/front_2.jpg'), # 正面
|
|||
|
('rear', './image/rear_2.jpg', './saliency/rear_2.jpg'), # 后面
|
|||
|
('side', './image/side_2.jpg', './saliency/side_2.jpg'), # 侧面(做圆检测)
|
|||
|
]
|
|||
|
|
|||
|
out_dir = './result2'
|
|||
|
thresh_dir = './thresh2'
|
|||
|
os.makedirs(out_dir, exist_ok=True)
|
|||
|
os.makedirs(thresh_dir, exist_ok=True)
|
|||
|
|
|||
|
for tag, orig_path, mask_src in triplets:
|
|||
|
# # ======================= 生成显著性图 (可以注释掉,在u2net_saliency生成)=======================
|
|||
|
print(f"处理 {tag} 图像中...")
|
|||
|
generate_saliency_map(orig_path, mask_src)
|
|||
|
# # ==========================================================================================
|
|||
|
|
|||
|
# #======================= 阈值化处理 =======================
|
|||
|
print(f'\n===== 处理 {tag} =====')
|
|||
|
|
|||
|
# ---------- 1) 阈值化掩模 ----------
|
|||
|
gray = cv2.imread(mask_src, cv2.IMREAD_GRAYSCALE)
|
|||
|
if gray is None:
|
|||
|
raise FileNotFoundError(mask_src)
|
|||
|
|
|||
|
# Otsu 自动阈值 + 可选偏移(偏移范围建议0~20之间)
|
|||
|
offset = -10 # 负值让阈值变更敏感,保留更多区域
|
|||
|
otsu_val, _ = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
|
|||
|
final_thresh = max(0, min(255, otsu_val + offset))
|
|||
|
_, mask_bin = cv2.threshold(gray, final_thresh, 255, cv2.THRESH_BINARY)
|
|||
|
print(f'[Mask-{tag}] Otsu阈值={otsu_val:.1f}, 最终阈值={final_thresh}')
|
|||
|
|
|||
|
# 可选的轻度闭运算(平滑小孔,不破坏细节)
|
|||
|
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
|
|||
|
mask_bin = cv2.morphologyEx(mask_bin, cv2.MORPH_CLOSE, kernel, iterations=1)
|
|||
|
|
|||
|
# 保存阈值化结果
|
|||
|
mask_path = os.path.join(thresh_dir, f'{tag}_1_mask_thresh.png')
|
|||
|
cv2.imwrite(mask_path, mask_bin)
|
|||
|
print(f'[Mask-{tag}] 阈值化掩模已保存: {mask_path}')
|
|||
|
|
|||
|
# ---------- 2) 画长/宽线并写像素尺寸 ----------
|
|||
|
mask_vis_path = os.path.join(out_dir, f'{tag}_size_lines_mask.png')
|
|||
|
orig_vis_path = os.path.join(out_dir, f'{tag}_size_lines_orig.png')
|
|||
|
calculate_and_draw_bbox(
|
|||
|
mask_path, # 纯白掩模
|
|||
|
mask_vis_path, # 绘制后的掩模输出
|
|||
|
orig_path, # 原图
|
|||
|
orig_vis_path # 绘制后的原图输出
|
|||
|
)
|
|||
|
|
|||
|
# ---------- 3) 仅“side”做圆心检测 ----------
|
|||
|
if tag == 'side':
|
|||
|
detect_and_draw_circles(mask_src, orig_path, out_dir)
|