๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

AI/Computer Vision

[Computer Vision] Trapped-ball Segmentation

728x90
๋ฐ˜์‘ํ˜•
๐Ÿ‘€ ๋ณธ ์˜ˆ์ œ๋Š” Window10์˜ VSCode, Python3.11.0๋กœ ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

 

Trapped-ball Segmentation์€ ์ฃผ๋กœ ๋งŒํ™” ํ”„๋ ˆ์ž„๊ณผ ๊ฐ™์€ ์ด๋ฏธ์ง€์—์„œ ์˜๋ฏธ ์žˆ๋Š” ์˜์—ญ์„ ์ถ”์ถœํ•˜๋Š”๋ฐ ํšจ๊ณผ์ ์ธ ๋น„์ง€๋„ ์ด๋ฏธ์ง€ ๋ถ„ํ•  ๊ธฐ๋ฒ•์ด๋‹ค.

 

์ด ๋ฐฉ๋ฒ•์€ ์ด๋ฏธ์ง€์˜ ๊ฒฝ๊ณ„์„ ์— ์˜ํ•ด ์ •์˜๋œ ๋‹ซํžŒ ์ง€์—ญ์„ ์‹๋ณ„ํ•˜๊ณ  ์ด๋ฅผ ํ†ตํ•ด ์ด๋ฏธ์ง€ ๋‚ด์—์„œ ๋ถ„ํ• ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.

 

์ด ์•Œ๊ณ ๋ฆฌ์ฆ˜์€ ํ˜•ํƒœํ•™์  ์—ฐ์‚ฐ์„ ์‚ฌ์šฉํ•˜์—ฌ ํฐ ํ์‡„๋œ ์ง€์—ญ์„ ์‹๋ณ„ํ•˜๊ณ , ๊ฐ ์ง€์—ญ์— ๋Œ€ํ•ด ๊ณ ์œ ํ•œ ๊ฐ’์„ ์ฑ„์šฐ๋Š” ๋ฐฉ์‹์œผ๋กœ ์ž‘๋™ํ•œ๋‹ค.

 

๋™์ž‘ ์ˆœ์„œ

  1. ํ‘๋ฐฑ ๋˜๋Š” ์ƒ‰์ƒ์ด ์—†๋Š” ์Šค์ผ€์น˜ ์ด๋ฏธ์ง€๋ฅผ ํƒ€๊นƒ์œผ๋กœ ์ค€๋น„ํ•œ๋‹ค.
  2. ํ˜•ํƒœํ•™์  ์—ฐ์‚ฐ(Morphological Operations)์„ ์ˆ˜ํ–‰(์นจ์‹(Erosion), ํŒฝ์ฐฝ(Dilation)ํ•˜์—ฌ ๊ฐ ์˜์—ญ์˜ ๊ฒฝ๊ณ„๋ฅผ ์ž˜ ๋“œ๋Ÿฌ๋‚ด๋„๋ก ํ•œ๋‹ค.
  3. ๋น„๊ต์  ๋„“์€ ์˜์—ญ์„ ์ฐพ๊ณ , ํŠน์ • ํฌ๊ธฐ ๋˜๋Š” ๋ชจ์–‘์— ๊ธฐ๋ฐ˜ํ•˜์—ฌ ์˜์—ญ์„ ํ•„ํ„ฐ๋งํ•œ๋‹ค.
  4. ์ฐพ์•„๋‚ธ ์˜์—ญ์€ ๊ณ ์œ ํ•œ ๊ฐ’์œผ๋กœ ์ฑ„์›Œ์ง€๊ณ , ์ด ๊ณผ์ •์„ ํ†ตํ•ด ํฐ ์ง€์—ญ๋“ค์ด ๋ช…ํ™•ํ•˜๊ฒŒ ์‹œ๊ฐํ™” ๋œ๋‹ค.
  5. ํฐ ์˜์—ญ๋“ค์ด ์ „๋ถ€ ์ฑ„์›Œ์ง€๊ณ , ๋” ์ž‘์€ ์˜์—ญ์œผ๋กœ ์„ธ๋ถ„ํ™”ํ•˜๋ฉด์„œ ์ฑ„์šฐ๊ธฐ๋ฅผ ๋ฐ˜๋ณตํ•œ๋‹ค. (์ด ์ž‘์—…์€ ์ดˆ๊ธฐ ๋‹จ๊ณ„์— ์„ค์ •ํ•œ ๊ธฐ์ค€์— ๋”ฐ๋ผ ๋ฐ˜๋ณตํ•˜๊ณ , ๋ชจ๋“  ์˜์—ญ์ด ๋‹ค ์ฑ„์›Œ์งˆ ๋•Œ๊นŒ์ง€ ๋ฐ˜๋ณตํ•œ๋‹ค.)
  6. ์ตœ์ข…์ ์œผ๋กœ ์„ธ๋ถ„ํ™”๋œ ์ด๋ฏธ์ง€๋ฅผ ์ถœ๋ ฅํ•œ๋‹ค.

 

๋™์ž‘์„ ํ•˜๋‚˜ํ•˜๋‚˜ ๋”ฐ๋ผ๊ฐ€๋ณด์ž.

 

๋จผ์ € ํƒ€๊นƒ ์ด๋ฏธ์ง€๋ฅผ Threshold๋กœ ํ‘๋ฐฑ ์ด๋ฏธ์ง€๋กœ ๋ณ€๊ฒฝํ•œ๋‹ค.

img = cv2.imread("./test.jpg",cv2.IMREAD_GRAYSCALE)
ret, binary = cv2.threshold(image, 220, 255, cv2.THRESH_BINARY)
unfilled = binary

 

ํ˜•ํƒœํ•™์  ์—ฐ์‚ฐ์œผ๋กœ ๊ฒฝ๊ณ„๋ฅผ ์ž˜ ๋“œ๋Ÿฌ๋‚˜๊ฒŒ ์ฒ˜๋ฆฌํ•œ๋‹ค.

def get_unfilled_point(image: np.ndarray)->np.ndarray:
    """๊ฐ’์ด 255์ธ ์˜์—ญ(๋นˆ ์˜์—ญ)์— ํ•ด๋‹นํ•˜๋Š” ํฌ์ธํŠธ๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.

    # ์ธ์ž
        image: ์ฒ˜๋ฆฌํ•  ์ด๋ฏธ์ง€.

    # ๋ฐ˜ํ™˜ ๊ฐ’
        ๋นˆ ์˜์—ญ(์„ ํ™”๊ฐ€ ์—†๋Š” ์˜์—ญ)์— ๋Œ€ํ•œ ํฌ์ธํŠธ ๋ฐฐ์—ด.
    """
    y, x = np.where(image == 255) # ์ด๋ฏธ์ง€์—์„œ ๊ฐ’์ด 255์ธ ํ”ฝ์…€์˜ x,y ์ขŒํ‘œ๋ฅผ ์ฐพ๋Š”๋‹ค.

    return np.stack((x.astype(int), y.astype(int)), axis=-1) # x,y ์ขŒํ‘œ๋ฅผ ๊ฒฐํ•ฉํ•˜์—ฌ ํฌ์ธํŠธ ๋ฐฐ์—ด์„ ์ƒ์„ฑํ•œ๋‹ค.

def exclude_area(image:np.ndarray, radius)->np.ndarray:
    """์ด๋ฏธ์ง€์—์„œ ๊ฒฝ๊ณ„์— ๊ฐ€๊นŒ์šด ํฌ์ธํŠธ๋ฅผ ์ œ์™ธํ•˜๊ธฐ ์œ„ํ•ด ์นจ์‹์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.
    ํŒฝ์ฐฝ ํ›„ ์‹œ๋“œ ํฌ์ธํŠธ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์˜์—ญ์„ ์ฑ„์šฐ๊ณ ์ž ํ•œ๋‹ค.
    ์‹œ๋“œ ํฌ์ธํŠธ๊ฐ€ ๊ฒฝ๊ณ„ ๊ทผ์ฒ˜์— ์žˆ์„ ๊ฒฝ์šฐ, ์ฑ„์šฐ๊ธฐ ๊ฒฐ๊ณผ์— ํฌํ•จ๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์œผ๋ฉฐ,
    ๋‹ค์Œ ์ฑ„์šฐ๊ธฐ ์ž‘์—…์„ ์œ„ํ•œ ์œ ํšจํ•œ ํฌ์ธํŠธ๊ฐ€ ๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ๋‹ค. ๋”ฐ๋ผ์„œ,
    ์ด๋Ÿฌํ•œ ํฌ์ธํŠธ๋Š” ์นจ์‹์„ ํ†ตํ•ด ๋ฌด์‹œํ•œ๋‹ค.

    # Arguments
        image: ์ด๋ฏธ์ง€
        radius: ๊ตฌํ˜• ๊ตฌ์กฐ ์š”์†Œ์˜ ๋ฐ˜์ง€๋ฆ„

    # Returns
        ์นจ์‹๋œ ์ด๋ฏธ์ง€
    """
    return cv2.morphologyEx(image, cv2.MORPH_ERODE, get_ball_structuring_element(radius), anchor=(-1, -1), iterations=1)

 

๊ทธ๋ฆฌ๊ณ  ํฐ์˜์—ญ์„ ์ฐพ๊ธฐ ์œ„ํ•ด ๊ตฌํ˜• ๊ตฌ์กฐ ์š”์†Œ๋ฅผ ๋งŒ๋“ค๊ณ  ์˜์—ญ์„ ํƒ์ƒ‰ํ•œ๋‹ค.

def get_ball_structuring_element(radius:int)->np.ndarray:
    """ํŠน์ • ๋ฐ˜์ง€๋ฆ„์„ ๊ฐ€์ง„ ๊ตฌํ˜• ๊ตฌ์กฐ ์š”์†Œ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜.
    ์ด ๊ตฌ์กฐ ์š”์†Œ๋Š” ํ˜•ํƒœํ•™์  ์ž‘์—…(morphology operation)์—์„œ ์‚ฌ์šฉ๋œ๋‹ค.
    ๊ตฌ์˜ ๋ฐ˜์ง€๋ฆ„์€ ์ผ๋ฐ˜์ ์œผ๋กœ (leaking_gap_size / 2)์™€ ๊ฐ™๋‹ค.

    # Arguments
        radius: ๊ตฌํ˜• ๊ตฌ์กฐ ์š”์†Œ์˜ ๋ฐ˜์ง€๋ฆ„ 
             
    # Returns
        ๊ตฌํ˜• ๊ตฌ์กฐ ์š”์†Œ์˜ ๋ฐฐ์—ด
    """
    return cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2 * radius + 1, 2 * radius + 1))

def trapped_ball_fill_single(image:np.ndarray, seed_point:tuple[int,int], radius:int)->np.ndarray:
    """๋‹จ์ผ Trapped-ball ์ฑ„์šฐ๊ธฐ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๋Š” ํ•จ์ˆ˜
    
    Flood Fill : ๋‹ค์ฐจ์› ๋ฐฐ์—ด์˜ ์–ด๋–ค ์นธ๊ณผ ์—ฐ๊ฒฐ๋œ ์˜์—ญ์„ ์ฐพ๋Š” ์•Œ๊ณ ๋ฆฌ์ฆ˜.

    # Arguments
        image: ํฐ์ƒ‰(๋นˆ ์˜์—ญ) ๋ฐฐ๊ฒฝ์˜ ๊ฒ€์€(์ฑ„์›Œ์ง„) ์„ ํ™”๋ฅผ ๊ฐ€์ง„ ์ด๋ฏธ์ง€
        seed_point: ๊ฐ‡ํžŒ ๊ณต ์ฑ„์šฐ๊ธฐ์šฉ ์‹œ๋“œ ํฌ์ธํŠธ, ํŠœํ”Œ ํ˜•์‹(int,int)
        radius: ๊ตฌํ˜• ๊ตฌ์กฐ ์š”์†Œ์˜ ๋ฐ˜์ง€๋ฆ„
    # Returns
        ์ฑ„์šด ์ด๋ฏธ์ง€
    """
    ball = get_ball_structuring_element(radius) # radius๋กœ ๊ตฌํ˜• ๊ตฌ์กฐ ์š”์†Œ ์ƒ์„ฑ

    pass1 = np.full(image.shape, 255, np.uint8) # ์ „์ฒด๊ฐ€ ํฐ์ƒ‰์ธ ๋ฐฐ์—ด
    pass2 = np.full(image.shape, 255, np.uint8) # ์ „์ฒด๊ฐ€ ํฐ์ƒ‰์ธ ๋ฐฐ์—ด

    im_inv = cv2.bitwise_not(image) # ์ด๋ฏธ์ง€ ์ƒ‰์ƒ ๋ฐ˜์ „

    # Flood fill ์ˆ˜ํ–‰
    mask1 = cv2.copyMakeBorder(im_inv, 1, 1, 1, 1, cv2.BORDER_CONSTANT, 0) # ๊ฒฝ๊ณ„ ์ถ”๊ฐ€
    _, pass1, _, _ = cv2.floodFill(pass1, mask1, seed_point, 0, 0, 0, 4) # flood fill

    # ์ด๋ฏธ์ง€ ํŒฝ์ฐฝ(dilation) ์ˆ˜ํ–‰, ๊ฐญ ์‚ฌ์ด์˜ ์˜์—ญ์ด ๋ถ„๋ฆฌ๋จ.
    pass1 = cv2.morphologyEx(pass1, cv2.MORPH_DILATE, ball, anchor=(-1, -1), iterations=1)
    mask2 = cv2.copyMakeBorder(pass1, 1, 1, 1, 1, cv2.BORDER_CONSTANT, 0) # ๊ฒฝ๊ณ„ ์ถ”๊ฐ€

    # ๋‹ค์‹œ ์‹œ๋“œ ํฌ์ธํŠธ๋กœ Flood fill ์ˆ˜ํ–‰ํ•˜์—ฌ ํ•˜๋‚˜์˜ ์ฑ„์›€ ์˜์—ญ ์„ ํƒ
    _, pass2, _, rect = cv2.floodFill(pass2, mask2, seed_point, 0, 0, 0, 4)
    # ์ฑ„์›€ ๊ฒฐ๊ณผ์— ๋Œ€ํ•˜ ์นจ์‹(erosion) ์ˆ˜ํ–‰ํ•˜์—ฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€ ์ฑ„์›€
    pass2 = cv2.morphologyEx(pass2, cv2.MORPH_ERODE, ball, anchor=(-1, -1), iterations=1)

    return pass2
    
def trapped_ball_fill_multi(image:np.ndarray, radius:int, method:str='mean', max_iter:int=1000)->np.ndarray:
    """์—ฌ๋Ÿฌ๋ฒˆ์˜ ๊ฐ‡ํžŒ ๊ณต ์ฑ„์šฐ๊ธฐ(Trapped-Ball Fill) ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜์—ฌ ๋ชจ๋“  ์œ ํšจํ•œ ์˜์—ญ์„ ์ฑ„์šด๋‹ค.

    # Arguments
        image: ํฐ์ƒ‰(๋นˆ ์˜์—ญ) ๋ฐฐ๊ฒฝ์˜ ๊ฒ€์€(์ฑ„์›Œ์ง„) ์„ ํ™”๋ฅผ ๊ฐ€์ง„ ์ด๋ฏธ์ง€
        radius: ๊ตฌํ˜• ๊ตฌ์กฐ ์š”์†Œ์˜ ๋ฐ˜์ง€๋ฆ„
        method: ์ฑ„์šฐ๊ธฐ ๊ฒฐ๊ณผ ํ•„ํ„ฐ๋ง ๋ฐฉ๋ฒ• 
               'max' ๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ ํฐ ๋ฐ˜์ง€๋ฆ„์„ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ™์€ ํฐ ์˜์—ญ์„ ์„ ํƒ
        max_iter: ์ตœ๋Œ€ ๋ฐ˜๋ณต ํšŸ์ˆ˜
    # Returns
        ์ฑ„์šด ํฌ์ธํŠธ ๋ฐฐ์—ด
    """

    unfill_area = image # ์ฑ„์šฐ์ง€ ์•Š์€ ์˜์—ญ์„ ์šฐ๋„ˆ๋ณธ ์ด๋ฏธ์ง€๋กœ ์ดˆ๊ธฐํ™”
    filled_area, filled_area_size, result = [], [], []

    for _ in range(max_iter):
        # ๊ฒฝ๊ณ„์— ๊ฐ€๊นŒ์šด ํฌ์ธํŠธ๋ฅผ ์ œ์™ธํ•œ ํ›„ ๋นˆ ์˜์—ญ์˜ ํฌ์ธํŠธ๋ฅผ ๊ฐ€์ก์˜ด
        points = get_unfilled_point(exclude_area(unfill_area, radius)) 

        # ๋นˆ ํฌ์ธํŠธ๊ฐ€ ์—†์œผ๋ฉด ์ข…๋ฃŒ
        if not len(points) > 0:
            break
        # ์ฒซ ๋ฒˆ์งธ ํฌ์ธํŠธ๋ฅผ ๊ธฐ์ค€์œผ๋กœ Trapped-Ball Fill ์ˆ˜ํ–‰
        fill = trapped_ball_fill_single(unfill_area, (points[0][0], points[0][1]), radius)
        # ์ฑ„์šด ์˜์—ญ๊ณผ ์›๋ณธ ์ด๋ฏธ์ง€์˜ ๋น„ํŠธ AND ์—ฐ์‚ฐ ์ˆ˜ํ–‰
        unfill_area = cv2.bitwise_and(unfill_area, fill)
        # ์ฑ„์šด ์˜์—ญ์˜ ํฌ์ธํŠธ๋ฅผ ์ €์žฅ
        filled_area.append(np.where(fill == 0))
        filled_area_size.append(len(np.where(fill == 0)[0]))

    filled_area_size = np.asarray(filled_area_size) # ์ฑ„์šด ์˜์—ญ์˜ ํฌ๊ธฐ๋ฅผ ๋ฐฐ์—ด๋กœ ๋ฐ›์Œ


    # ํ•„ํ„ฐ๋ง
    """
    ## max
    - ์ฑ„์šด ์˜์—ญ์˜ ํฌ๊ธฐ ์ค‘ ์ตœ๋Œ€๊ฐ’์„ ๊ธฐ์ค€์œผ๋กœ ํ•„ํ„ฐ๋ง, ๊ฐ€์žฅ ํฐ์˜์—ญ์„ ์„ ํƒํ•˜์—ฌ ๊ฒฐ๊ณผ์— ํฌํ•จ, ๊ทธ๋ณด๋‹ค ์ž‘์€ ์˜์—ญ์€ ์ œ์™ธ
    - ์ผ๋ฐ˜์ ์œผ๋กœ ํฐ ๋ฐ˜์ง€๋ฆ„์„ ์‚ฌ์šฉํ•  ๋•Œ ์œ ์šฉํ•˜๋ฉฐ, ๋ฐฐ๊ฒฝ ๊ฐ™์€ ํฐ ์˜์—ญ์„ ์„ ํƒํ•  ๋•Œ ์ ํ•ฉ
    - ์ด ๋ฐฉ๋ฒ•์€ ๋…ธ์ด์ฆˆ๊ฐ€ ์ž‘์€ ์ฑ„์›€ ๊ฒฐ๊ณผ๋ฅผ ๋ฌด์‹œํ•˜๊ณ , ์ฃผ์š” ์ฑ„์šด ์˜์—ญ๋งŒ์„ ์ถ”์ถœํ•˜๊ณ ์ž ํ•  ๋•Œ ์‚ฌ์šฉ

    ## median
    - ์ฑ„์šด ์˜์—ญ์˜ ํฌ๊ธฐ ์ค‘ ์ค‘์•™๊ฐ’์„ ๊ธฐ์ค€์œผ๋กœ ํ•„ํ„ฐ๋ง, ์ค‘์•™๊ฐ’์€ ๋ฐฐ์—ด์„ ์ •๋ ฌํ–ˆ์„ ๋•Œ, ์ค‘๊ฐ„๊ฐ’์— ์œ„์น˜ํ•œ ๊ฐ’
    - ๊ทน๋‹จ์ ์ธ ๊ฐ’์— ๋œ ์˜ํ–ฅ์„ ๋ฐ›๊ธฐ ๋•Œ๋ฌธ์—, ์ด์ƒ์น˜๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ์—๋„ ์•ˆ์ •์ ์ธ ๊ฒฐ๊ณผ๋ฅผ ์ œ๊ณต
    - ๋‹ค์–‘ํ•œ ํฌ๊ธฐ์˜ ์ฑ„์šด ์˜์—ญ์ด ์กด์žฌํ•  ๋•Œ, ์ค‘๊ฐ„ ์ •๋„์˜ ํฌ๊ธฐ๋ฅผ ๊ฐ€์ง„ ์˜์—ญ์„ ์„ ํƒํ•˜๊ณ ์ž ํ•  ๋•Œ ์‚ฌ์šฉ
    - ํฌ๊ธฐ๊ฐ€ ๋งค์šฐ ์ž‘์€ ์˜์—ญ๊ณผ ๋งค์šฐ ํฐ ์˜์—ญ์ด ํ˜ผ์žฌํ•  ๋•Œ, ์ค‘๊ฐ„ ๊ทœ๋ชจ์˜ ์ฑ„์šด ์˜์—ญ์„ ์„ ํƒํ•˜๋Š” ๋ฐ ์œ ์šฉ

    ## mean
    - ์ฑ„์šด ์˜์—ญ์˜ ํฌ๊ธฐ ํ‰๊ท ๊ฐ’์„ ๊ธฐ์ค€์œผ๋กœ ํ•„ํ„ฐ๋ง
    - ๋ชจ๋“  ์ฑ„์šด์˜์—ญ์˜ ํฌ๊ธฐ๋ฅผ ๋”ํ•œ ํ›„, ์ฑ„์šด ์˜์—ญ์˜ ๊ฐœ์ˆ˜๋กœ ๋‚˜๋ˆˆ ๊ฐ’์„ ์‚ฌ์šฉํ•˜์—ฌ ํ•„ํ„ฐ๋ง ๊ธฐ์ค€์„ ์„ค์ •
    - ํ‰๊ท ์ ์ธ ํฌ๊ธฐ๋ฅผ ๊ฐ€์ง„ ์ฑ„์šด ์˜์—ญ์„ ์„ ํƒํ•˜๊ณ ์ž ํ•  ๋•Œ ์‚ฌ์šฉ
    - ์ „์ฒด์ ์ธ ์ฑ„์šด ๊ฒฐ๊ณผ์˜ ํฌ๊ธฐ๊ฐ€ ๋น„์Šทํ•  ๋•Œ ์ ํ•ฉํ•˜๋ฉฐ, ํฌ๊ธฐ๊ฐ€ ํฌ๊ฒŒ ์ฐจ์ด๋‚˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ์— ์œ ์šฉ
    
    """
    if method == 'max':
        area_size_filter = np.max(filled_area_size)
    elif method == 'median':
        area_size_filter = np.median(filled_area_size)
    elif method == 'mean':
        area_size_filter = np.mean(filled_area_size)
    else:
        area_size_filter = 0

    # ํ•„ํ„ฐ๋ง๋œ ์˜์—ญ์˜ ์ธ๋ฑ์Šค๋ฅผ ์ฐพ๋Š”๋‹ค.
    result_idx = np.where(filled_area_size >= area_size_filter)[0]

    # ํ•„ํ„ฐ๋ง ๊ฒฐ๊ณผ ๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€
    for i in result_idx:
        result.append(filled_area[i])

    return result # ์ฑ„์šด ํฌ์ธํŠธ์˜ ๋ฐฐ์—ด ๋ฐ˜ํ™˜

 

๋” ์ž‘์€ ์˜์—ญ์„ ์„ธ๋ถ„ํ™” ํ•˜๋ฉด์„œ ์ž‘์—…ํ•œ๋‹ค.

 

import cv2
import numpy as np


img = cv2.imread("./test.jpg",cv2.IMREAD_GRAYSCALE)
ret, binary = cv2.threshold(image, 220, 255, cv2.THRESH_BINARY)
unfilled = binary
radius = radius

fills = []
for idx, method in enumerate(methods):
   fill = trapped_ball_fill_multi(image=unfilled,radius=3,method=method)
   fills+=fill
   unfilled = mark_fill(unfilled,fills)

   fill = flood_fill_multi(unfilled)
   fills += fill

   fillmap = build_fill_map(unfilled, fills)
   fillmap = merge_fill(fillmap)
   cv2.imshow(str(idx),unfilled)

 

์ด๋ ‡๊ฒŒ ์ฑ„์šด ์˜์—ญ ๋งต์„ id๋ฅผ ์ค˜์„œ ๋ถ„ํ•  ํ•œ๋‹ค.

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์˜์—ญ๋ณ„๋กœ ๊ตฌ๋ถ„์ด ๋œ๋‹ค.

def build_fill_map(image: np.ndarray, fills: list) -> np.ndarray:
    """๊ฐ ํ”ฝ์…€์„ ์ฑ„์šด ์˜์—ญ์˜ ID๋กœ ํ‘œ์‹œํ•˜๋Š” ๋ฐฐ์—ด์„ ์ƒ์„ฑํ•˜๋Š” ํ•จ์ˆ˜.

    # Arguments
        image: ์ฒ˜๋ฆฌํ•  ์ด๋ฏธ์ง€.
        fills: ์ฑ„์šด ์˜์—ญ์˜ ํฌ์ธํŠธ ๋ฐฐ์—ด. ๊ฐ ํฌ์ธํŠธ๋Š” (y, x) ํ˜•ํƒœ์˜ ์ขŒํ‘œ๋ฅผ ํฌํ•จํ•ด์•ผ ํ•จ.
    
    # Returns
        ๊ฐ ํ”ฝ์…€์— ์ฑ„์šด ์˜์—ญ์˜ ID๊ฐ€ ํ‘œ์‹œ๋œ ๋ฐฐ์—ด.
    """
    result = np.zeros(image.shape[:2], np.int_)  # ๊ฒฐ๊ณผ ๋ฐฐ์—ด ์ดˆ๊ธฐํ™”, ๋ชจ๋“  ๊ฐ’์€ 0

    for index, fill in enumerate(fills):
        result[fill] = index + 1  # ์ฑ„์šด ์˜์—ญ์˜ ID๋กœ ํ‘œ์‹œ

    return result  # ๊ฒฐ๊ณผ ๋ฐฐ์—ด ๋ฐ˜ํ™˜

 

๊ทธ๋ฆฌ๊ณ  ์ด ์ฑ„์›€ ๋งต์„ ์˜์—ญ ๋ณ‘ํ•ฉ์„ ํ•˜๋ฉด์„œ ์˜์—ญ ์„ธ๋ถ„ํ™”๋ฅผ ๋ฐ˜๋ณตํ•œ๋‹ค.

def get_bounding_rect(points:np.ndarray)->tuple:
    """์ฃผ์–ด์ง„ ํฌ์ธํŠธ ์ง‘ํ•ฉ์˜ ์ตœ์†Œ ๋ฐ ์ตœ๋Œ€ x,y ์ขŒํ‘œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ฒฝ๊ณ„ ์‚ฌ๊ฐํ˜•์„ ๊ณ„์‚ฐ

    # Arguments
        points: (y,x) ํ˜•ํƒœ์˜ ๋ฐฐ์—ด
    # Returns
        ์‚ฌ๊ฐํ˜• ํ˜•ํƒœ์˜ ๊ฒฝ๊ณ„ ์‚ฌ๊ฐํ˜• ์ขŒํ‘œ
    """
    x1, y1, x2, y2 = np.min(points[1]), np.min(points[0]), np.max(points[1]), np.max(points[0])
    return (x1, y1, x2, y2)

def get_border_bounding_rect(h:int, w:int, p1:tuple, p2:tuple, r:int)->tuple:
    """ ์ฃผ์–ด์ง„ ๊ฒฝ๊ณ„ ์‚ฌ๊ฐํ˜•์— ํŠน์ • ํฌ๊ธฐ์˜ ์—ฌ๋ฐฑ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์œ ํšจํ•œ ๊ฒฝ๊ณ„ ์‚ฌ๊ฐํ˜•์„ ๊ณ„์‚ฐ 
    ๊ฒฝ๊ณ„๊ฐ€ ์ด๋ฏธ์ง€์˜ ํฌ๊ธฐ๋ฅผ ์ดˆ๊ณผํ•˜์ง€ ์•Š๋„๋ก ์กฐ์ •

    # Arguments
        h: ์ด๋ฏธ์ง€์˜ ์ตœ๋Œ€ ๋†’์ด
        w: ์ด๋ฏธ์ง€์˜ ์ตœ๋Œ€ ๋„ˆ๋น„
        p1: ๊ฒฝ๊ณ„ ์‚ฌ๊ฐํ˜•์˜ ์‹œ์ž‘ ์ 
        p2: ๊ฒฝ๊ณ„ ์‚ฌ๊ฐํ˜•์˜ ๋ ์ 
        r: ์—ฌ๋ฐฑ์˜ ๋ฐ˜๊ฒฝ
    # Returns
        ์—ฌ๋ฐฑ์ด ์ถ”๊ฐ€๋œ ๊ฒฝ๊ณ„ ์‚ฌ๊ฐํ˜•์˜ ์ขŒํ‘œ๋ฅผ ๋ฐ˜ํ™˜
    """
    x1, y1, x2, y2 = p1[0], p1[1], p2[0], p2[1]

    x1 = x1 - r if 0 < x1 - r else 0
    y1 = y1 - r if 0 < y1 - r else 0
    x2 = x2 + r + 1 if x2 + r + 1 < w else w
    y2 = y2 + r + 1 if y2 + r + 1 < h else h

    return (x1, y1, x2, y2)

def get_border_point(points:list, rect:tuple, max_height:int, max_width:int)->tuple:
    """์ฃผ์–ด์ง„ ํฌ์ธํŠธ์˜ ๊ฒฝ๊ณ„ ํฌ์ธํŠธ์™€ ๊ทธ ํ˜•ํƒœ๋ฅผ ๊ณ„์‚ฐ 
    ๊ฒฝ๊ณ„ ์‚ฌ๊ฐํ˜•์— ์—ฌ๋ฐฑ์„ ์ถ”๊ฐ€ํ•˜๊ณ , ํ•ด๋‹น ์˜์—ญ์—์„œ ๊ฒฝ๊ณ„ ํ”ฝ์…€์„ ์ถ”์ถœ

    # Arguments
        points: ์ฑ„์šด ์˜์—ญ์˜ ํฌ์ธํŠธ
        rect: ์ฑ„์šด ์˜์—ญ์˜ ๊ฒฝ๊ณ„ ์‚ฌ๊ฐํ˜•
        max_height: ์ด๋ฏธ์ง€ ์ตœ๋Œ€ ๋†’์ด
        max_width: ์ด๋ฏธ์ง€ ์ตœ๋Œ€ ๋„ˆ๋น„
    # Returns
        ๊ฒฝ๊ณ„ ํ”ฝ์…€ ์ขŒํ‘œ์™€ ํฌ์ธํŠธ์˜ ๋‹ค๊ฐํ˜• ํ˜•ํƒœ๋ฅผ ๋ฐ˜ํ™˜
    """
    # Get a local bounding rect.
    border_rect = get_border_bounding_rect(max_height, max_width, rect[:2], rect[2:], 2)

    # Get fill in rect.
    fill = np.zeros((border_rect[3] - border_rect[1], border_rect[2] - border_rect[0]), np.uint8)
    # Move points to the rect.
    fill[(points[0] - border_rect[1], points[1] - border_rect[0])] = 255

    # Get shape.
    contours, _ = cv2.findContours(fill, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    approx_shape = cv2.approxPolyDP(contours[0], 0.02 * cv2.arcLength(contours[0], True), True)

    # Get border pixel.
    cross = cv2.getStructuringElement(cv2.MORPH_CROSS, (3, 3))
    border_pixel_mask = cv2.morphologyEx(fill, cv2.MORPH_DILATE, cross, anchor=(-1, -1), iterations=1) - fill
    border_pixel_points = np.where(border_pixel_mask == 255)

    # Transform points back to fillmap.
    border_pixel_points = (border_pixel_points[0] + border_rect[1], border_pixel_points[1] + border_rect[0])

    return border_pixel_points, approx_shape

def merge_fill(fillmap: np.ndarray, max_iter: int = 10) -> np.ndarray:
    """์ฑ„์šด ์˜์—ญ ๋ณ‘ํ•ฉ ํ•จ์ˆ˜.

    # Arguments
        fillmap: ์ฑ„์šด ์˜์—ญ ID๊ฐ€ ํ‘œ์‹œ๋œ ์ด๋ฏธ์ง€.
        max_iter: ์ตœ๋Œ€ ๋ฐ˜๋ณต ํšŸ์ˆ˜.
    
    # Returns
        ๋ณ‘ํ•ฉ๋œ ์ฑ„์šด ์˜์—ญ ์ด๋ฏธ์ง€.
    """
    max_height, max_width = fillmap.shape[:2]
    result = fillmap.copy()  # ๊ฒฐ๊ณผ ๋ฐฐ์—ด ์ดˆ๊ธฐํ™”

    for i in range(max_iter):

        result[np.where(fillmap == 0)] = 0  # ๋นˆ ์˜์—ญ์€ 0์œผ๋กœ ์„ค์ •

        fill_id = np.unique(result.flatten())  # ํ˜„์žฌ ์ฑ„์šด ์˜์—ญ ID ์ถ”์ถœ
        fills = []

        for j in fill_id:
            point = np.where(result == j)
            fills.append({
                'id': j,
                'point': point,
                'area': len(point[0]),
                'rect': get_bounding_rect(point)
            })

        for j, f in enumerate(fills):
            # ๋ผ์ธ ID๋Š” ๋ฌด์‹œ
            if f['id'] == 0:
                continue

            border_points, approx_shape = get_border_point(f['point'], f['rect'], max_height, max_width)
            border_pixels = result[border_points]
            pixel_ids, counts = np.unique(border_pixels, return_counts=True)

            ids = pixel_ids[np.nonzero(pixel_ids)]
            new_id = f['id']
            if len(ids) == 0:
                # ์ฃผ๋ณ€์— ์ƒ‰์ƒ์ด ๋ณ€๊ฒฝ๋œ ๋ผ์ธ์œผ๋กœ ๋‘˜๋Ÿฌ์‹ธ์ธ ๊ฒฝ์šฐ
                if f['area'] < 5:
                    new_id = 0
            else:
                # ๊ฐ€์žฅ ์ ‘์ด‰์ด ๋งŽ์€ ์˜์—ญ์˜ ID๋กœ ์„ค์ •
                new_id = ids[0]

            # ์กฐ๊ฑด์— ๋”ฐ๋ผ ID ์—…๋ฐ์ดํŠธ
            if len(approx_shape) == 1 or f['area'] == 1:
                result[f['point']] = new_id

            if len(approx_shape) in [2, 3, 4, 5] and f['area'] < 500:
                result[f['point']] = new_id

            if f['area'] < 250 and len(ids) == 1:
                result[f['point']] = new_id

            if f['area'] < 50:
                result[f['point']] = new_id

        # ๋ณ€ํ™”๊ฐ€ ์—†์œผ๋ฉด ์ข…๋ฃŒ
        if len(fill_id) == len(np.unique(result.flatten())):
            break

    return result  # ๋ณ‘ํ•ฉ๋œ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜

 

์ตœ์ข…์ ์œผ๋กœ ๊ธฐ์กด ์„ ํ™” ์ด๋ฏธ์ง€์—์„œ ์„ ํ™”๋ฅผ ์ง€์šฐ๊ณ  ์˜์—ญ๋ณ„๋กœ ๋‹ค๋ฅธ ์ƒ‰์œผ๋กœ ์น ํ•˜์—ฌ ๋ฐ‘์ƒ‰์„ ๋ฐ˜ํ™˜ ํ•œ๋‹ค.

def thinning(fillmap: np.ndarray, max_iter: int = 100) -> np.ndarray:
    """์„  ์˜์—ญ์„ ์ฃผ๋ณ€ ์ฑ„์šด ์ƒ‰์ƒ์œผ๋กœ ์ฑ„์šฐ๋Š” ํ•จ์ˆ˜.

    # Arguments
        fillmap: ์ฑ„์šด ์˜์—ญ ID๊ฐ€ ํ‘œ์‹œ๋œ ์ด๋ฏธ์ง€.
        max_iter: ์ตœ๋Œ€ ๋ฐ˜๋ณต ํšŸ์ˆ˜.

    # Returns
        ์ฑ„์šด ํ›„์˜ ์ด๋ฏธ์ง€.
    """
    line_id = 0  # ๊ธฐ๋ณธ ์„  ID
    h, w = fillmap.shape[:2]  # ์ด๋ฏธ์ง€ ๋†’์ด์™€ ๋„ˆ๋น„
    result = fillmap.copy()  # ๊ฒฐ๊ณผ๋ฅผ ์ €์žฅํ•  ๋ฐฐ์—ด ์ดˆ๊ธฐํ™”

    for iterNum in range(max_iter):
        # ์„ ์˜ ํฌ์ธํŠธ๋ฅผ ์ฐพ๊ธฐ. ํฌ์ธํŠธ๊ฐ€ ์—†์œผ๋ฉด ์ข…๋ฃŒ.
        line_points = np.where(result == line_id)
        if not len(line_points[0]) > 0:
            break

        # ์„ ๊ณผ ์ฑ„์šด ์˜์—ญ ์‚ฌ์ด์˜ ํฌ์ธํŠธ๋ฅผ ์ฐพ๊ธฐ.
        line_mask = np.full((h, w), 255, np.uint8)  # ์„  ๋งˆ์Šคํฌ ์ดˆ๊ธฐํ™”
        line_mask[line_points] = 0  # ์„ ์˜ ํฌ์ธํŠธ๋ฅผ 0์œผ๋กœ ์„ค์ •
        line_border_mask = cv2.morphologyEx(line_mask, cv2.MORPH_DILATE,
                                             cv2.getStructuringElement(cv2.MORPH_CROSS, (3, 3)), anchor=(-1, -1),
                                             iterations=1) - line_mask  # ์„ ์˜ ๊ฒฝ๊ณ„ ํฌ์ธํŠธ ์ฐพ๊ธฐ
        line_border_points = np.where(line_border_mask == 255)  # ๊ฒฝ๊ณ„ ํฌ์ธํŠธ ์ฐพ๊ธฐ

        result_tmp = result.copy()  # ์ž„์‹œ ๊ฒฐ๊ณผ ๋ฐฐ์—ด ์ƒ์„ฑ
        # ๊ฐ ๊ฒฝ๊ณ„ ํฌ์ธํŠธ๋ฅผ ์ฃผ๋ณ€ ์ฑ„์šด ์˜์—ญ์˜ ID๋กœ ์ฑ„์›€
        for i, _ in enumerate(line_border_points[0]):
            x, y = line_border_points[1][i], line_border_points[0][i]

            # ์ฃผ๋ณ€ ํ”ฝ์…€์„ ํ™•์ธํ•˜์—ฌ ์ฑ„์›€
            if x - 1 > 0 and result[y][x - 1] != line_id:
                result_tmp[y][x] = result[y][x - 1]
                continue

            if x - 1 > 0 and y - 1 > 0 and result[y - 1][x - 1] != line_id:
                result_tmp[y][x] = result[y - 1][x - 1]
                continue

            if y - 1 > 0 and result[y - 1][x] != line_id:
                result_tmp[y][x] = result[y - 1][x]
                continue

            if y - 1 > 0 and x + 1 < w and result[y - 1][x + 1] != line_id:
                result_tmp[y][x] = result[y - 1][x + 1]
                continue

            if x + 1 < w and result[y][x + 1] != line_id:
                result_tmp[y][x] = result[y][x + 1]
                continue

            if x + 1 < w and y + 1 < h and result[y + 1][x + 1] != line_id:
                result_tmp[y][x] = result[y + 1][x + 1]
                continue

            if y + 1 < h and result[y + 1][x] != line_id:
                result_tmp[y][x] = result[y + 1][x]
                continue

            if y + 1 < h and x - 1 > 0 and result[y + 1][x - 1] != line_id:
                result_tmp[y][x] = result[y + 1][x - 1]
                continue

        result = result_tmp.copy()  # ์ตœ์ข… ๊ฒฐ๊ณผ ์—…๋ฐ์ดํŠธ

    return result  # ์ฑ„์šด ํ›„์˜ ์ด๋ฏธ์ง€ ๋ฐ˜ํ™˜

def show_fill_map(fillmap: np.ndarray) -> np.ndarray:
    """์ฑ„์šด ์˜์—ญ์„ ์ƒ‰์ƒ์œผ๋กœ ํ‘œ์‹œํ•˜์—ฌ ์‹œ๊ฐํ™”ํ•˜๋Š” ํ•จ์ˆ˜.

    # Arguments
        fillmap: ์ฑ„์šด ์˜์—ญ ID๊ฐ€ ํฌํ•จ๋œ ์ด๋ฏธ์ง€ ๋ฐฐ์—ด.

    # Returns
        ์ƒ‰์ƒ์ด ์ ์šฉ๋œ ์ด๋ฏธ์ง€ ๋ฐฐ์—ด.
    """
    # ๊ฐ ์ฑ„์šด ์˜์—ญ์— ๋Œ€ํ•ด ๋žœ๋ค ์ƒ‰์ƒ ์ƒ์„ฑ
    colors = np.random.randint(0, 255, (np.max(fillmap) + 1, 3))
    # ์„ ์˜ ID๋Š” 0์ด๋ฉฐ, ๊ทธ ์ƒ‰์ƒ์€ ๊ฒ€์ •์ƒ‰์œผ๋กœ ์„ค์ •
    colors[0] = [0, 0, 0]

    return colors[fillmap]  # ์ƒ‰์ƒ์ด ์ ์šฉ๋œ ์ด๋ฏธ์ง€ ๋ฐ˜ํ™˜

fill = show_fill_map(thinning(fillmap))
cv2.imshow("fill",cv2.convertScaleAbs(fill))

 

 

 

GitHub - pupba/Trapped-Ball-Segmentation: Trapped-Ball-Segmentation

Trapped-Ball-Segmentation. Contribute to pupba/Trapped-Ball-Segmentation development by creating an account on GitHub.

github.com

์—ฌ๊ธฐ์„œ ๋ชจ๋“  ์ฝ”๋“œ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

728x90
๋ฐ˜์‘ํ˜•