Python opencv识别图片中重叠圆的圆心位置

python 复制代码
# -*- coding: utf-8 -*-
"""
只标注圆心 + 颜色一致性过滤 + 输出过程图

思路:
1) 灰度 + Otsu 阈值;若圆为黑色前景,则反相使圆为白色前景(距离变换以白为前景)
2) 形态学闭运算,修补小孔/断裂
3) 欧氏距离变换(圆心处距离≈半径)
4) 距离模板相关得到峰值响应,阈值化为候选区域
5) 每个候选区域在距离图上取局部极大(圆心),并做"颜色一致性"过滤:
   - 圆心像素必须为前景(白)
   - 圆心附近小圆盘区域的前景占比 >= min_fg_ratio(默认 0.6)
6) 仅在圆心画十字;保存中间过程图
"""

import os
import cv2
import numpy as np

# ---------------- 配置参数 ----------------
img_path = 'code.jpg'
out_dir = 'out'                  # 输出过程图目录
invert_binary = True             # 若原图是白底黑圆,请保持 True(反相后圆=白色前景)
peak_thresh_ratio = 0.60         # 模板相关响应阈值(相对最大值比例)
min_peak_area = 5                # 峰值连通域最小面积(像素),过滤噪点
borderSize = 75                  # 模板/边界扩展参数
gap = 10                         # 模板与边缘的空隙
# 颜色一致性检查
disk_fraction = 0.5              # 用于颜色一致性检测的邻域半径 = r * disk_fraction(若 r 小则自适应 >= 4 像素)
min_fg_ratio = 0.60              # 邻域内白色前景比例阈值(0~1),越大越严格

# 标注样式
marker_color = (0, 255, 0)
marker_size = 12
marker_thickness = 1


def ensure_dir(d):
    if not os.path.exists(d):
        os.makedirs(d)


def to_vis_u8(img_f32):
    """将 float 图像线性归一化到 0~255 的 uint8,便于保存/显示"""
    v = cv2.normalize(img_f32, None, 0, 255, cv2.NORM_MINMAX)
    return v.astype(np.uint8)


def fg_ratio_in_disk(fg_u8, cx, cy, r_eff):
    """
    计算以 (cx, cy) 为圆心、半径 r_eff 的小圆盘内,前景(255)像素比例。
    fg_u8: 二值图或形态学结果,前景=255,背景=0
    """
    h, w = fg_u8.shape[:2]
    if r_eff < 1:
        return 0.0
    x0, y0 = max(0, cx - r_eff), max(0, cy - r_eff)
    x1, y1 = min(w - 1, cx + r_eff), min(h - 1, cy + r_eff)
    roi = fg_u8[y0:y1+1, x0:x1+1]
    if roi.size == 0:
        return 0.0

    # 在 ROI 内生成圆形掩膜
    mask = np.zeros_like(roi, dtype=np.uint8)
    cv2.circle(mask, (cx - x0, cy - y0), r_eff, 255, thickness=-1)
    area = int(np.count_nonzero(mask))
    if area == 0:
        return 0.0
    # 前景像素计数(同时满足掩膜与前景)
    fg_count = int(np.count_nonzero(cv2.bitwise_and(roi, mask)))
    # 因为前景为 255,bitwise_and 后非零即计数;比例按像素个数计算
    return fg_count / area


def main():
    ensure_dir(out_dir)

    # 1) 读图与灰度
    im = cv2.imread(img_path, cv2.IMREAD_COLOR)
    if im is None:
        raise FileNotFoundError(f'无法读取图像:{img_path}')
    gray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)

    # 2) 二值化(Otsu)
    _, bw = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)
    # 反相:使圆为白色前景
    if invert_binary:
        bw = cv2.bitwise_not(bw)
    cv2.imwrite(os.path.join(out_dir, '01_bw.png'), bw)

    # 3) 形态学闭运算(修补小孔/细小断裂)
    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
    morph = cv2.morphologyEx(bw, cv2.MORPH_CLOSE, kernel)
    cv2.imwrite(os.path.join(out_dir, '02_morph.png'), morph)

    # 4) 距离变换
    dist = cv2.distanceTransform(morph, cv2.DIST_L2, 5)
    cv2.imwrite(os.path.join(out_dir, '03_dist.png'), to_vis_u8(dist))

    # 5) 构造模板并做归一化相关
    distborder = cv2.copyMakeBorder(
        dist, borderSize, borderSize, borderSize, borderSize,
        cv2.BORDER_CONSTANT | cv2.BORDER_ISOLATED, 0
    )
    kernel2 = cv2.getStructuringElement(
        cv2.MORPH_ELLIPSE, (2 * (borderSize - gap) + 1, 2 * (borderSize - gap) + 1)
    )
    kernel2 = cv2.copyMakeBorder(
        kernel2, gap, gap, gap, gap,
        cv2.BORDER_CONSTANT | cv2.BORDER_ISOLATED, 0
    )
    distTempl = cv2.distanceTransform(kernel2, cv2.DIST_L2, 5)

    nxcor = cv2.matchTemplate(distborder, distTempl, cv2.TM_CCOEFF_NORMED)
    cv2.imwrite(os.path.join(out_dir, '04_nxcor.png'), to_vis_u8(nxcor))

    # 6) 阈值化得到峰值区域,并做最小面积过滤
    _, mx, _, _ = cv2.minMaxLoc(nxcor)
    _, peaks = cv2.threshold(nxcor, mx * peak_thresh_ratio, 255, cv2.THRESH_BINARY)
    peaks8u = cv2.convertScaleAbs(peaks)

    # 可选:删除太小的连通域(腐蚀/开运算或按轮廓面积过滤)
    cnt_res = cv2.findContours(peaks8u, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
    contours = cnt_res[0] if len(cnt_res) == 2 else cnt_res[1]
    peaks_clean = np.zeros_like(peaks8u)
    kept_bboxes = []
    for cnt in contours:
        area = cv2.contourArea(cnt)
        if area >= min_peak_area:
            cv2.drawContours(peaks_clean, [cnt], -1, 255, thickness=-1)
            kept_bboxes.append(cv2.boundingRect(cnt))
    cv2.imwrite(os.path.join(out_dir, '05_peaks_mask.png'), peaks_clean)
    

    # 7) 在每个候选区域中找局部极大,并做颜色一致性过滤
    annotated = im.copy()
    centers = []
    
    for index, (x, y, w_box, h_box) in enumerate(kept_bboxes):
        # 用峰值掩膜作为 mask,限定在候选区域内找最大值(圆心)
        mask_roi = peaks_clean[y:y+h_box, x:x+w_box]
        min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(
            dist[y:y+h_box, x:x+w_box], mask=mask_roi
        )
        cx, cy = int(max_loc[0] + x), int(max_loc[1] + y)
        r = int(round(max_val))  # 估计半径

        # --- 颜色一致性过滤 ---
        # 1) 圆心像素必须为前景(白)
        if morph[cy, cx] == 0:
            continue
        # 2) 圆心邻域的前景占比必须足够高
        r_eff = max(4, int(r * disk_fraction))
        fg_ratio = fg_ratio_in_disk(morph, cx, cy, r_eff)
        if fg_ratio < min_fg_ratio:
            continue

        centers.append((cx, cy))
        # 只画圆心(绿色十字)
        cv2.drawMarker(
            annotated, (cx, cy),
            color=marker_color,
            markerType=cv2.MARKER_CROSS,
            markerSize=marker_size,
            thickness=marker_thickness,
            line_type=cv2.LINE_AA
        )
        cv2.putText(
            annotated, f'#{index+1} ({cx}, {cy})', (cx + 5, cy - 5),
            cv2.FONT_HERSHEY_SIMPLEX, 0.5, marker_color, 1, cv2.LINE_AA
        )

    # 8) 输出
    print(f'最终圆心数:{len(centers)}')
    for i, (cx, cy) in enumerate(centers, start=1):
        print(f'#{i}: x={cx}, y={cy}')

    cv2.imwrite(os.path.join(out_dir, '06_centers.png'), annotated)

    cv2.imshow('centers', annotated)
    cv2.waitKey(0)
    cv2.destroyAllWindows()


if __name__ == '__main__':
    main()
相关推荐
VidDown3 小时前
24fps 转 25fps 不只是改个数字:帧率转换、PAL/NTSC 与音频补偿
python·网络协议·音视频·视频编解码·视频
死磕音视频5 小时前
让小模型做选择题:限定候选项并读取 logits 概率的入门实践(附 Python 代码)
开发语言·python·大模型·文本分类·transformers
凤山老林5 小时前
Spring Boot 接入国密 SM2/SM3/SM4 的一些实践记录
spring boot·后端·python
再__努力1点7 小时前
OpenCV 5 vs OpenCV 4:一次讲清所有差异
人工智能·opencv·计算机视觉
pride.li7 小时前
第03章 Python 教程-基础语法:注释与变量
服务器·前端·后端·python·算法·计算机视觉
databook7 小时前
用 Pydantic AI 从零搭建一个能干活的 AI Agent
人工智能·python
顽疲7 小时前
Java vue 养老系统源码详解:月账单 care_month_bill 与 Quartz 扣费 Job 实践
java·开发语言·vue.js·vue养老·springboot养老·java养老
康小旺7 小时前
把 markitdown 做成 Windows 桌面版:拖进文档就出 Markdown,附 233MB→64MB 打包裁剪全过程
python
Wang's Blog8 小时前
Java 项目实战: 交友项目 - 安装开发工具与环境准备
java·开发语言·交友
启观川8 小时前
数据结构与算法-第 3 章 常用算法-贪心算法
数据结构·笔记·python·算法·贪心算法