본문 바로가기

Programming/Python

[Python] 파이썬에서 쓰레드 이용하여 이미지 병렬 처리

 

오늘은 파이썬에서 쓰레드(Thread)를 이용하여 이미지 영상처리 기법을 병렬로 처리하는 방법을 알아보겠습니다. 

 

Thread란?

**Thread(쓰레드)**는 컴퓨터 프로그램에서 실행되는 가장 작은 단위입니다. 하나의 프로세스(작업 단위)는 여러 쓰레드를 포함할 수 있으며, 이들 쓰레드는 동일한 메모리 공간을 공유하면서 독립적으로 실행될 수 있습니다. 쓰레드는 경량 프로세스(lightweight process)라고도 하며, 다중 작업을 수행할 때 사용됩니다.

쓰레드의 특징

  1. 동시성 (Concurrency):
    • 여러 쓰레드가 동일한 프로세스 내에서 독립적으로 실행될 수 있습니다.
    • 이를 통해 프로그램은 동시성을 가지며, 동시에 여러 작업을 수행할 수 있게 됩니다.
  2. 메모리 공유:
    • 동일한 프로세스 내의 쓰레드는 메모리와 자원을 공유합니다. 예를 들어, 전역 변수나 프로세스의 메모리 공간에 접근할 수 있습니다. 이로 인해 쓰레드 간에 데이터를 쉽게 공유할 수 있지만, 동시에 동기화 문제가 발생할 수 있습니다.
  3. 경량성:
    • 쓰레드는 프로세스보다 생성과 종료가 빠르고, 컨텍스트 전환이 적은 비용으로 이루어집니다. 따라서 쓰레드를 사용하면 보다 가벼운 방식으로 병렬 작업을 수행할 수 있습니다.
  4. 멀티스레딩:
    • 멀티스레딩은 하나의 프로세스 내에서 여러 쓰레드를 동시에 실행하여 작업을 병렬로 처리하는 기술입니다. 이 방법은 프로그램의 성능을 높이고, 응답성을 개선하는 데 유용합니다.

파이썬에서 병렬처리

  • 파이썬에서는 배열 처리를 순차적으로 진행하기 때문에, Thread를 사용하여 배열을 병렬로 처리할 수 있습니다. 
  • 이는 여러 이미지의 처리 작업을 병렬로 실행하여 전체 처리 시간을 단축시킬 수 있습니다. 

 

파이썬에서 이미지 병렬처리를 위한 코드는 아래와 같습니다.

아래 코드는 Python에서 병렬 처리를 이용해 이미지 배열을 처리하는 방법을 보여줍니다.

이미지 처리는 예시를 위해 컬러 영상을 불러와서 흑백으로 변환하는 코드를 보여드리겠습니다. 

 

import glob
import cv2
import numpy as np
import concurrent.futures
from tqdm.notebook import tqdm
import matplotlib.pyplot as plt
from datetime import datetime

# 이미지 데이터 path 불러오기 
image_dir = './data'	# 이미지 데이터 directory
image_list = glob.glob(f'{image_dir}/*.png')	# 이미지 데이터 path 리스트 
print(len(image_list)) # 전체 이미지 개수

# 쓰레드를 진행할 영상 처리 함수
def data_load_and_preprocessing(index):
    image_path = image_list[index]
    image = cv2.imread(image_path)
    image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    return image

# 시간 측정해보기 (쓰레드 전 시작시간)
startTime = datetime.now()
print('Start Time : {0}'.format(startTime))

# 쓰레드 진행
with concurrent.futures.ThreadPoolExecutor() as executor:
    image_array = list(tqdm(executor.map(data_load_and_preprocessing, range(len(image_list))), total=len(image_list)))
image_array = np.array(image_array)				# 데이터 로드 후 전처리 결과
print(image_array.shape)						# 데이터 shape

# 시간 측정해보기 (쓰레드 후 종료시간)
endTime = datetime.now()
runTime = endTime - startTime
print('End Time : {0}'.format(endTime))
print('Running Time : ' + str(runTime))

 

해당 코드는 각 이미지를 data_load_and_preprocessing라는 함수로 처리하며, ThreadPoolExecutor를 사용하여 여러 스레드에서 동시에 작업을 수행합니다.

 

세부적으로 살펴보면:

  1. concurrent.futures.ThreadPoolExecutor:
    • Python의 표준 라이브러리 concurrent.futures에 포함된 ThreadPoolExecutor는 여러 작업을 병렬로 실행할 수 있도록 도와주는 클래스입니다.
    • 이 클래스는 여러 개의 스레드를 사용해 작업을 병렬로 처리할 수 있게 해줍니다.
  2. executor.map:
    • map 메서드는 제공된 함수 ( data_load_and_preprocessing  )를 각 입력에 대해 호출합니다.
    • 여기서는 range(len(image_list))가 함수의 인자로 사용됩니다.
    • 이 함수는 data_load_and_preprocessing  를 image_list의 각 인덱스에 대해 호출하는 작업을 여러 스레드에 분배하여 병렬로 처리합니다.
    • 인덱스는 오름차순 순서대로 진행됩니다. (0,1,2,3,4~~~)
    • 즉, data_load_and_preprocessing  함수는 image_list의 각 이미지에 대해 병렬로 실행됩니다.
  3. image_array:
    • image_array는 executor.map으로부터 반환된 결과를 리스트로 변환한 것입니다.
    • 이 리스트는 각 data_load_and_preprocessing 호출의 반환 값을 포함합니다.

요약하면, 이 코드는 image_arr에 있는 여러 이미지를 병렬로 처리하면서 그 진행 상황을 tqdm을 통해 시각적으로 보여주는 것입니다. 병렬 처리 덕분에 전체 이미지 처리 시간이 줄어들 수 있습니다.

 

결과 그림을 확인해보면 100개의 데이터를 로드하고 전처리(흑백처리)를 진행하는데 2.77초가 걸린 것을 확인할 수 있습니다.


 

그렇다면 쓰레드 처리하지 않고 순차적으로 데이터를 처리하면 어떤 결과가 나타날까요 ?

아래코드는 data_load_and_preprocessing() 함수와 마찬가지로 데이터를 로드하고 전처리(흑백처리)를 진행합니다. 

그러나 병렬처리를 진행하지 않고, 순차적으로 진행하게 됩니다.

startTime = datetime.now()
print('Start Time : {0}'.format(startTime))

image_array = []

for index in tqdm(range(len(image_list))):
    image_path = image_list[index]
    image = cv2.imread(image_path)
    image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    
    image_array.append(image)
    

image_array = np.array(image_array)
print(image_array.shape)

endTime = datetime.now()
runTime = endTime - startTime
print('End Time : {0}'.format(endTime))
print('Running Time : ' + str(runTime))

 

쓰레드 없이 순차적으로 데이터 로드 및 전처리를 진행한 결과는 아래와 같습니다. 

 

결과 그림을 확인해보면 쓰레드를 사용하지 않으면 100개의 데이터를 로드하고 전처리(흑백처리)를 진행하는데 4.08초가 걸린 것을 확인할 수 있습니다.