어떻게 맡게 되었나?
이번 프로젝트는 교수님의 연구를 돕기 위한 목적으로 진행되었다.
교수님과는 주전공인 "조경학과" 졸업작품을 통해 인연이 있는데, 당시 나는 한국 도시공원 평가 지표 프로젝트 Parkscore를 진행하며 개발을 활용한 경험이 있었다. 이를 계기로 교수님께서 조경과 컴퓨팅을 접목한 연구를 도와달라는 요청을 주셨다.
교수님께서는 대륙 별 경관 사진을 분석하는 연구를 진행 중이었으며, 이를 위해 데이터 수집과 이미지 분석이 필요했다.
특히, 단순한 데이터 수집이 아닌 자동화된 분석 프로세스를 원하셨고, 이를 위해 나는 크롤링부터 분석까지 한 번에 수행하는 파이프라인을 구축하는 것이 더 효율적이라고 판단했다.
교수님도 제안에 긍정적으로 동의해주셨고, 결과적으로 이미지 수집부터 분석까지 자동으로 진행되는 시스템을 개발하게 되었다.
프로젝트 소개
프로젝트의 간단한 소개를 하자면, 다음과 같다.
기존의 경관 연구는 주로 주관적인 해석에 의존하는 경우가 많았지만, 이에 벗어나 양식측정학(Stylometry)과 데이터 분석 기법을 적용하여 이미지의 색상, 명암, 패턴 등을 수치적, 즉 정량적으로 분석하는 방법을 도입하는 것이다.
프로젝트에서 나는 해당 분석 방법에 대한 내용을 자세히 알기보다는 내용을 숙지하고 구현을 하는 데 초점을 맞췄다. 즉, 데이터 수집부터 분석, 저장까지 일괄적으로 처리하는 자동화된 시스템을 구축하는 것에 집중했다.
기능
프로젝트의 핵심 기능은 다음과 같다.
1. 구글 이미지 크롤링: 데이터 수집
Chrome Driver 제어 및 원하는 정보를 얻기 위해 사용되는 selenium 라이브러리를 활용하여 특정 검색어를 기반으로 구글 이미지에서 각 대륙별 경관 이미지를 자동으로 수집
2. 이미지 분석
수집된 데이터를 통해 시각적 특징을 정량적으로 측정하는 방법론을 사용하여 분석을 진행
- 색상 분석 : 각 이미지의 RGB 색상 분포를 3차원 공간에서 분석 및 색상이 얼마나 다양하게 사용되었는지를 수치화
- 프랙탈 차원 분석 : 어떤 공간에서 특정 패턴이 얼마나 조밀하게 채워지는가를 나타내는 비율인 프랙탈 차원을 이용하여 색상이 얼마나 넓고 균일하게 분포하는지를 측정하여 색상의 다양성을 분석
- 거칠기(명암 대비) 분석 : 밝기 정보를 기반으로 이미지를 3D 지형처럼 변환하여 밝은 부분을 높게, 어두운 부분을 낮게 설정하여 표면 거칠기를 측정. 거칠기 값이 높을 수록 명암 대비가 강한 이미지
- 색상 군집 분석 : K-Means 알고리즘을 사용하여 주요 색상을 5개로 그룹화하여 가장 많이 사용된 색상을 파이차트로 시각화하여 색상 대비 분석
3. 결과 저장
각 방법론을 이용한 분석 결과를 단순히 터미널에서 출력하는 것만으로는 관리가 어렵다고 판단해서 교수님이 연구 데이터를 체계적으로 관리할 수 있도록 결과를 csv 파일로 저장하는 방식을 도입했다.
csv 파일 같은 경우는 각 대륙별 분석 결과를 개별 저장한 파일과 모든 대륙을 종합한 파일을 추가로 생성했다.
크롤링 초보의 어려웠던 점
이번 프로젝트를 통해 크롤링을 처음으로 해봤다. 단순히 텍스트가 아닌 파일 형식이 이미지이다보니 나에게 좀 더 어렵게 느껴졌다.
구글을 통한 이미지 크롤링을 구현하면서 느낀 어려움은 다음과 같다.
- Selenium에서 개발자 도구를 통해 적절한 CSS Selector를 찾는 과정
- 이미지 다운로드 실패시, 설정한 수집 개수를 맞추도록 하는 로직을 구현하는 과정
1. Selenium을 활용한 크롤링 요소 선택의 어려움
처음에는 단순히 Selenium을 이용해서 검색어를 입력하면 간단하게 이미지 요소를 가져올 수 있을 거라고 생각했다. 하지만, 실제 크롤링을 구현하면서 구글 이미지 검색 결과 페이지에서 특정한 CSS Selector 또는 XPath를 찾아야 한다는 점을 알게 되었다.
특히, 고화질 이미지를 확보하기 위해서는 단순한 썸네일이 아닌 원본 이미지를 크롤링해야했다.
이를 위해 썸네일 이미지를 먼저 클릭한 후 원본 이미지를 추출하는 방식을 적용했다.
썸네일 클릭 후 원본 이미지 크롤링 구현
기본적으로 구글 검색 결과에는 썸네일 이미지가 먼저 표시되며, 썸네일을 클릭해야 원본 이미지를 가져올 수 있다.
썸네일 클릭을 위한 <div>는 CSS Selector를 사용했고, 원본 이미지를 가져오기 위한 <img>는 XPATH 방식을 사용했다. 아래는 이와 관련된 코드이다.
# 썸네일 이미지 찾기
image_elements = self.driver.find_elements(By.CSS_SELECTOR, ".H8Rx8c")
# 썸네일 클릭 후 원본 이미지 가져오기
self.driver.execute_script("arguments[0].click();", image_elements[i])
time.sleep(2) # 로딩 대기
# 원본 이미지 가져오기
original_img = self.driver.find_element(By.XPATH, "//img[contains(@class, 'FyHeAf')]")
image_url = original_img.get_attribute("src")
구현 과정은 다음과 같다.
1️⃣ find_elements(By.CSS_SELECTOR, ".H8Rx8c")를 사용해 썸네일 이미지 찾기
2️⃣ .click()을 실행하여 썸네일을 클릭하여 원본 이미지를 표시
3️⃣ find_element(By.XPATH, "//img[contains(@class, 'FyHeAf')]")를 사용해 원본 이미지의 URL을 가져옴
여기서 'FyHeAf' 같은 경우는 개발자 도구를 통해 확인할 수 있다.
2. 원하는 개수만큼 이미지가 크롤링되지 않은 문제
크롤링을 진행할 때, 특정 이유로 인해 설정한 개수만큼 이미지를 다운로드하지 못하는 문제가 발생했다. 예를 들어, 일부 이미지가 로딩되지 않았거나, 접근할 수 없다는 403 에러가 나왔다. 이로 인해 설정한 개수 보다 적은 이미지가 저장되었다. 즉 100개의 이미지를 크롤링하려고 했는데 만약 1개가 누락되면 최종적으로 99개의 이미지만 다운로드된다는 것이다.
이를 해결하기 위해 다음 이미지로 넘어가서 설정한 개수를 맞추도록 구현했다.
print(f"=== 이미지 수집 시작: {continent} ===")
download_cnt = 0
i = 0
# 썸네일 클릭 후 원본 이미지 다운로드
while download_cnt < self.count and i < len(image_elements):
try:
# 썸네일 클릭
self.driver.execute_script("arguments[0].click();", image_elements[i])
time.sleep(2)
# 원본 이미지 찾기 (XPath 방식)
original_img = self.driver.find_element(By.XPATH, "//img[contains(@class, 'FyHeAf')]")
image_url = original_img.get_attribute("src")
# 파일 저장
image_filename = f"{download_cnt+1:04d}.jpg"
image_path = os.path.join(save_dir, image_filename)
# 이미지 다운로드 시도
success = False
for attemp in range(self.max_retries):
try:
urllib.request.urlretrieve(image_url, image_path)
print(f"다운로드 완료: {image_path}")
success = True
break
except Exception as e:
print(f"{image_url} 다운로드 실패 (시도 {attemp+1}/{self.max_retries}): {e}")
if success:
download_cnt += 1
except Exception as e:
print(f"썸네일 {i} 클릭 또는 원본 이미지 추출 실패: {e}")
# 실패시 다음 썸네일로 이동
i += 1
print(f'=== 이미지 수집 종료 / 총 다운로드: {download_cnt}/{self.count} ===')
self.driver.quit()
1️⃣ 다운로드 재시도 및 개수 맞추기
원본 이미지를 가져온 후 urllib.request.urlretrieve()를 사용하여 저장하는데, 다운로드 실패 시 최대 재시도 횟수(self.max_retries)만큼 재시도를 하고 실패하면 다음 이미지로 넘어가게 구현
2️⃣ 다운로드가 성공하면 개수를 증가, 실패하면 다음 이미지로 이동
다운로드가 성공적으로 완료되었을 경우 download_cnt를 증가시키고 설정된 개수(self.count)만큼 채울 때까지 반복
3️⃣ 설정된 개수만큼 다운로드될 때까지 다음 이미지 시도
다운로드 개수가 self.count보다 작다면 i를 증가시켜 다음 이미지로 이동하여 계속 크롤링 수행
사용자 입장에서 생각해보기
최적화를 위해 적용한 기법
프로젝트의 핵심 목표는 교수님이 연구에 활용할 데이터를 빠르게 제공하는 것이다.
교수님이 바쁘시기 때문에 전반적인 실행 속도를 최적화하여 결과를 신속하게 제공해야 한다고 판단했다.
이를 위해 비동기 처리와 멀티 프로세싱을 도입하여 시간을 대폭 단축하였다.
성능 최적화 비교
아래는 성능 최적화 전후의 실행 결과를 비교한 것이다.
실행 과정 | 최적화 전(이미지 700개 기준) | 최적화 후(이미지 700개 기준) | 차이 |
크롤링 시간 | 1,597.41초 (약 26분) | 239.58초 (약 4분) | 6.6배 단축 |
분석 시간 | 180.36초 (약 3분) | 43.33초 (약 43초) | 4.2배 단축 |
총 실행 시간 | 1,777.83초 (약 30분) | 282.94초 (약 4.7분) | 6.3배 단축 |
크롤링: 비동기 처리 적용
크롤링은 구글 서버에 요청을 보내고 응답을 받는 네트워크 I/O 작업이므로, CPU보다는 대기 시간이 많은 I/O Bound 작업에 해당한다.
이를 최적화하기 위해 Python의 asyncio를 활용하여 비동기 처리를 진행했다.
여러 대륙의 이미지를 동시에 요청하여 병렬적으로 크롤링을 수행할 수 있었고, 한 대륙의 크롤링이 끝나면 즉시 분석을 시작하여 대기 시간을 최소화할 수 있었다.
수행 시간이 26분에서 4분으로 감소하여 약 6.6배 단축할 수 있었다.
이미지 분석: 멀티 프로세싱 적용
이미지 분석 작업은 색상 분석, 프랙탈 차원 분석, K-Means 등을 수행하는 CPU Bound 작업이다.
즉, 네트워크 요청이 아니라 CPU 연산이 집중적으로 발생하는 작업이므로, 멀티 프로세싱을 적용하여 성능을 향상시켰다.
Python의 multiprocessing.Pool을 활용하여 병렬 처리를 하였고 각 대륙의 이미지 분석을 독립적은 프로세스로 처리하여 수행 시간이 23분에서 43초로 감소하여 약 4.2배 단축할 수 있었다.
유지보수성 및 확장성을 고려한 설계
최적화뿐만 아니라 추후 기능 확장이나 유지보수가 용이하도록 객체 지향적으로 설계했다.
각 기능을 독립적인 클래스로 분리하여 모듈화(ImageCrawler, ImageAnalyzer, ImageProcessor)하였는데, 처음부터 체계적으로 설계한 이유는 교수님이 연구를 진행하면서 새로운 분석 기법을 추가할 가능성이 높다고 판단했기 때문에 추후에 수정사항이 발생했을 때 어려움이 없게 하기위해서이다.
첫 외주, 좋은 경험이었다.
이번 프로젝트는 내가 처음으로 진행한 외주 프로젝트이다.
실제 클라이언트의 요구사항에 맞게 구현을 해야하다보니 느낀 점은 다음과 같다.
요구사항을 빠르게 이해하고, 해결책을 제시하는 능력
처음 교수님이 요청하셨을 때, 단순히 이미지를 크롤링해서 저장하는 정도의 작업을 기대하셨을 수도 있다. 하지만 교수님의 연구 목적을 고려했을 때 크롤링 이후에도 분석 과정까지 자동화하는 것이 훨씬 효율적이라고 판단했다. 결과적으로 크롤링과 분석을 하나의 파이프라인으로 구축하면서 연구 효율이 높아졌다고 클라이언트인 교수님이 말해주셨다.
이때 느낀 점은 단순히 요청을 그대로 수행하는 것이 아니라 더 나은 방법을 제안하는 것도 중요하다고 생각했다.
클라이언트의 입장에서 생각하기
이번 프로젝트에서 가장 중요했던 것은 교수님이 원하는 결과를 빠르게 제공하는 것이다.
연구 데이터를 활용하는 입장에서 분석 결과를 얻기까지 너무 오래 걸리면 효율성이 떨어질 수 있기 때문이다.
그래서 비동기 처리와 멀티 프로세싱을 도입하여 실행 시간을 크게 단축했고, 교수님에게 직접 비동기 처리 전후의 실행 시간을 보여드려 이에 만족하셨다. 클라이언트가 원하는 것이 무엇인지 정확히 이해하고, 최적의 솔루션을 제공하는 것도 중요하다는 것을 느꼈다.
GitHub - Kyeong6/landscape_analysis: 경관(Landscape Scenery)을 수치화하여 정량적인 지표 설정 및 분석 자동
경관(Landscape Scenery)을 수치화하여 정량적인 지표 설정 및 분석 자동화 프로젝트 - Kyeong6/landscape_analysis
github.com
'Project' 카테고리의 다른 글
[GemmaSprint] 키워드 추출 및 Text-to-SQL (2) | 2024.09.27 |
---|---|
[ParkScore] 한국 도시공원 평가 지표 프로젝트 (1) | 2024.07.11 |