[OpenCV] 이미지의 외곽선 검출하여 내부 영역 추출하기

Jinjin·2023년 11월 19일
2
post-thumbnail

🔻테두리 추출 API

@app.route('/border-extraction', methods=['POST'])
def borderExtraction():
    #이전에 있던 파일 삭제하기
    removeFile()

    # JSON 데이터 파싱
    roomId = request.form.get('roomId')
    originalFile = request.files['originalFile']
    newFile = request.files['newFile']

    # 파일을 저장할 경로 지정
    originalPath = './borderImages/' + roomId + originalFile.name + '.jpg';
    newPath = './borderImages/' + roomId + newFile.name + '.jpg';

    # 파일을 서버에 저장
    originalFile.save(originalPath)
    newFile.save(newPath)

    # originalFile의 색을 변경(회색 → 검은색)
    colorChangeToBlack(originalPath)

    # 파일에서 테두리 안의 데이터 추출
    borderExtractionTest(roomId, originalPath, newPath)

    # 이미지를 서버에서 삭제
    os.remove(originalPath)
    os.remove(newPath)

    return send_file('./borderImages/'+roomId+'output.jpg', as_attachment=True)
  1. removeFile() 함수를 호출하여 테두리 내부 영역을 추출한 이미지를 저장하려는 폴더에서 이전 파일을 삭제한다.

  2. roomId, originalFile, newFile 데이터를 받아온다.

  • roomId : stomp로 방을 구별할 때 사용한 후, 파일의 이름을 분리하기 위해 다시 사용함

  • originalFile : 원본 테두리 파일

  • newFile : 원본 테두리를 따라 그리고 색칠까지 마친 파일

  1. 파일을 저장할 경로를 지정하고 파일을 서버에 저장한다.

  2. colorChangeToBlack(originalPath) 함수를 호출하여 originalFile에서 그린 원본 테두리의 색(회색)을 검은색으로 변경한다.

  3. borderExtractionTest(roomId, originalPath, newPath) 함수를 호출하여 originalFile의 테두리 내부 영역에 newFile의 이미지를 덮어서 추출한다.

  4. 이미지를 서버에서 삭제함

  5. 추출한 이미지를 반환함(반환한 후, 추출한 이미지는 저장된 상태이므로 해당 API를 시작할 때 removeFile() 함수를 호출하여 파일을 삭제하고 시작했던 것이다)


✅ removeFile() 함수

def removeFile():
    folder_path = './borderImages/'  # 삭제하려는 폴더 경로
    file_to_exclude = 'borderImageData'  # 제외할 파일명

    # 폴더 내 파일들을 리스트업하고 특정 파일을 제외하고 삭제
    for filename in os.listdir(folder_path):
        file_path = os.path.join(folder_path, filename)
        try:
            if os.path.isfile(file_path) and file_to_exclude not in filename:
                os.remove(file_path)
                print(f'{file_path} 파일이 삭제되었습니다.')
        except Exception as e:
            print(f'파일 삭제 중 에러 발생: {e}')

📁borderImages 폴더에서 📝borderImageData 파일을 제외한 나머지 파일을 모두 삭제한다.


✅ colorChangeToBlack(originalPath) 함수

def colorChangeToBlack(originalPath):
    # 이미지 오픈
    image = Image.open(originalPath)

    # 이미지 크기 가져오기
    width, height = image.size

    # 이미지 픽셀 데이터 가져오기
    pixels = list(image.getdata())

    # 흰색이 아닌 모든 픽셀을 검은색 픽셀로 바꾸기
    for i in range(len(pixels)):
        if pixels[i] != (255, 255, 255):
            pixels[i] = (0, 0, 0)

    # 변경된 픽셀 데이터를 이미지로 다시 만들기
    new_image = Image.new('RGB', (width, height))
    new_image.putdata(pixels)

    # 이미지 저장
    new_image.save(originalPath)

[결과]


✅ borderExtractionTest(roomId, originalPath, newPath)

# 테두리 영역 안의 그림만 추출한다.
def borderExtractionTest(roomId, originalPath, newPath):

    originalImage = cv2.imread(originalPath)
    
    if originalImage is None:
        print(f'Unable to load image from {originalPath}')
        exit()

    newImage = cv2.imread(newPath)
    if newImage is None:
        print(f'Unable to load image from {newPath}')
        exit()

    # [originalImage]에서 테두리 영역을 추출
    contours1, _ = cv2.findContours(cv2.inRange(originalImage, (0, 0, 0), (30, 30, 30)), cv2.RETR_EXTERNAL,
                                    cv2.CHAIN_APPROX_SIMPLE)

    # 팽창을 위한 커널 생성
    kernel = np.ones((5, 5), np.uint8)  # 팽창을 위한 5x5 크기의 커널

    # contour 기반으로 마스크 생성 후 팽창 적용
    mask = np.zeros(newImage.shape, dtype=np.uint8)
    cv2.drawContours(mask, contours1, -1, (255, 255, 255), thickness=cv2.FILLED)
    mask = cv2.dilate(mask, kernel, iterations=3)  # iterations 값은 팽창의 강도를 결정

    # [newImage]에서 [originalImage]의 테두리를 기반으로 영역 추출
    result_image = cv2.bitwise_and(newImage, mask)

    # mask 영역의 검은색 부분이 있는 위치를 찾는다.
    black_pixels_in_mask = (mask == [0, 0, 0]).all(axis=2)

    # result_image에서 mask 영역의 검은색 부분에 해당하는 영역에서만 픽셀을 하얀색으로 변경
    result_image[black_pixels_in_mask] = [255, 255, 255]

    # 이미지의 높이와 너비 가져오기
    height, width = result_image.shape[:2]

    # 이미지를 중앙을 기준으로 500x500으로 자르기
    start_row = max(0, int((height - 500) / 2))
    end_row = min(height, start_row + 500)
    start_col = max(0, int((width - 500) / 2))
    end_col = min(width, start_col + 500)

    # 이미지를 500x500으로 자르거나 확장하기
    result_image = result_image[start_row:end_row, start_col:end_col]

    if result_image.shape[0] < 500:  # 세로가 500보다 작으면
        pad_top = (500 - result_image.shape[0]) // 2
        pad_bottom = 500 - result_image.shape[0] - pad_top
        result_image = cv2.copyMakeBorder(result_image, pad_top, pad_bottom, 0, 0, cv2.BORDER_CONSTANT,
                                          value=(255, 255, 255))

    if result_image.shape[1] < 500:  # 가로가 500보다 작으면
        pad_left = (500 - result_image.shape[1]) // 2
        pad_right = 500 - result_image.shape[1] - pad_left
        result_image = cv2.copyMakeBorder(result_image, 0, 0, pad_left, pad_right, cv2.BORDER_CONSTANT,
                                          value=(255, 255, 255))

    # 결과 이미지를 저장
    cv2.imwrite('./borderImages/'+roomId+'output.jpg', result_image)

🔴원본 테두리(originalFile)의 외곽선을 찾는다.

contours1, _ = cv2.findContours(cv2.inRange(originalImage, (0, 0, 0), (30, 30, 30)), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
  • originalImage : 외곽선을 찾을 이미지
  • cv2.inRange(originalImage, (0, 0, 0), (30, 30, 30) : (0,0,0)에서 (30,30,30) 범위에 있는 픽셀을 찾는다.
  • cv2.RETR_EXTERNAL : 가장 바깥 쪽의 외곽선을 찾는다.
  • cv2.CHAIN_APPROX_SIMPLE : 외곽선을 표현하는 방법 중에 하나이다. 이건 외곽선을 압축해서 표현한다.
  • contours1 : 찾은 외곽선 정보를 담고 있는 리스트
  • hierarchy : 계층 정보를 담고 있는 numpy 배열

📌 cv2.CHAIN_APPROX_SIMPLE 방법 외의 외곽선을 검출하는 방법 ❓

  • cv2.CHAIN_APPROX_NONE
    : 외곽선은 근사화하지 않고, 모든 점을 반환한다. 따라서 너무 많은 포인트를 반환해서 메모리를 많이 사용한다.
    [결과]

  • cv2.CHAIN_APPROX_TC89_L1 or cv2.CHAIN_APPROX_TC89_KCOS
    : Teh-Chin 연결 근사화 알고리즘을 사용하여 외곽선을 근사화한다.
    [결과]

  • cv2.CHAIN_APPROX_SIMPLE
    : 가장 가까운 두 점 사이의 직선만 남기고 나머지는 버린다. 대부분의 경우 좋은 근사화 결과를 제공한다.
    [결과]


🟠커널을 생성하여 이미지 팽창하기

 # 팽창을 위한 커널 생성
kernel = np.ones((5, 5), np.uint8)  # 팽창을 위한 5x5 크기의 커널

# contour 기반으로 마스크 생성 후 팽창 적용
mask = np.zeros(newImage.shape, dtype=np.uint8)
cv2.drawContours(mask, contours1, -1, (255, 255, 255), thickness=cv2.FILLED)
mask = cv2.dilate(mask, kernel, iterations=3)  # iterations 값은 팽창의 강도를 결정

# [newImage]에서 [originalImage]의 테두리를 기반으로 영역 추출
result_image = cv2.bitwise_and(newImage, mask)

📌 왜? 이미지를 팽창하는가?
추출한 이미지를 움직이게 만들기 위해서는 검은색 테두리가 보여야하기 때문에 커널을 이용하여 이미지를 팽창함

📌 커널(kernel)이란?
커널은 이미지에서 특정 연산을 수행하기 위해 사용되는 작은 행렬이다. 이 작은 행렬은 주로 패턴, 모양 또는 필터를 정의하며, 이미지의 특정 부분을 분석하거나 변형하기 위해 사용한다.

📌 팽창을 위한 크기를 5x5로 설정한 이유?
크기를 더 크게 늘리면(Ex.10x10) 팽창 연산이 더 넓은 영역에 적용된다. 이로써, 이미지에서 밝은 영역을 더 많이 확장시키고 주변 영역에 더 크게 영향을 미친다. 또한, 값이 크면 연산에 소요되는 계산 비용이 더 높아지고 세부 사항이 더 많이 손실될 수 있으므로 성능을 테스트 한 후 5x5로 적용시켰다.

  • mask = np.zeros(newImage.shape, dtype=np.uint8)
    : newImage의 가로, 세로 크기에 맞춘 형태로 mask를 생성하며 dtype=np.uint8으로 픽셀 값은 0~255사이의 값을 가지도록 했다. 또한, np.zeros 함수로 배경을 모두 검은색으로 색칠했다.
    [결과]

  • cv2.drawContours(mask, contours1, -1, (255, 255, 255), thickness=cv2.FILLED)
    : contours1 의 윤곽선 정보로 mask에 그리며 윤곽선의 내부 영역은 (255, 255, 255)로 하얀색으로 설정했다.
    [결과]

  • mask = cv2.dilate(mask, kernel, iterations=3)
    : cv2.dilate() 함수를 생성하여 mask의 이미지를 kernel 만큼 팽창한다. 또한, iterations을 통해 팽창을 몇 번 반복할 것인지 설정한다.
    iterations 값이 클 수록 팽창 연산이 더 많이 수행되어 영역이 더 확장됨
    [결과]

🟡 newImage를 mask에 덮어씌운다.

  • result_image = cv2.bitwise_and(newImage, mask)
    비트 AND 연산을 통해서 mask의 하얀색 영역에 newImage를 덮어씌운다.
    AND연산은 서로 같은 비트 (1 AND 1 = 1) 인 경우에만 1이므로 여기서는 하얀색 부분(0~255영역)
    [결과]

🟢 검은색 픽셀의 배경을 하얀색으로 바꾼다.

# mask 영역의 검은색 부분이 있는 위치를 찾는다.
black_pixels_in_mask = (mask == [0, 0, 0]).all(axis=2)

# result_image에서 mask 영역의 검은색 부분에 해당하는 영역에서만 픽셀을 하얀색으로 변경
result_image[black_pixels_in_mask] = [255, 255, 255]
  • black_pixels_in_mask = (mask == [0, 0, 0]).all(axis=2)
    : mask에서 [0,0,0] 값 즉, 검은색에 해당하는 픽셀을 찾고 검은색 픽셀에 해당하는 부분을 True로 표시하는 boolean 배열이다. 여기서, mask == [0, 0, 0]).all(axis=2) 코드는 각 픽셀에 대해 [0,0,0]인 픽셀을 찾을 때 R, G, B 모두 검은색인지를 확인한다.

  • result_image[black_pixels_in_mask] = [255, 255, 255]
    : 찾은 배열에서 True로 표시된 부분 즉, 검은색 부분에 해당하는 부분을 하얀색(255, 255, 255)로 변경한다.
    [결과]


🔵 추출한 이미지를 500x500 사이즈로 변경하여 저장한다.

# 이미지의 높이와 너비 가져오기
height, width = result_image.shape[:2]

# 이미지를 중앙을 기준으로 500x500으로 자르기
start_row = max(0, int((height - 500) / 2))
end_row = min(height, start_row + 500)
start_col = max(0, int((width - 500) / 2))
end_col = min(width, start_col + 500)

# 이미지를 500x500으로 자르거나 확장하기
result_image = result_image[start_row:end_row, start_col:end_col]

if result_image.shape[0] < 500:  # 세로가 500보다 작으면
	pad_top = (500 - result_image.shape[0]) // 2
    pad_bottom = 500 - result_image.shape[0] - pad_top
    result_image = cv2.copyMakeBorder(result_image, pad_top, pad_bottom, 0, 0, cv2.BORDER_CONSTANT,
                                          value=(255, 255, 255))

if result_image.shape[1] < 500:  # 가로가 500보다 작으면
     pad_left = (500 - result_image.shape[1]) // 2
     pad_right = 500 - result_image.shape[1] - pad_left
     result_image = cv2.copyMakeBorder(result_image, 0, 0, pad_left, pad_right, cv2.BORDER_CONSTANT,
                                          value=(255, 255, 255))

추출한 이미지에서 가로 또는 세로의 길이가 500보다 작으면 하얀색 배경으로 크기를 확장한다.
500보다 크면 크기를 줄인다.

[결과]

profile
BE Developer

0개의 댓글