✅ 什么是 KMeans 聚类?为什么要用它?
通俗解释:
KMeans 聚类就像“自动分类器”,它根据像素的灰度值,把整张图分成亮度不同的几类区域。比如,把黑色背景、亮一点的重影、最亮的主影区分开。
为什么用它:
图像中的亮度差异很明显:
背景暗(低灰度)
重影比背景亮但比主影暗
主影最亮
KMeans 可以自动分组像素,不需要手动设阈值,适应性强,适用于批量图像处理。
✅ 什么是“形态学去噪”?用来干什么?
通俗解释:
形态学操作(闭运算 + 开运算)是图像“清洗”的方法。它像拿橡皮擦先“填补小缝”,再“擦除小点”。
闭运算(先膨胀后腐蚀):把小洞、小黑缝“补掉”,比如有些线段断裂的地方。
开运算(先腐蚀后膨胀):把孤立的小白点(噪声)清除掉,不影响主体。
为什么用它:
提取主影/重影区域时会有毛刺或孤立点,用这两步操作可以得到干净、光滑、连通的区域。
✅ 如何提取矩形?为什么这么做?
通俗解释:
图像中白色区域一块块的,就是我们要的主影或重影,提取过程如下:
findContours:找出每块白色区域的边界;
minAreaRect:给每块区域找一个最小的矩形框包住它(不一定是水平的);
过滤小面积:剔除掉太小的区域(可能是误识别或噪声);
保存矩形四个顶点和中心点,供后续识别/排序使用。
为什么这么做:
HUD 图像的线段都是“矩形状”,用这个方式提取,可以准确还原出每一条本体/重影线条的位置、大小和角度。
整体框架分析
图像预处理
读取图像:使用
cv2.imread()
加载图像;灰度化 + 高斯模糊降噪。目的:减少噪点干扰,提高 KMeans 聚类的稳定性和准确性。
KMeans 聚类分离区域
将图像像素聚为三类:背景、重影、主影。
使用灰度聚类,自动划分亮度区域,无需人工阈值。
np.argsort(centers)
:按亮度从低到高排序,最高为主影,中间为重影。
重影精细扣除
先对主影膨胀,再将其从重影中抠除,避免重叠误判。
形态学操作:闭操作填孔洞,开操作去小块,保留连续区域。
轮廓提取与筛选
cv2.findContours
找出边界,cv2.minAreaRect
提取最小包围矩形。通过面积过滤去除异常区域,确保提取的是完整主影/重影。
重影恢复
判断是否为竖直主影,如是,则计算重影偏移并“拼回”完整矩形。
避免因重影被主影遮挡而只识别半个的问题。
排序与主-重影匹配
主影按
(y, x)
排序。重影按“与主影最接近”策略匹配,一一对应,确保顺序一致。
日志输出与可视化
输出数量、坐标、偏移关系。
在图像上绘制每个主影和重影的轮廓和编号(M0/G0…),用于工程核对。
核心算法步骤流程图
mermaid
复制编辑
flowchart TD A[读取图像 & 灰度化 & 高斯模糊] --> B[KMeans聚类(3类)] B --> C[构建主影/重影掩码] C --> D[主影膨胀 & 精细扣除重影] D --> E[闭/开操作去噪] E --> F[findContours + 最小外接矩形提取] F --> G[竖直重影恢复] G --> H1[主影中心按(y,x)排序] H1 --> H2[重影与主影一一匹配] H2 --> I[输出偏移日志] I --> J[绘图标注编号]
import cv2 # OpenCV库,用于图像处理
import numpy as np # 数值计算库
import os # 文件系统操作
def detect_rectangles(image_path, log_dir='log', rows=5, cols=9,
main_ratio=0.5, ghost_ratio=0.5, visualize=True):
os.makedirs(log_dir, exist_ok=True) # 创建日志目录
# === 核心算法(不变) ===
img = cv2.imread(image_path) # 读取输入图像
if img is None:
# 若图像无法加载,抛出错误
raise FileNotFoundError(f"无法加载图片: {image_path}")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 转为灰度
gray_blur = cv2.GaussianBlur(gray, (3, 3), 0) # 小尺度高斯模糊,去除细小噪声
# 将图像像素重塑为 n×1 列向量,准备用于 K-means 聚类
pixels = gray_blur.reshape(-1, 1).astype(np.float32)
# K-means 终止条件:最大迭代次数 100 或 误差阈值 0.2
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 0.2)
# 3 类聚类:背景、重影、主影
_, labels, centers = cv2.kmeans(pixels, 3, None, criteria, 10, cv2.KMEANS_PP_CENTERS)
labels2d = labels.flatten().reshape(gray.shape) # 将聚类标签重构为图像形状
centers = centers.flatten() # 三个簇中心灰度
idx = np.argsort(centers) # 按灰度升序排列簇索引
ghost_idx, main_idx = idx[1], idx[2] # 中灰度为重影,最高灰度为主影
# 构建二值掩码:主影=255,其余=0;重影=255,其余=0
main_mask = (labels2d == main_idx).astype(np.uint8) * 255
ghost_mask = (labels2d == ghost_idx).astype(np.uint8) * 255
# 删除主影区域:先膨胀主影,再与重影取反交
main_dilate = cv2.dilate(
main_mask,
cv2.getStructuringElement(cv2.MORPH_RECT, (7, 3)), # 长条矩形核,重点在横向膨胀
iterations=2
)
ghost_only = cv2.bitwise_and(ghost_mask, cv2.bitwise_not(main_dilate))
# 噪声过滤:先闭运算填洞,再开运算去小斑点
kern = cv2.getStructuringElement(cv2.MORPH_RECT, (3,3))
ghost_only = cv2.morphologyEx(ghost_only, cv2.MORPH_CLOSE, kern, iterations=1)
ghost_only = cv2.morphologyEx(ghost_only, cv2.MORPH_OPEN, kern, iterations=1)
# 矩形提取函数:检测轮廓并最小外界矩形
def extract(mask, thresh):
rects, centers = [], []
cnts, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for c in cnts:
if cv2.contourArea(c) < thresh:
continue # 小面积过滤
r = cv2.minAreaRect(c) # 最小外界矩形
box = cv2.boxPoints(r).astype(int) # 获取4个顶点
if len(box)==4:
rects.append(box)
centers.append(tuple(r[0])) # 矩形中心坐标
return rects, centers
# 统计主影轮廓面积并取第 N 大作为基准
cnts_main, _ = cv2.findContours(main_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
areas = sorted([cv2.contourArea(c) for c in cnts_main], reverse=True)
N = rows*cols
base_area = areas[N-1] if len(areas)>=N else (areas[-1] if areas else 0)
# 按基准乘比率过滤:主影与重影
main_boxes, main_centers = extract(main_mask, base_area*main_ratio)
raw_gb, raw_gc = extract(ghost_only, base_area*ghost_ratio)
# 恢复竖直重影:若重影对应主影为竖直,则拼回完整矩形
recovered, vidx = [], []
for i,(gx,gy) in enumerate(raw_gc):
d = [np.hypot(gx-mx, gy-my) for mx,my in main_centers] # 匹配最近主影
mi = int(np.argmin(d))
xs, ys = zip(*main_boxes[mi])
if max(ys)-min(ys) > max(xs)-min(xs): # 判定为竖直
dx, dy = gx-np.mean(xs), gy-np.mean(ys)
rec = (main_boxes[mi] + np.array([dx,dy])).astype(int)
recovered.append(rec)
vidx.append(i)
# 最终重影:剔除原始竖直 + 加入恢复后
ghost_boxes = [b for i,b in enumerate(raw_gb) if i not in vidx] + recovered
ghost_centers = [c for i,c in enumerate(raw_gc) if i not in vidx] + \
[(float(np.mean(b[:,0])), float(np.mean(b[:,1]))) for b in recovered]
# 排序:先对主影按 (y, x) 排序,再让重影一一对应
def sort_by_coord(centers, boxes):
items = list(zip(centers, boxes))
items.sort(key=lambda x: (x[0][1], x[0][0])) # Y优先,再X
sc, sb = zip(*items) if items else ([], [])
return list(sc), list(sb)
main_centers, main_boxes = sort_by_coord(main_centers, main_boxes)
sorted_gc, sorted_gb = [], []
temp_c, temp_b = ghost_centers.copy(), ghost_boxes.copy()
for mc in main_centers:
d = [np.hypot(mc[0]-gc[0], mc[1]-gc[1]) for gc in temp_c]
idx_min = int(np.argmin(d)) if d else 0
sorted_gc.append(temp_c.pop(idx_min))
sorted_gb.append(temp_b.pop(idx_min))
ghost_centers, ghost_boxes = sorted_gc, sorted_gb
# 输出日志及可视化标注
offsets=[]
with open(os.path.join(log_dir,'results.txt'),'w') as f:
f.write(f"Main Count: {len(main_boxes)}\n")
for i,(x,y) in enumerate(main_centers): f.write(f"Main[{i}]: ({x:.1f},{y:.1f})\n")
f.write(f"Ghost Count: {len(ghost_boxes)}\n")
for i,(x,y) in enumerate(ghost_centers): f.write(f"Ghost[{i}]: ({x:.1f},{y:.1f})\n")
f.write("Offsets:\n")
for i,(gx,gy) in enumerate(ghost_centers):
d = [np.hypot(gx-mx, gy-my) for mx,my in main_centers]
mi = int(np.argmin(d)); dx,dy = gx-main_centers[mi][0], gy-main_centers[mi][1]
offsets.append((mi,i,dx,dy))
f.write(f"(Main[{mi}],Ghost[{i}]) dx={dx:.1f},dy={dy:.1f}\n")
if visualize:
res=img.copy()
for i,box in enumerate(main_boxes):
cv2.drawContours(res,[box],0,(0,0,255),2)
cx,cy=map(int,main_centers[i]); cv2.putText(res,f"M{i}",(cx+5,cy),cv2.FONT_HERSHEY_SIMPLEX,0.5,(0,0,255),1)
for i,box in enumerate(ghost_boxes):
cv2.drawContours(res,[box],0,(255,0,0),2)
cx,cy=map(int,ghost_centers[i]); cv2.putText(res,f"G{i}",(cx+5,cy),cv2.FONT_HERSHEY_SIMPLEX,0.5,(255,0,0),1)
cv2.imwrite(os.path.join(log_dir,'step7_final_result.png'),res)
return main_boxes, ghost_boxes, offsets, (res if visualize else None)
import cv2
import os
import argparse
from circle_detector import detect_circles
from visualizer import draw_circles, save_results
from fas import calculate_all_params
from rectangle_detector import detect_rectangles
from lines_detector_v5pro import detect_lines_v5r_debug
from detect_rectangles_pro import detect_rectangles_v2
# ===== 配置区域:修改图像路径 =====
IMAGE_PATH = "sample_data/cl_2.jpg" # 待检测的图像路径
PARAMS_FILE = "sample_data/input_params.txt"
RESULT_FILE = "log/hud_projection_results.txt"
def main():
"""主函数:控制程序执行流程"""
print("=" * 60)
print("黑底白圆检测器 | 霍夫变换算法(模块化版本)")
print("=" * 60)
# 检查图像文件是否存在
if not os.path.exists(IMAGE_PATH):
print(f"错误:文件不存在 - {IMAGE_PATH}")
return
if not os.path.exists(PARAMS_FILE):
print(f"错误:参数文件不存在 - {PARAMS_FILE}")
return
#####执行矩形检测
# 不可视化,只输出结果文件
# main_boxes, ghost_boxes, offsets, _ = detect_rectangles('sample_data/ghost.jpeg', 'log')
# main_boxes, ghost_boxes, offsets, _= detect_rectangles(
# 'sample_data/ghost.jpeg', 'log',rows=5,cols=9,visualize=True)
####lines检测
detect_rectangles_v2("sample_data/ghost.jpeg")
# 执行圆形检测
circles, proc_time, original_img = detect_circles(IMAGE_PATH)
if not circles:
print("检测失败,未找到圆形目标")
return
print(f"成功检测到 {len(circles)} 个圆,开始计算HUD投影参数...")
# 计算HUD投影参数
hud_params = calculate_all_params(circles, PARAMS_FILE)
if hud_params is None:
print("HUD参数计算失败")
return
# 绘制检测结果
result_img = draw_circles(original_img, circles)
# 保存结果图像和坐标信息
output_name = "result_hough_" + os.path.basename(IMAGE_PATH)
save_results(result_img, circles, output_name, proc_time)
# 保存HUD计算结果
save_hud_results(hud_params, RESULT_FILE)
# 输出检测报告
print("\n检测完成报告:")
print(f"处理时间: {proc_time:.2f} 秒")
print(f"检测到圆形数量: {len(circles)}")
print(f"结果图像保存路径: log/{output_name}")
print(f"坐标信息保存路径: log/hough_coordinates.txt")
print("\nHUD投影参数计算结果:")
print(f"下视角(LDA): {hud_params['lda']:.4f} 度")
print(f"左视角(LOA): {hud_params['loa']:.4f} 度")
print(f"横向视场角(FOVH): {hud_params['fovh']:.4f} 度")
print(f"纵向视场角(FOVV): {hud_params['fovv']:.4f} 度")
print(f"投影距离(VID): {hud_params['vid']:.4f} 毫米")
print(f"虚像长度: {hud_params['length']:.4f} 毫米")
print(f"虚像宽度: {hud_params['width']:.4f} 毫米")
print(f"特定点畸变(Distortion_geely): {hud_params['distortion_geely']:.6f}")
# print(f"所有点畸变(Distortion_CS): {hud_params['distortion_cs']:.6f}")
print(f"结果已保存至: {RESULT_FILE}")
def save_hud_results(params, file_path):
"""保存HUD计算结果到文件"""
try:
with open(file_path, 'w') as f:
f.write("===== HUD投影参数计算结果 =====\n\n")
f.write(f"下视角(LDA): {params['lda']:.6f} 度\n")
f.write(f"左视角(LOA): {params['loa']:.6f} 度\n")
f.write(f"横向视场角(FOVH): {params['fovh']:.6f} 度\n")
f.write(f"纵向视场角(FOVV): {params['fovv']:.6f} 度\n")
f.write(f"投影距离(VID): {params['vid']:.6f} 毫米\n")
f.write(f"虚像长度: {params['length']:.6f} 毫米\n")
f.write(f"虚像宽度: {params['width']:.6f} 毫米\n")
f.write("\n================================\n")
f.write(f"特定点畸变(Distortion_geely): {params['distortion_geely']:.6f}\n")
# f.write(f"所有点畸变(Distortion_CS): {params['distortion_cs']:.6f}\n")
print(f"HUD参数已保存至: {file_path}")
except Exception as e:
print(f"保存结果出错: {str(e)}")
if __name__ == "__main__":
main()
"""
lines_detector.py
新增校准逻辑:确保本体和重影一一配对,自动剔除额外检测区域。
"""
import cv2
import numpy as np
import os
import argparse
import math
def detect_rectangles(image_path, log_dir='log', rows=5, cols=9, visualize=True):
os.makedirs(log_dir, exist_ok=True)
# 1. 读取 & 预处理
img = cv2.imread(image_path)
if img is None:
raise FileNotFoundError(f"无法加载图片: {image_path}")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
gray_blur = cv2.GaussianBlur(gray, (5, 5), 0)
cv2.imwrite(os.path.join(log_dir, 'step1_gray_blur.png'), gray_blur)
# 2. K-means 分割为 3 个灰度层级
pixels = gray_blur.reshape(-1, 1).astype(np.float32)
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 0.2)
_, labels, centers = cv2.kmeans(pixels, 3, None, criteria, 10, cv2.KMEANS_PP_CENTERS)
labels2d = labels.flatten().reshape(gray_blur.shape)
centers = centers.flatten()
sorted_idx = np.argsort(centers)[::-1] # 高到低
overlap_idx, main_idx, ghost_idx = sorted_idx
overlap_mask = (labels2d == overlap_idx).astype(np.uint8) * 255
main_mask = (labels2d == main_idx).astype(np.uint8) * 255
ghost_mask = (labels2d == ghost_idx).astype(np.uint8) * 255
cv2.imwrite(os.path.join(log_dir, 'step2_overlap_mask.png'), overlap_mask)
cv2.imwrite(os.path.join(log_dir, 'step3_main_mask.png'), main_mask)
cv2.imwrite(os.path.join(log_dir, 'step4_ghost_mask.png'), ghost_mask)
# 3. 合并重叠区
main_full = cv2.bitwise_or(main_mask, overlap_mask)
ghost_full = cv2.bitwise_or(ghost_mask, overlap_mask)
cv2.imwrite(os.path.join(log_dir, 'step5_main_full.png'), main_full)
cv2.imwrite(os.path.join(log_dir, 'step6_ghost_full.png'), ghost_full)
# 4. 噪声过滤
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
def clean(mask):
m = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=2)
m = cv2.morphologyEx(m, cv2.MORPH_OPEN, kernel, iterations=1)
return m
main_clean = clean(main_full)
ghost_clean = clean(ghost_full)
cv2.imwrite(os.path.join(log_dir, 'step7_main_clean.png'), main_clean)
cv2.imwrite(os.path.join(log_dir, 'step8_ghost_clean.png'), ghost_clean)
# 5. 提取矩形及中心
def extract(mask):
boxes, centers = [], []
cnts, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for c in cnts:
if cv2.contourArea(c) < 10:
continue
r = cv2.minAreaRect(c)
box = cv2.boxPoints(r).astype(int)
boxes.append(box)
centers.append(r[0])
return boxes, centers
main_boxes, main_centers = extract(main_clean)
ghost_boxes, ghost_centers = extract(ghost_clean)
# 6. 校准配对:贪心算法,保证一一对应
# 构建距离列表
candidates = []
for i, m in enumerate(main_centers):
for j, g in enumerate(ghost_centers):
d = math.hypot(g[0] - m[0], g[1] - m[1])
candidates.append((d, i, j))
candidates.sort(key=lambda x: x[0])
used_m, used_g = set(), set()
pairs = [] # 存最终索引对 (i, j)
for d, i, j in candidates:
if i in used_m or j in used_g:
continue
used_m.add(i);
used_g.add(j)
pairs.append((i, j))
# 当任何一方用完即跳出
if len(used_m) == len(main_centers) or len(used_g) == len(ghost_centers):
break
# 裁剪多余检测,保证成对
main_boxes_calib, main_centers_calib = [], []
ghost_boxes_calib, ghost_centers_calib = [], []
for i, j in pairs:
main_boxes_calib.append(main_boxes[i])
main_centers_calib.append(main_centers[i])
ghost_boxes_calib.append(ghost_boxes[j])
ghost_centers_calib.append(ghost_centers[j])
main_boxes, main_centers = main_boxes_calib, main_centers_calib
ghost_boxes, ghost_centers = ghost_boxes_calib, ghost_centers_calib
# 7. 计算偏移并输出日志
with open(os.path.join(log_dir, 'results.txt'), 'w') as f:
f.write(f"Main Count: {len(main_boxes)}\n")
f.write(f"Ghost Count: {len(ghost_boxes)}\n")
for idx, (i, j) in enumerate(pairs):
dx = ghost_centers[j][0] - main_centers[i][0]
dy = ghost_centers[j][1] - main_centers[i][1]
f.write(f"Pair Main[{i}] - Ghost[{j}]: dx={dx:.2f}, dy={dy:.2f}\n")
print(f"结果已写入 {log_dir}/results.txt,已保证一一配对。")
# 8. 可视化
vis = img.copy()
for box, cen in zip(main_boxes, main_centers):
cv2.drawContours(vis, [box], 0, (0, 0, 255), 2)
cv2.circle(vis, (int(cen[0]), int(cen[1])), 3, (0, 0, 255), -1)
for box, cen in zip(ghost_boxes, ghost_centers):
cv2.drawContours(vis, [box], 0, (255, 0, 0), 2)
cv2.circle(vis, (int(cen[0]), int(cen[1])), 3, (255, 0, 0), -1)
cv2.imwrite(os.path.join(log_dir, 'step9_final_result.png'), vis)
return main_boxes, ghost_boxes, pairs, vis
# #已经把竖着的重影恢复出来了,但是还不对,然后重影也不够精确
# import cv2
# import numpy as np
# import os
#
#
# def detect_rectangles(image_path, log_dir='log', rows=5, cols=9,
# main_ratio=0.5, ghost_ratio=0.08, visualize=True):
# os.makedirs(log_dir, exist_ok=True)
#
# # 1. 读取 & 预处理
# img = cv2.imread(image_path)
# if img is None:
# raise FileNotFoundError(f"无法加载图片: {image_path}")
# gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# gray_blur = cv2.GaussianBlur(gray, (5, 5), 0)
# cv2.imwrite(os.path.join(log_dir, 'step1_gray_blur.png'), gray_blur)
#
# # 2. K-means 分割主影 / 重影 / 背景
# pixels = gray_blur.reshape(-1, 1).astype(np.float32)
# criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 0.2)
# _, labels, centers = cv2.kmeans(pixels, 3, None, criteria, 10, cv2.KMEANS_PP_CENTERS)
# labels2d = labels.flatten().reshape(gray.shape)
# centers = centers.flatten()
# idx = np.argsort(centers)
# ghost_idx, main_idx = idx[1], idx[2]
#
# main_mask = (labels2d == main_idx).astype(np.uint8) * 255
# ghost_mask = (labels2d == ghost_idx).astype(np.uint8) * 255
# cv2.imwrite(os.path.join(log_dir, 'step2_main_mask.png'), main_mask)
# cv2.imwrite(os.path.join(log_dir, 'step3_ghost_mask.png'), ghost_mask)
#
# # 3. 精细扣除:在最小膨胀范围内删除主影区域
# main_dilate = cv2.dilate(main_mask, cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)), iterations=1)
# ghost_only = cv2.bitwise_and(ghost_mask, cv2.bitwise_not(main_dilate))
# cv2.imwrite(os.path.join(log_dir, 'step4_ghost_only_raw.png'), ghost_only)
#
# # 4. 噪声过滤:闭运算 + 开运算 去除小斑点
# kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
# ghost_only = cv2.morphologyEx(ghost_only, cv2.MORPH_CLOSE, kernel, iterations=1)
# ghost_only = cv2.morphologyEx(ghost_only, cv2.MORPH_OPEN, kernel, iterations=1)
# cv2.imwrite(os.path.join(log_dir, 'step5_ghost_filtered.png'), ghost_only)
#
# # 5. 根据面积阈值提取主影 & 重影矩形
# def extract(mask, thresh):
# rects, centers = [], []
# cnts, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# for c in cnts:
# if cv2.contourArea(c) < thresh:
# continue
# r = cv2.minAreaRect(c)
# box = cv2.boxPoints(r).astype(int)
# if box.shape[0] == 4:
# rects.append(box)
# centers.append(r[0])
# return rects, centers
#
# cnts_main, _ = cv2.findContours(main_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# areas = sorted([cv2.contourArea(c) for c in cnts_main], reverse=True)
# N = rows * cols
# base_area = areas[N - 1] if len(areas) >= N else (areas[-1] if areas else 0)
# main_thresh = base_area * main_ratio
# ghost_thresh = base_area * ghost_ratio
#
# main_boxes, main_centers = extract(main_mask, main_thresh)
# ghost_boxes, ghost_centers = extract(ghost_only, ghost_thresh)
#
# # 6. 恢复竖直重影:将被本体遮挡的那半截“拼回来”
# recovered = []
# rec_img = img.copy()
# for mi, (gx, gy) in enumerate(ghost_centers):
# # 找对应主影
# dists = [np.hypot(gx - mx, gy - my) for mx, my in main_centers]
# mi_best = int(np.argmin(dists))
# box = main_boxes[mi_best]
# xs, ys = zip(*box)
# w, h = max(xs) - min(xs), max(ys) - min(ys)
# # 仅对竖直主影做恢复
# if h > w:
# # 计算重影中心相对主影中心偏移
# dx = gx - np.mean(xs)
# dy = gy - np.mean(ys)
# # 拼回完整矩形
# rec_box = (box + np.array([dx, dy])).astype(int)
# recovered.append(rec_box)
# cv2.drawContours(rec_img, [rec_box], 0, (0, 255, 0), 2)
# cv2.imwrite(os.path.join(log_dir, 'step6_recovered_vertical.png'), rec_img)
#
# ghost_boxes.extend(recovered)
#
# # 7. 计算偏移 & 输出日志
# offsets = []
# for gi, (gx, gy) in enumerate([np.mean(b, axis=0) for b in ghost_boxes]):
# dists = [np.hypot(gx - mx, gy - my) for mx, my in main_centers]
# mi = int(np.argmin(dists))
# dx, dy = gx - main_centers[mi][0], gy - main_centers[mi][1]
# offsets.append((mi, gi, dx, dy))
#
# with open(os.path.join(log_dir, 'results.txt'), 'w') as f:
# f.write(f"Main Count: {len(main_boxes)}\n")
# f.write(f"Ghost Count: {len(ghost_boxes)}\n")
# for off in offsets:
# f.write(f"{off}\n")
#
# # 8. 最终可视化
# if visualize:
# res = img.copy()
# for b in main_boxes: cv2.drawContours(res, [b], 0, (0, 0, 255), 2)
# for b in ghost_boxes: cv2.drawContours(res, [b], 0, (255, 0, 0), 2)
# cv2.imwrite(os.path.join(log_dir, 'step7_final_result.png'), res)
#
# return main_boxes, ghost_boxes, offsets, (res if visualize else None)
######################这个已经比较完美了,但是还要改进
# def detect_rectangles(image_path, log_dir='log', rows=5, cols=9,
# main_ratio=0.5, ghost_ratio=0.08, visualize=True):
# import cv2, numpy as np, os
#
# os.makedirs(log_dir, exist_ok=True)
#
# img = cv2.imread(image_path)
# gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# gray_blur = cv2.GaussianBlur(gray, (5, 5), 0)
#
# # Step 1: K-means分割
# pixels = gray_blur.reshape(-1, 1).astype(np.float32)
# criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 0.2)
# _, labels, centers = cv2.kmeans(pixels, 3, None, criteria, 10, cv2.KMEANS_PP_CENTERS)
# labels2d = labels.flatten().reshape(gray.shape)
# centers = centers.flatten()
# idx = np.argsort(centers)
# ghost_idx, main_idx = idx[1], idx[2]
#
# main_mask = (labels2d == main_idx).astype(np.uint8) * 255
# ghost_mask = (labels2d == ghost_idx).astype(np.uint8) * 255
#
# # 保存第2、3步图
# cv2.imwrite(os.path.join(log_dir, 'main_mask.png'), main_mask)
# cv2.imwrite(os.path.join(log_dir, 'ghost_mask.png'), ghost_mask)
#
# # Step 2:(改进)直接在二值mask基础上精细扣除
# main_dilate = cv2.dilate(main_mask, np.ones((3, 3)), iterations=1) # 小范围膨胀
# ghost_only = cv2.bitwise_and(ghost_mask, cv2.bitwise_not(main_dilate))
# ghost_only = cv2.morphologyEx(ghost_only, cv2.MORPH_OPEN, np.ones((2, 2)), iterations=1)
#
# cv2.imwrite(os.path.join(log_dir, 'ghost_only.png'), ghost_only)
#
# def extract(mask, thresh):
# rects, centers = [], []
# cnts, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# for c in cnts:
# if cv2.contourArea(c) < thresh: continue
# r = cv2.minAreaRect(c)
# box = cv2.boxPoints(r).astype(int)
# if box.shape[0] == 4:
# rects.append(box)
# centers.append(r[0])
# return rects, centers
#
# # Step 3: 提取框
# cnts_main, _ = cv2.findContours(main_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# areas = sorted([cv2.contourArea(c) for c in cnts_main], reverse=True)
# N = rows * cols
# base_area = areas[N - 1] if len(areas) >= N else (areas[-1] if areas else 0)
# main_thresh = base_area * main_ratio
# ghost_thresh = base_area * ghost_ratio
#
# main_boxes, main_centers = extract(main_mask, main_thresh)
# ghost_boxes, ghost_centers = extract(ghost_only, ghost_thresh)
#
# offsets = []
# for gi, g in enumerate(ghost_boxes):
# gc = np.mean(g, axis=0)
# dists = [np.linalg.norm(gc - np.mean(m, axis=0)) for m in main_boxes]
# mi = int(np.argmin(dists))
# dx, dy = gc - np.mean(main_boxes[mi], axis=0)
# offsets.append((mi, gi, dx, dy))
#
# with open(os.path.join(log_dir, 'results.txt'), 'w') as f:
# f.write(f"Main Count:{len(main_boxes)}\n")
# f.write(f"Ghost Count:{len(ghost_boxes)}\n")
# for o in offsets: f.write(f"{o}\n")
#
# if visualize:
# result_img = img.copy()
# for b in main_boxes: cv2.drawContours(result_img, [b], 0, (0, 0, 255), 2)
# for b in ghost_boxes: cv2.drawContours(result_img, [b], 0, (255, 0, 0), 2)
# cv2.imwrite(os.path.join(log_dir, 'final_result.png'), result_img)
#
# return main_boxes, ghost_boxes, offsets, result_img
###########################这个第三步生成的图已经很好了,其实已经把重影都识别出来了
# def detect_rectangles(image_path, log_dir='log', rows=5, cols=9,
# main_ratio=0.5, ghost_ratio=0.08, visualize=True):
# import cv2, numpy as np, os
#
# os.makedirs(log_dir, exist_ok=True)
#
# img = cv2.imread(image_path)
# gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# gray_blur = cv2.GaussianBlur(gray, (5, 5), 0)
# cv2.imwrite(os.path.join(log_dir, 'step1_gray_blur.png'), gray_blur)
#
# # Step 1: K-means分割
# pixels = gray_blur.reshape(-1, 1).astype(np.float32)
# criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 0.2)
# _, labels, centers = cv2.kmeans(pixels, 3, None, criteria, 10, cv2.KMEANS_PP_CENTERS)
# labels2d = labels.flatten().reshape(gray.shape)
# centers = centers.flatten()
# idx = np.argsort(centers)
# ghost_idx, main_idx = idx[1], idx[2]
#
# main_mask = (labels2d == main_idx).astype(np.uint8) * 255
# ghost_mask = (labels2d == ghost_idx).astype(np.uint8) * 255
# cv2.imwrite(os.path.join(log_dir, 'step2_main_mask.png'), main_mask)
# cv2.imwrite(os.path.join(log_dir, 'step3_ghost_mask.png'), ghost_mask)
#
# # Step 2: 形态学闭运算
# h_k = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 3))
# v_k = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 15))
# main_clean = cv2.bitwise_or(
# cv2.morphologyEx(main_mask, cv2.MORPH_CLOSE, h_k, iterations=2),
# cv2.morphologyEx(main_mask, cv2.MORPH_CLOSE, v_k, iterations=2)
# )
# ghost_closed = cv2.bitwise_or(
# cv2.morphologyEx(ghost_mask, cv2.MORPH_CLOSE, h_k, iterations=2),
# cv2.morphologyEx(ghost_mask, cv2.MORPH_CLOSE, v_k, iterations=2)
# )
# cv2.imwrite(os.path.join(log_dir, 'step4_main_clean.png'), main_clean)
# cv2.imwrite(os.path.join(log_dir, 'step5_ghost_closed.png'), ghost_closed)
#
# # Step 3: 扣除主影区域
# main_dilate = cv2.dilate(main_clean, np.ones((13, 13)), iterations=1)
# ghost_only = cv2.bitwise_and(ghost_closed, cv2.bitwise_not(main_dilate))
# ghost_only = cv2.morphologyEx(ghost_only, cv2.MORPH_OPEN, np.ones((3, 3)), iterations=1)
# cv2.imwrite(os.path.join(log_dir, 'step6_ghost_only.png'), ghost_only)
#
# def extract(mask, thresh):
# rects, centers = [], []
# cnts, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# for c in cnts:
# if cv2.contourArea(c) < thresh: continue
# r = cv2.minAreaRect(c)
# box = cv2.boxPoints(r).astype(int)
# if box.shape[0] == 4:
# rects.append(box)
# centers.append(r[0])
# return rects, centers
#
# cnts_main, _ = cv2.findContours(main_clean, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# areas = sorted([cv2.contourArea(c) for c in cnts_main], reverse=True)
# N = rows * cols
# base_area = areas[N - 1] if len(areas) >= N else (areas[-1] if areas else 0)
# main_thresh = base_area * main_ratio
# ghost_thresh = base_area * ghost_ratio
#
# main_boxes, main_centers = extract(main_clean, main_thresh)
# ghost_boxes, ghost_centers = extract(ghost_only, ghost_thresh)
#
# # Step 4: 竖直重影优化(扩展区域搜索)
# refined_ghost_boxes = []
# ghost_refine_img = img.copy()
# for box in main_boxes:
# x_min, x_max = min(box[:, 0]) - 15, max(box[:, 0]) + 15
# y_min, y_max = min(box[:, 1]) - 30, max(box[:, 1]) + 30
# x_min, x_max = max(0, x_min), min(gray.shape[1], x_max)
# y_min, y_max = max(0, y_min), min(gray.shape[0], y_max)
#
# roi = gray[y_min:y_max, x_min:x_max]
# main_roi_mask = np.zeros_like(roi)
# box_shifted = box - [x_min, y_min]
# cv2.drawContours(main_roi_mask, [box_shifted], -1, 255, -1)
# main_val = np.mean(roi[main_roi_mask == 255])
# ghost_thr = main_val - 20 if main_val > 20 else 10
# ghost_roi = ((roi > ghost_thr) & (roi < main_val - 5)).astype(np.uint8) * 255
# ghost_roi = cv2.morphologyEx(ghost_roi, cv2.MORPH_OPEN, np.ones((3, 3)), iterations=1)
# ghost_roi = cv2.subtract(ghost_roi, main_roi_mask)
#
# cnts, _ = cv2.findContours(ghost_roi, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# for c in cnts:
# if cv2.contourArea(c) < 5: continue
# r = cv2.minAreaRect(c)
# gb = cv2.boxPoints(r).astype(int) + [x_min, y_min]
# refined_ghost_boxes.append(gb)
# cv2.drawContours(ghost_refine_img, [gb], -1, (255, 0, 0), 2)
# cv2.rectangle(ghost_refine_img, (x_min, y_min), (x_max, y_max), (0, 255, 0), 1)
#
# cv2.imwrite(os.path.join(log_dir, 'step7_ghost_refine.png'), ghost_refine_img)
#
# ghost_boxes.extend(refined_ghost_boxes)
#
# offsets = []
# for gi, g in enumerate(ghost_boxes):
# gc = np.mean(g, axis=0)
# dists = [np.linalg.norm(gc - np.mean(m, axis=0)) for m in main_boxes]
# mi = int(np.argmin(dists))
# dx, dy = gc - np.mean(main_boxes[mi], axis=0)
# offsets.append((mi, gi, dx, dy))
#
# with open(os.path.join(log_dir, 'results.txt'), 'w') as f:
# f.write(f"Main Count:{len(main_boxes)}\n")
# f.write(f"Ghost Count:{len(ghost_boxes)}\n")
# for o in offsets: f.write(f"{o}\n")
#
# if visualize:
# result_img = img.copy()
# for b in main_boxes: cv2.drawContours(result_img, [b], 0, (0, 0, 255), 2)
# for b in ghost_boxes: cv2.drawContours(result_img, [b], 0, (255, 0, 0), 2)
# cv2.imwrite(os.path.join(log_dir, 'step8_final_result.png'), result_img)
#
# return main_boxes, ghost_boxes, offsets, result_img
# ###################终于分割开了!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!但是精度不够
# def detect_rectangles(image_path, log_dir='log', rows=5, cols=9,
# main_ratio=0.5, ghost_ratio=0.2, visualize=False):
# import cv2
# import numpy as np
# import os
#
# img = cv2.imread(image_path)
# if img is None:
# raise FileNotFoundError(f"无法加载图片: {image_path}")
# gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# gray = cv2.GaussianBlur(gray, (5, 5), 0)
#
# # 主影mask
# pixels = gray.reshape(-1, 1).astype(np.float32)
# criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 0.2)
# _, labels, centers = cv2.kmeans(pixels, 3, None, criteria, 10, cv2.KMEANS_PP_CENTERS)
# centers = centers.flatten()
# idx = np.argsort(centers)
# ghost_idx, main_idx = idx[1], idx[2]
# labels2d = labels.flatten().reshape(gray.shape)
# main_mask = (labels2d == main_idx).astype(np.uint8) * 255
# ghost_mask = (labels2d == ghost_idx).astype(np.uint8) * 255
#
# # 形态学闭运算
# h_k = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 3))
# v_k = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 15))
# main_clean = cv2.bitwise_or(
# cv2.morphologyEx(main_mask, cv2.MORPH_CLOSE, h_k, iterations=2),
# cv2.morphologyEx(main_mask, cv2.MORPH_CLOSE, v_k, iterations=2)
# )
# ghost_closed = cv2.bitwise_or(
# cv2.morphologyEx(ghost_mask, cv2.MORPH_CLOSE, h_k, iterations=2),
# cv2.morphologyEx(ghost_mask, cv2.MORPH_CLOSE, v_k, iterations=2)
# )
#
# # ------------ 彻底分离重影掩膜核心 ------------
# # 主影膨胀多一点(强行扩展主影掩膜覆盖)
# main_dilate = cv2.dilate(main_clean, cv2.getStructuringElement(cv2.MORPH_RECT, (13, 13)), iterations=1)
# # 从重影掩膜中扣除所有主影膨胀区,只剩“绝对不会有本体的重影”
# ghost_only = cv2.bitwise_and(ghost_closed, cv2.bitwise_not(main_dilate))
# # 去杂点
# ghost_only = cv2.morphologyEx(ghost_only, cv2.MORPH_OPEN, cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)),
# iterations=1)
#
# # 后续照旧
# cnts_main, _ = cv2.findContours(main_clean, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# areas = sorted([cv2.contourArea(c) for c in cnts_main], reverse=True)
# N = rows * cols
# base_area = areas[N - 1] if len(areas) >= N else (areas[-1] if areas else 0)
# main_thresh = base_area * main_ratio
# ghost_thresh = base_area * ghost_ratio
#
# def extract(mask, thresh):
# rects, centers = [], []
# cnts, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# for c in cnts:
# if cv2.contourArea(c) < thresh:
# continue
# r = cv2.minAreaRect(c)
# box = cv2.boxPoints(r).astype(int)
# if box.shape[0] == 4:
# rects.append(box)
# centers.append(r[0])
# return rects, centers
#
# main_boxes, main_centers = extract(main_clean, main_thresh)
# ghost_boxes, ghost_centers = extract(ghost_only, ghost_thresh)
#
# offsets = []
# for gi, g in enumerate(ghost_centers):
# if not main_centers:
# break
# dists = [np.hypot(g[0] - m[0], g[1] - m[1]) for m in main_centers]
# mi = int(np.argmin(dists))
# dx, dy = g[0] - main_centers[mi][0], g[1] - main_centers[mi][1]
# offsets.append((mi, gi, dx, dy))
#
# os.makedirs(log_dir, exist_ok=True)
# with open(os.path.join(log_dir, 'results.txt'), 'w') as f:
# f.write(f"Main Count: {len(main_boxes)}\n")
# f.write(f"Ghost Count: {len(ghost_boxes)}\n")
# f.write("Offsets(main_i,ghost_i,dx,dy):\n")
# for it in offsets:
# f.write(f"{it}\n")
#
# result_img = None
# if visualize:
# result_img = img.copy()
# for b in main_boxes:
# cv2.drawContours(result_img, [b], 0, (0, 0, 255), 2)
# for b in ghost_boxes:
# cv2.drawContours(result_img, [b], 0, (255, 0, 0), 2)
# cv2.imwrite(os.path.join(log_dir, 'result.png'), result_img)
#
# return main_boxes, ghost_boxes, offsets, result_img
######################这个全部识别到了,但是框的不太对
# import cv2
# import numpy as np
# import os
#
#
# def detect_rectangles(image_path, log_dir='log', rows=5, cols=9,
# main_ratio=0.5, ghost_ratio=0.8, visualize=False):
# """
# 检测 HUD 投影图中的主影与重影矩形,并计算位移偏移。
# 参数:
# image_path: 图像路径
# log_dir: 输出目录
# rows, cols: HUD 主影排布行列数,用于动态估算基准面积
# main_ratio: 主影过滤阈值占基准面积比例
# ghost_ratio: 重影过滤阈值占基准面积比例(相对于基准面积)
# visualize: 是否在返回时附带标注图像
# 返回:
# main_boxes: 主影矩形列表 (Nx4x2 int numpy array)
# ghost_boxes: 重影矩形列表
# offsets: 重影相对于主影的偏移列表 [(idx_main, idx_ghost, dx, dy), ...]
# result_img (可选): 标注后的 BGR 图像,当 visualize=True
# """
# # 1. 读取与预处理
# img = cv2.imread(image_path)
# if img is None:
# raise FileNotFoundError(f"无法加载图片: {image_path}")
# gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# gray = cv2.GaussianBlur(gray, (5, 5), 0)
#
# # 2. K-means 3 类聚类: 背景、重影、主影
# pixels = gray.reshape(-1, 1).astype(np.float32)
# criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 0.2)
# _, labels, centers = cv2.kmeans(pixels, 3, None, criteria, 10, cv2.KMEANS_PP_CENTERS)
# centers = centers.flatten()
# idx = np.argsort(centers)
# ghost_idx, main_idx = idx[1], idx[2]
#
# labels2d = labels.flatten().reshape(gray.shape)
# main_mask = (labels2d == main_idx).astype(np.uint8) * 255
# ghost_mask = (labels2d == ghost_idx).astype(np.uint8) * 255
#
# # 3. 定向形态学闭运算
# h_k = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 3))
# v_k = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 15))
# main_clean = cv2.bitwise_or(
# cv2.morphologyEx(main_mask, cv2.MORPH_CLOSE, h_k, iterations=2),
# cv2.morphologyEx(main_mask, cv2.MORPH_CLOSE, v_k, iterations=2)
# )
# ghost_clean = cv2.bitwise_or(
# cv2.morphologyEx(ghost_mask, cv2.MORPH_CLOSE, h_k, iterations=2),
# cv2.morphologyEx(ghost_mask, cv2.MORPH_CLOSE, v_k, iterations=2)
# )
#
# # 4. 留纯重影
# ghost_clean = cv2.subtract(ghost_clean, main_clean)
#
# # 5. 动态面积阈值
# cnts_main, _ = cv2.findContours(main_clean, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# areas = sorted([cv2.contourArea(c) for c in cnts_main], reverse=True)
# N = rows * cols
# base_area = areas[N - 1] if len(areas) >= N else (areas[-1] if areas else 0)
# main_thresh = base_area * main_ratio
# ghost_thresh = base_area * ghost_ratio
#
# # 6. 提取矩形与质心
# def extract(mask, thresh):
# rects, centers = [], []
# cnts, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# for c in cnts:
# if cv2.contourArea(c) < thresh:
# continue
# r = cv2.minAreaRect(c)
# box = cv2.boxPoints(r).astype(int)
# if box.shape[0] == 4:
# rects.append(box)
# centers.append(r[0])
# return rects, centers
#
# main_boxes, main_centers = extract(main_clean, main_thresh)
# ghost_boxes, ghost_centers = extract(ghost_clean, ghost_thresh)
#
# # 7. 计算偏移
# offsets = []
# for gi, g in enumerate(ghost_centers):
# if not main_centers:
# break
# dists = [np.hypot(g[0] - m[0], g[1] - m[1]) for m in main_centers]
# mi = int(np.argmin(dists))
# dx = g[0] - main_centers[mi][0]
# dy = g[1] - main_centers[mi][1]
# offsets.append((mi, gi, dx, dy))
#
# # 8. 保存结果
# os.makedirs(log_dir, exist_ok=True)
# with open(os.path.join(log_dir, 'results.txt'), 'w') as f:
# f.write(f"Main Count: {len(main_boxes)}\n")
# f.write(f"Ghost Count: {len(ghost_boxes)}\n")
# f.write("Offsets(main_i,ghost_i,dx,dy):\n")
# for it in offsets:
# f.write(f"{it}\n")
#
# result_img = None
# if visualize:
# result_img = img.copy()
# for b in main_boxes:
# cv2.drawContours(result_img, [b], 0, (0, 0, 255), 2)
# for b in ghost_boxes:
# cv2.drawContours(result_img, [b], 0, (255, 0, 0), 2)
# cv2.imwrite(os.path.join(log_dir, 'result.png'), result_img)
#
# return main_boxes, ghost_boxes, offsets, result_img
#######################这个识别到的有点少
# import cv2
# import numpy as np
# import os
#
#
# def detect_rectangles(image_path, log_dir='log'):
# # Load image and convert to grayscale
# img = cv2.imread(image_path)
# if img is None:
# print(f"Error: could not load image {image_path}")
# return
# gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
#
# # Flatten and prepare for k-means clustering (3 clusters: background, ghost, main)
# pixel_vals = gray.reshape((-1, 1)).astype(np.float32)
# criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 0.2)
# k = 3
# _, labels, centers = cv2.kmeans(pixel_vals, k, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS)
# centers = centers.flatten()
#
# # Determine cluster labels: sort centers
# sorted_centers_idx = np.argsort(centers)
# background_label = sorted_centers_idx[0]
# ghost_label = sorted_centers_idx[1]
# main_label = sorted_centers_idx[2]
#
# # Create binary masks for main and ghost segments
# labels = labels.flatten().reshape(gray.shape)
# main_mask = np.uint8(labels == main_label) * 255
# ghost_mask = np.uint8(labels == ghost_label) * 255
#
# # Morphological operations to clean noise
# kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
# main_mask = cv2.morphologyEx(main_mask, cv2.MORPH_OPEN, kernel, iterations=2)
# ghost_mask = cv2.morphologyEx(ghost_mask, cv2.MORPH_OPEN, kernel, iterations=2)
#
# # Find contours for each mask
# contours_main, _ = cv2.findContours(main_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# contours_ghost, _ = cv2.findContours(ghost_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
#
# # Filter contours by area to remove small noise
# min_area = 50
# main_rects = [(x, y, w, h) for cnt in contours_main
# for x, y, w, h in [cv2.boundingRect(cnt)]
# if cv2.contourArea(cnt) > min_area]
# ghost_rects = [(x, y, w, h) for cnt in contours_ghost
# for x, y, w, h in [cv2.boundingRect(cnt)]
# if cv2.contourArea(cnt) > min_area]
#
# # Ensure log directory exists
# os.makedirs(log_dir, exist_ok=True)
#
# # Write results to text file
# result_txt = os.path.join(log_dir, 'results.txt')
# with open(result_txt, 'w') as f:
# f.write(f"Main rectangles: {len(main_rects)}\n")
# f.write(f"Ghost rectangles: {len(ghost_rects)}\n")
#
# # Draw rectangles on original image
# output_img = img.copy()
# for (x, y, w, h) in main_rects:
# cv2.rectangle(output_img, (x, y), (x + w, y + h), (0, 0, 255), 2) # red for main
# for (x, y, w, h) in ghost_rects:
# cv2.rectangle(output_img, (x, y), (x + w, y + h), (255, 0, 0), 2) # blue for ghost
#
# # Save annotated image
# result_img = os.path.join(log_dir, 'result.png')
# cv2.imwrite(result_img, output_img)
#
# # Print counts
# print(f"Main rectangles: {len(main_rects)}")
# print(f"Ghost rectangles: {len(ghost_rects)}")
#
#
#
import cv2
import numpy as np
import time
# 霍夫变换检测参数
CIRCLE_DP = 1.0 #控制霍夫空间(累加器)的分辨率,dp=1.0 表示累加器与输入图像分辨率相同,dp=2.0 表示累加器分辨率为图像的一半(速度更快,但精度降低)
CIRCLE_MIN_DIST = 30 #圆之间的最小距离
CIRCLE_PARAM1 = 50 #Canny边缘检测的高阈值,若边缘缺失(圆不完整),降低 param1(如 30),若噪声过多(误检多),增大 param1(如 80)
CIRCLE_PARAM2 = 15 #控制累加器阈值,决定圆被检测到的置信度,若漏检严重(圆未检出),减小 param2,若误检多(假圆),增大 param2
CIRCLE_MIN_R = 5 #圆的半径范围
CIRCLE_MAX_R = 40
def is_circle_duplicate(circle_list, center, radius, factor=0.7):
"""检查圆是否重叠"""
for circle in circle_list:
dist = np.sqrt((circle['center'][0] - center[0]) ** 2 +
(circle['center'][1] - center[1]) ** 2)
min_dist = factor * (radius + circle['radius'])
if dist < min_dist:
return True
return False
def detect_circles(image_path):
"""执行检测并返回排序后的圆"""
start_time = time.time()
img = cv2.imread(image_path)
if img is None:
print(f"错误:无法读取图像 - {image_path}")
return [], 0, None
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
cleaned_img = cv2.morphologyEx(gray, cv2.MORPH_OPEN, kernel)
blurred = cv2.GaussianBlur(cleaned_img, (9, 9), 2)
# 霍夫圆检测
circles = cv2.HoughCircles(
blurred, cv2.HOUGH_GRADIENT, dp=CIRCLE_DP, minDist=CIRCLE_MIN_DIST,
param1=CIRCLE_PARAM1, param2=CIRCLE_PARAM2,
minRadius=CIRCLE_MIN_R, maxRadius=CIRCLE_MAX_R
)
circle_results = []
if circles is not None:
circles = np.round(circles[0, :]).astype("int")
for (x, y, r) in circles:
if not is_circle_duplicate(circle_results, (x, y), r):
circle_results.append({'center': (x, y), 'radius': r, 'type': 'hough'})
# 按行优先排序(先Y,再X)
if circle_results:
circle_results = sort_circles_by_grid(circle_results)
return circle_results, time.time() - start_time, img
def sort_circles_by_grid(circles):
"""按网格顺序排序圆(从上到下,从左到右)"""
if not circles:
return circles
# 按Y坐标排序
circles.sort(key=lambda c: c['center'][1])
# 分组为行
rows = []
if len(circles) > 0:
current_row = [circles[0]]
row_y = circles[0]['center'][1]
for circle in circles[1:]:
y = circle['center'][1]
# 如果Y坐标差异小于行高阈值,则属于同一行
if abs(y - row_y) < 30: # 行高阈值可调整
current_row.append(circle)
else:
rows.append(current_row)
current_row = [circle]
row_y = y
rows.append(current_row)
# 每行内按X坐标排序
for row in rows:
row.sort(key=lambda c: c['center'][0])
# 展平为一维列表
sorted_circles = []
for row in rows:
sorted_circles.extend(row)
return sorted_circles
import cv2
import os
import numpy as np
def draw_circles(img, circles):
"""绘制圆并按顺序标记序号"""
result_img = img.copy()
for idx, circle in enumerate(circles, 1):
center = circle['center']
radius = circle['radius']
# 绘制圆和圆心
cv2.circle(result_img, center, radius, (0, 255, 0), 2)
cv2.circle(result_img, center, 2, (0, 0, 255), -1)
# 标记序号
text = f"{idx}"
text_pos = (center[0] + 10, center[1] + 5)
cv2.putText(result_img, text, text_pos,
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)
# 标记半径
cv2.putText(result_img, f"r={radius}",
(center[0] - 10, center[1] - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (200, 100, 255), 1)
return result_img
def save_results(img, circles, output_name, proc_time):
"""保存结果和坐标"""
os.makedirs("log", exist_ok=True)
output_path = os.path.join("log", output_name)
# 添加统计信息
stats_text = f"检测到 {len(circles)} 个圆 | 时间: {proc_time:.2f}s"
cv2.putText(img, stats_text, (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)
cv2.imwrite(output_path, img)
# 保存坐标
coord_path = os.path.join("log", "circle_coordinates.txt")
with open(coord_path, "w") as f:
f.write("序号, X, Y, 半径\n")
for idx, circle in enumerate(circles, 1):
x, y = circle['center']
f.write(f"{idx}, {x}, {y}, {circle['radius']}\n")
import numpy as np
def read_input_params(file_path):
"""读取输入参数文件"""
try:
with open(file_path, 'r') as f:
params = [float(line.strip()) for line in f.readlines()]
if len(params) != 6:
raise ValueError("输入参数文件必须包含6行有效数据")
return {
'p': params[0], # 相机像素尺寸(mm/像素)
'f': params[1], # 相机焦距(mm)
'CCDx': params[2], # 横向分辨率(像素)
'CCDy': params[3], # 纵向分辨率(像素)
'VIDs': params[4], # 理论投影距离(mm)
'FOVs': params[5] # 纵向视场角设计值(度)
}
except Exception as e:
print(f"读取参数文件出错: {str(e)}")
return None
def calculate_lda(y_coord, params):
"""计算下视角(LDA)"""
if params is None:
return 0
# 公式: LDA = arctan(((y坐标 - 0.5*CCDy)*p)/f)
y_ccd = y_coord - 0.5 * params['CCDy']
numerator = y_ccd * params['p']
lda_rad = np.arctan(numerator / params['f'])
return np.degrees(lda_rad) # 转换为角度
def calculate_loa(x_coord, params):
"""计算左视角(LOA)"""
if params is None:
return 0
# 公式: LOA = arctan(((x坐标 - 0.5*CCDx)*p)/f)
x_ccd = x_coord - 0.5 * params['CCDx']
numerator = x_ccd * params['p']
loa_rad = np.arctan(numerator / params['f'])
return np.degrees(loa_rad) # 转换为角度
def calculate_fov(circles, params):
"""计算视场角(FOV)"""
if params is None or len(circles) < 104:
return 0, 0
# 确保索引有效
idx47 = 46 # 第47个点的索引(0-based)
idx58 = 57 # 第58个点的索引(0-based)
idx69 = 68 # 第69个点的索引(0-based)
idx104 = 103 # 第104个点的索引(0-based)
# 横向FOVH计算
x69 = circles[idx69]['center'][0]
x47 = circles[idx47]['center'][0]
delta_x = x69 - x47
fovh_rad = 2 * np.arctan((0.5 * delta_x * params['p']) / params['f'])
fovh_deg = np.degrees(fovh_rad)
# 纵向FOVV计算
y104 = circles[idx104]['center'][1]
y12 = circles[11]['center'][1] # 第12个点的索引(0-based)
delta_y = y104 - y12
fovv_rad = 2 * np.arctan((0.5 * delta_y * params['p']) / params['f'])
fovv_deg = np.degrees(fovv_rad)
return fovh_deg, fovv_deg
def calculate_vid(fovv_deg, params):
"""计算投影距离(VID)"""
if params is None:
return 0
# 公式: VID = (FOVV/FOVs)*VIDs
return (fovv_deg / params['FOVs']) * params['VIDs']
def calculate_virtual_image_size(vid, loa_deg, lda_deg, fovh_deg, fovv_deg):
"""计算虚像大小"""
if vid <= 0:
return 0, 0
# 转换为弧度
loa_rad = np.radians(loa_deg)
lda_rad = np.radians(lda_deg)
fovh_rad = np.radians(fovh_deg)
fovv_rad = np.radians(fovv_deg)
# 计算虚像长度
length = vid * np.cos(loa_rad) * (
np.tan(0.5 * fovh_rad + loa_rad) +
np.tan(0.5 * fovh_rad - loa_rad)
)
# 计算虚像宽度
width = vid * np.cos(lda_rad) * (
np.tan(0.5 * fovv_rad + lda_rad) -
np.tan(lda_rad - 0.5 * fovv_rad)
)
return length, width
def calculate_distortion_geely(circles):
"""计算特定点的畸变 (Distortion_geely)"""
if len(circles) < 115:
print(f"警告: 检测到{len(circles)}个圆,少于115个,无法计算Distortion_geely")
return 0
# 获取指定点的坐标 (0-based索引)
x1, y1 = circles[0]['center'] # 第1个点
x2, y2 = circles[22]['center'] # 第23个点
x3, y3 = circles[92]['center'] # 第93个点
x4, y4 = circles[114]['center'] # 第115个点
# 第一步: 计算中心点坐标
Xa = (x1 + x2 + x3 + x4) / 4
Ya = (y1 + y2 + y3 + y4) / 4
# 第二步: 计算相对于中心点的偏移
Xb = x1 - Xa
Yb = y1 - Ya
Xc = x2 - Xa
Yc = y2 - Ya
Xd = x3 - Xa
Yd = y3 - Ya
Xe = x4 - Xa
Ye = y4 - Ya
# 第三步: 计算平均偏移
Xf = (abs(Xb) + abs(Xc) + abs(Xd) + abs(Xe)) / 4
Yf = (abs(Yb) + abs(Yc) + abs(Yd) + abs(Ye)) / 4
# 第四步: 计算D值
D = np.sqrt(Xf ** 2 + Yf ** 2)
# 第五步: 计算最大偏差与D的比值
deviations = [
np.sqrt((abs(Xf) - abs(Xb)) ** 2 + (abs(Yf) - abs(Yb)) ** 2),
np.sqrt((abs(Xf) - abs(Xc)) ** 2 + (abs(Yf) - abs(Yc)) ** 2),
np.sqrt((abs(Xf) - abs(Xd)) ** 2 + (abs(Yf) - abs(Yd)) ** 2),
np.sqrt((abs(Xf) - abs(Xe)) ** 2 + (abs(Yf) - abs(Ye)) ** 2)
]
max_deviation = max(deviations)
distortion = max_deviation / D if D > 0 else 0
return distortion
# def calculate_distortion_cs(circles):
# """计算所有点的畸变 (Distortion_CS)"""
# if len(circles) < 115:
# print(f"警告: 检测到{len(circles)}个圆,少于115个,无法计算Distortion_CS")
# return 0
#
# # 仅使用前115个点
# selected_circles = circles[:115]
#
# # 第一步: 计算所有点的中心点坐标
# x_coords = [circle['center'][0] for circle in selected_circles]
# y_coords = [circle['center'][1] for circle in selected_circles]
#
# Xa = sum(x_coords) / len(selected_circles)
# Ya = sum(y_coords) / len(selected_circles)
#
# # 第二步和第三步: 计算每个点的偏移和平均偏移
# X_offsets = [x - Xa for x in x_coords]
# Y_offsets = [y - Ya for y in y_coords]
#
# Xf = sum(abs(x) for x in X_offsets) / len(X_offsets)
# Yf = sum(abs(y) for y in Y_offsets) / len(Y_offsets)
#
# # 第四步: 计算D值
# D = np.sqrt(Xf * Xf + Yf * Yf)
#
# # 第五步: 计算每个点的偏差并找出最大值
# max_deviation = 0
# for i in range(len(selected_circles)):
# deviation = np.sqrt((abs(Xf) - abs(X_offsets[i])) ** 2 +
# (abs(Yf) - abs(Y_offsets[i])) ** 2)
# max_deviation = max(max_deviation, deviation)
#
# distortion = max_deviation / D if D > 0 else 0
#
# return distortion
def calculate_all_params(circles, params_file):
"""计算所有HUD投影参数"""
# 读取输入参数
params = read_input_params(params_file)
if params is None:
return None
# 确保有足够的圆
if len(circles) < 104:
print(f"警告: 检测到{len(circles)}个圆,少于104个,无法完成所有计算")
return None
# 第58个圆的坐标(1-based -> 0-based索引57)
circle58 = circles[57]
x58, y58 = circle58['center']
# 计算下视角(LDA)
lda = calculate_lda(y58, params)
# 计算左视角(LOA)
loa = calculate_loa(x58, params)
# 计算视场角(FOV)
fovh, fovv = calculate_fov(circles, params)
# 计算投影距离(VID)
vid = calculate_vid(fovv, params)
# 计算虚像大小
length, width = calculate_virtual_image_size(vid, loa, lda, fovh, fovv)
# 计算畸变
distortion_geely = calculate_distortion_geely(circles)
# distortion_cs = calculate_distortion_cs(circles)
return {
'lda': lda, # 下视角(度)
'loa': loa, # 左视角(度)
'fovh': fovh, # 横向视场角(度)
'fovv': fovv, # 纵向视场角(度)
'vid': vid, # 投影距离(mm)
'length': length, # 虚像长度(mm)
'width': width, # 虚像宽度(mm)
# 'distortion_geely': distortion_geely, # 吉利畸变
# 'distortion_cs': distortion_cs # 国标畸变
'distortion_geely': round(distortion_geely, 6),
# 'distortion_cs': round(distortion_cs, 6)
}
onlyjuxing
import cv2
import numpy as np
import os
# 确保输出文件夹存在
os.makedirs('log', exist_ok=True)
# 读取图像并转为灰度图
image_path = 'sample_data/ghost.jpeg'
gray = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
if gray is None:
raise FileNotFoundError(f"无法读取图像: {image_path}")
color_img = cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)
# 使用 KMeans 对图像亮点像素聚类,自动区分本体与重影
pixels = gray[gray > 0].astype(np.float32)
if pixels.size == 0:
raise ValueError("图像中未检测到任何亮点。")
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
_, labels, centers = cv2.kmeans(pixels.reshape(-1, 1), 2, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS)
centers = centers.flatten()
main_center = centers.max()
ghost_center = centers.min()
thresh = (main_center + ghost_center) / 2.0
main_mask = (gray > thresh).astype(np.uint8) * 255
ghost_gray = gray.copy()
ghost_gray[main_mask == 255] = 0
# 本体连通域分析
num_labels, labels_im, stats, centroids = cv2.connectedComponentsWithStats(main_mask, 8, cv2.CV_32S)
if num_labels <= 1:
raise ValueError("未检测到任何本体线段。")
object_labels = list(range(1, num_labels))
shape_w = int(np.median(stats[1:, cv2.CC_STAT_WIDTH]))
shape_h = int(np.median(stats[1:, cv2.CC_STAT_HEIGHT]))
# 自动估计垂直行间距(用于判断重影搜索窗口)
row_center_ys = sorted([centroids[i][1] for i in object_labels])
row_spacing = np.median(np.diff(row_center_ys)) if len(row_center_ys) > 5 else shape_h * 2
# 开始处理每个本体,寻找重影并输出结果
with open('log/detection_results.txt', 'w', encoding='utf-8') as f:
idx = 1
for label in object_labels:
mx, my = centroids[label]
x, y, w0, h0 = stats[label, cv2.CC_STAT_LEFT], stats[label, cv2.CC_STAT_TOP], stats[label, cv2.CC_STAT_WIDTH], \
stats[label, cv2.CC_STAT_HEIGHT]
# 绘制红色本体框
cv2.rectangle(color_img, (x, y), (x + w0, y + h0), (0, 0, 255), 2)
# 自动判断线条方向:宽>高为横线,否则为竖线
is_horizontal = w0 >= h0
# 定义重影搜索区域:横线向上搜索,竖线向左搜索
margin = 5
if is_horizontal:
# 横线:重影在上方区域
sx0 = int(mx - w0 / 2 - margin)
sx1 = int(mx + w0 / 2 + margin)
sy0 = int(my - row_spacing)
sy1 = int(my - h0 / 2)
else:
# 竖线:重影在左方区域
sx0 = int(mx - row_spacing)
sx1 = int(mx - w0 / 2)
sy0 = int(my - h0 / 2 - margin)
sy1 = int(my + h0 / 2 + margin)
sx0, sy0 = max(0, sx0), max(0, sy0)
sx1, sy1 = min(gray.shape[1], sx1), min(gray.shape[0], sy1)
roi = ghost_gray[sy0:sy1, sx0:sx1]
if roi.size == 0 or np.sum(roi) == 0:
gx, gy = mx, my
dist_h = 0.0
dist_v = 0.0
f.write(f"编号{idx}: 本体中心 ({mx:.1f},{my:.1f}),未检测到重影\n")
else:
# 改进的质心提取方式:避免偏移,保持重心靠近 ROI 重心
Y, X = np.indices(roi.shape)
total = float(np.sum(roi))
gx = float(np.sum(X * roi)) / total + sx0
gy = float(np.sum(Y * roi)) / total + sy0
if is_horizontal:
dist_h = 0.0
dist_v = abs(gy - my)
else:
dist_v = 0.0
dist_h = abs(gx - mx)
# 绘制蓝色重影框(与本体等尺寸)
gx0 = int(gx - w0 / 2)
gy0 = int(gy - h0 / 2)
gx1 = gx0 + w0
gy1 = gy0 + h0
cv2.rectangle(color_img, (gx0, gy0), (gx1, gy1), (255, 0, 0), 2)
f.write(
f"编号{idx}: 本体中心 ({mx:.1f},{my:.1f}),重影中心 ({gx:.1f},{gy:.1f}),"
f"水平距离 {dist_h:.1f} 像素,竖直距离 {dist_v:.1f} 像素\n"
)
idx += 1
# 保存输出图片
cv2.imwrite('log/detection_result.png', color_img)
##########################蓝色框有偏移
# # import cv2
# # import numpy as np
# # import os
# #
# import cv2
# import numpy as np
# import os
#
# # 确保输出文件夹存在
# os.makedirs('log', exist_ok=True)
#
# # 1. 读取输入图像并转为灰度图
# image_path = 'sample_data/ghost.jpeg'
# gray = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
# if gray is None:
# raise FileNotFoundError(f"无法读取图像: {image_path}")
# # 转为彩色图用于绘制彩色框
# color_img = cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)
#
# # 2. 利用 KMeans 聚类将非背景灰度像素分为两类:本体(高亮)和重影(暗)
# pixels = gray[gray > 0].astype(np.float32)
# if pixels.size == 0:
# raise ValueError("图像中未检测到任何亮点。")
# criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
# _, labels, centers = cv2.kmeans(pixels.reshape(-1, 1), 2, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS)
# centers = centers.flatten()
# main_center = centers[0] if centers[0] > centers[1] else centers[1]
# ghost_center = centers[1] if centers[0] > centers[1] else centers[0]
# threshold_val = (main_center + ghost_center) / 2.0
#
# # 生成本体掩膜(亮度大于阈值)和重影灰度图(去除本体后保留暗部)
# main_mask = np.zeros_like(gray, dtype=np.uint8)
# main_mask[gray > threshold_val] = 255
# ghost_gray = gray.copy()
# ghost_gray[main_mask == 255] = 0
#
# # 3. 连通组件分析识别本体矩形,并获取统计信息和质心
# num_labels, labels_im, stats, centroids = cv2.connectedComponentsWithStats(main_mask, 8, cv2.CV_32S)
# if num_labels <= 1:
# raise ValueError("未检测到任何本体线段。")
#
# # 获取所有本体的标签 ID 列表(跳过背景 0)
# object_labels = list(range(1, num_labels))
# # 获取典型矩形宽高,用于后续框大小
# shape_w = int(np.median(stats[1:, cv2.CC_STAT_WIDTH]))
# shape_h = int(np.median(stats[1:, cv2.CC_STAT_HEIGHT]))
#
# # 估计行间距,用于后续重影搜索区域计算
# row_center_ys = [centroids[i][1] for i in object_labels]
# row_center_ys.sort()
# if len(row_center_ys) > 1:
# spacings = [row_center_ys[i + 1] - row_center_ys[i] for i in range(len(row_center_ys) - 1)]
# row_spacing = float(np.median(spacings))
# else:
# row_spacing = shape_h * 2
#
# # 4. 对每个本体,按检测到的质心顺序进行处理并输出结果
# with open('log/detection_results.txt', 'w', encoding='utf-8') as f:
# idx = 1
# for label in object_labels:
# mx, my = centroids[label] # 本体中心坐标
# # 定位重影区域 ROI:本体上方,水平延伸 shape_w/2 + 5 像素
# x0 = int(mx - shape_w / 2 - 5)
# x1 = int(mx + shape_w / 2 + 5)
# y1 = int(my)
# y0 = int(my - min(row_spacing - shape_h, row_spacing * 0.5) - shape_h)
# x0, y0 = max(0, x0), max(0, y0)
# x1, y1 = min(gray.shape[1], x1), min(gray.shape[0], y1)
# roi = ghost_gray[y0:y1, x0:x1]
#
# if roi.size == 0 or np.sum(roi) == 0:
# # 未找到重影
# gx, gy = mx, my
# dist_h = 0.0
# dist_v = 0.0
# f.write(f"编号{idx}: 本体中心 ({mx:.1f},{my:.1f}),未检测到重影\n")
# else:
# # 计算重影灰度质心
# Y, X = np.indices(roi.shape)
# total = float(np.sum(roi))
# gx = float(np.sum(X * roi)) / total + x0
# gy = float(np.sum(Y * roi)) / total + y0
# # 判断线段方向:宽 > 高 为横线,否则为竖线
# w = stats[label, cv2.CC_STAT_WIDTH]
# h = stats[label, cv2.CC_STAT_HEIGHT]
# if w > h:
# # 横线:重影在垂直方向
# dist_h = 0.0
# dist_v = abs(gy - my)
# else:
# # 竖线:重影在水平方向
# dist_v = 0.0
# dist_h = abs(gx - mx)
# # 写入日志,包含中心坐标与水平/竖直距离
# f.write(
# f"编号{idx}: 本体中心 ({mx:.1f},{my:.1f}),重影中心 ({gx:.1f},{gy:.1f}),"
# f"水平距离 {dist_h:.1f} 像素,竖直距离 {dist_v:.1f} 像素\n"
# )
# # 绘制本体和重影的矩形框
# x, y, w0, h0 = stats[label, cv2.CC_STAT_LEFT], stats[label, cv2.CC_STAT_TOP], stats[
# label, cv2.CC_STAT_WIDTH], stats[label, cv2.CC_STAT_HEIGHT]
# # 红色框 本体
# cv2.rectangle(color_img, (x, y), (x + w0, y + h0), (0, 0, 255), 2)
# # 蓝色框 重影(使用同样大小框,以中心对齐)
# gx0 = int(gx - w0 / 2)
# gy0 = int(gy - h0 / 2)
# gx1 = gx0 + w0
# gy1 = gy0 + h0
# cv2.rectangle(color_img, (gx0, gy0), (gx1, gy1), (255, 0, 0), 2)
# idx += 1
#
# # 5. 保存标注结果图像(不弹窗)
# cv2.imwrite('log/detection_result.png', color_img)
###############非常好
# # 确保输出文件夹存在
# os.makedirs('log', exist_ok=True)
#
# # 1. 读取输入图像并转为灰度图
# image_path = 'sample_data/ghost.jpeg'
# gray = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
# if gray is None:
# raise FileNotFoundError(f"无法读取图像: {image_path}")
#
# # 将灰度图转换为彩色图,用于绘制彩色标注
# color_img = cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)
#
# # 2. 利用像素强度聚类,将亮度较高的本体与亮度较低的重影区分开
# # 首先,获取图像中所有非背景(黑色)像素的灰度值
# pixels = gray[gray > 0].astype(np.float32)
# if pixels.size == 0:
# raise ValueError("图像中未检测到任何亮点。")
#
# # 使用 KMeans 聚类将非零像素灰度分为两类(本体和重影):contentReference[oaicite:3]{index=3}
# criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
# _, labels, centers = cv2.kmeans(pixels.reshape(-1, 1), 2, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS)
# centers = centers.flatten()
# # 判断哪个聚类中心对应本体(较亮),哪个对应重影(较暗)
# main_cluster_center = centers[0] if centers[0] > centers[1] else centers[1]
# ghost_cluster_center = centers[1] if centers[0] > centers[1] else centers[0]
# # 取两个中心值的中点作为阈值,将灰度图划分为本体和重影区域
# threshold_val = (main_cluster_center + ghost_cluster_center) / 2.0
#
# # 生成二值掩膜:本体掩膜和重影灰度图
# main_mask = np.zeros_like(gray, dtype=np.uint8)
# main_mask[gray > threshold_val] = 255
# # 重影灰度图:将原图中本体像素去除(置零),保留下较暗的重影部分
# ghost_gray = gray.copy()
# ghost_gray[main_mask == 255] = 0
#
# # 3. 寻找每个本体矩形(连通组件)及其中心坐标:contentReference[oaicite:4]{index=4}
# num_labels, labels_im, stats, centroids = cv2.connectedComponentsWithStats(main_mask, 8, cv2.CV_32S)
# num_objects = num_labels - 1 # 去除背景标签
# if num_objects == 0:
# raise ValueError("未检测到任何本体线段。")
#
# # 提取所有本体的质心坐标列表(跳过标签0背景)
# object_centers = [tuple(centroids[i]) for i in range(1, num_labels)]
#
# # 将检测到的中心按垂直方向聚类分为5行
# rows = 5
# Y = np.array([centroids[i][1] for i in range(1, num_labels)], dtype=np.float32)
# K = rows if num_objects >= rows else num_objects
# _, row_labels, row_centers = cv2.kmeans(Y.reshape(-1, 1), K, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS)
# row_centers = row_centers.flatten()
# # 按行从上到下排序每个组件,然后在行内按x从小到大排序
# row_order = np.argsort(row_centers)
# sorted_labels = []
# for ci in row_order:
# indices = [i + 1 for i in range(row_labels.shape[0]) if row_labels[i] == ci]
# indices.sort(key=lambda a: centroids[a][0])
# sorted_labels += indices
#
# # 从统计信息获取典型的矩形宽度和高度(取中位数,以减小噪声影响)
# shape_w = int(np.median(stats[1:, cv2.CC_STAT_WIDTH]))
# shape_h = int(np.median(stats[1:, cv2.CC_STAT_HEIGHT]))
#
# # 估计相邻行的垂直间距,用于确定重影搜索区域高度
# row_centers_y = []
# for ci in row_order:
# # 取该行所有本体的平均y坐标作为行中心
# y_coords = [centroids[i][1] for i in range(1, num_labels) if row_labels[i - 1] == ci]
# if y_coords:
# row_centers_y.append(sum(y_coords) / len(y_coords))
# if len(row_centers_y) > 1:
# spacings = [row_centers_y[i + 1] - row_centers_y[i] for i in range(len(row_centers_y) - 1)]
# row_spacing = float(np.median(spacings))
# else:
# row_spacing = float(shape_h * 2)
#
# # 4. 针对每个本体,在其上方局部区域定位重影中心并计算距离
# margin = 5 # 水平余量,以涵盖重影可能的小水平偏移
# # 垂直搜索高度为行间距的一半或行间距减去本体高度(取较小者),最低不少于5像素
# max_search = int(max(5.0, min(row_spacing - shape_h, row_spacing * 0.5)))
#
# # 打开结果文本文件用于写入
# with open('log/detection_results.txt', 'w', encoding='utf-8') as f:
# for idx, label in enumerate(sorted_labels, start=1):
# mx, my = centroids[label] # 本体中心
# # 定义重影搜索ROI(在本体中心正上方区域)
# x0 = int(mx - shape_w / 2 - margin)
# x1 = int(mx + shape_w / 2 + margin)
# y1 = int(my) # ROI下边界(不包含本体所在行)
# y0 = int(my - max_search - shape_h) # ROI上边界(在本体上方一定距离处)
# # 边界检查,确保ROI在图像范围内
# if x0 < 0:
# x0 = 0
# if x1 > gray.shape[1]:
# x1 = gray.shape[1]
# if y0 < 0:
# y0 = 0
# if y1 > gray.shape[0]:
# y1 = gray.shape[0]
#
# roi = ghost_gray[y0:y1, x0:x1] # 提取重影区域的灰度图
# if roi.size == 0 or np.sum(roi) == 0:
# # 未在预期区域检测到任何重影像素
# line = f"编号{idx}: 本体中心坐标 ({mx:.1f}, {my:.1f}),未找到对应的重影"
# else:
# # 计算ROI中像素的灰度质心作为重影中心
# Y_coords, X_coords = np.indices(roi.shape)
# total_intensity = float(np.sum(roi))
# gx = float(np.sum(X_coords * roi)) / total_intensity + x0
# gy = float(np.sum(Y_coords * roi)) / total_intensity + y0
# # 计算本体与重影中心的欧氏距离(像素距离)
# dist = np.sqrt((gx - mx) ** 2 + (gy - my) ** 2)
# line = f"编号{idx}: 本体中心坐标 ({mx:.1f}, {my:.1f}),重影中心坐标 ({gx:.1f}, {gy:.1f}),像素距离 {dist:.1f} 像素"
# # 在图像上绘制标记和连线
# cv2.circle(color_img, (int(round(mx)), int(round(my))), 3, (0, 0, 255), -1) # 红色圆点标记本体
# cv2.circle(color_img, (int(round(gx)), int(round(gy))), 3, (255, 0, 0), -1) # 蓝色圆点标记重影
# cv2.line(color_img, (int(round(mx)), int(round(my))), (int(round(gx)), int(round(gy))), (0, 255, 0),
# 1) # 绿色连线
#
# f.write(line + "\n")
#
# # 保存带标注的结果图像(不显示图像)
# cv2.imwrite('log/detection_result.png', color_img)