@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)
removeFile()
함수를 호출하여 테두리 내부 영역을 추출한 이미지를 저장하려는 폴더에서 이전 파일을 삭제한다.
roomId
, originalFile
, newFile
데이터를 받아온다.
roomId
: stomp로 방을 구별할 때 사용한 후, 파일의 이름을 분리하기 위해 다시 사용함
originalFile
: 원본 테두리 파일
newFile
: 원본 테두리를 따라 그리고 색칠까지 마친 파일
파일을 저장할 경로를 지정하고 파일을 서버에 저장한다.
colorChangeToBlack(originalPath)
함수를 호출하여 originalFile
에서 그린 원본 테두리의 색(회색)을 검은색으로 변경한다.
borderExtractionTest(roomId, originalPath, newPath)
함수를 호출하여 originalFile
의 테두리 내부 영역에 newFile
의 이미지를 덮어서 추출한다.
이미지를 서버에서 삭제함
추출한 이미지를 반환함(반환한 후, 추출한 이미지는 저장된 상태이므로 해당 API를 시작할 때 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
파일을 제외한 나머지 파일을 모두 삭제한다.
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)
[결과]
# 테두리 영역 안의 그림만 추출한다.
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)
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
값이 클 수록 팽창 연산이 더 많이 수행되어 영역이 더 확장됨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]
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)로 변경한다.
[결과]
# 이미지의 높이와 너비 가져오기
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보다 크면 크기를 줄인다.
[결과]