2025. 1. 15. 14:36ㆍc,c++
아래 내용은 이미지/영상 처리 파이프라인을 C++과 Python 각각으로 구현하고, 멀티스레드를 적용해 성능 차이를 비교해보기 위한 예시 가이드입니다. 실제 구현 시에는 상황에 맞춰 코드를 조정하거나, 더 복잡한 기능(예: 객체 검출, 딥러닝 추론 등)을 추가할 수 있습니다. 여기서는 간단한 예시로써 프레임 단위로 블러(blur) 처리를 적용하는 코드를 살펴보고자 합니다.
1. 파이프라인 개요
- 영상 소스 준비
- 예: mp4 동영상 파일
- 고화질(예: 1920×1080)로 준비하면 CPU 사용량이 더 커서 성능 차이를 확인하기 쉬움
- 프레임 단위 읽기
- OpenCV를 사용해 동영상 파일로부터 한 프레임씩 읽어옴
- 이미지 처리(블러 등)
- 단순 예시: cv::GaussianBlur (C++) / cv2.GaussianBlur (Python)
- GPU가 아닌 CPU에서만 처리하도록 설정
- 멀티스레드를 적용해 여러 프레임(또는 영상의 구간)을 나눠서 병렬 처리
- 처리 시간 / CPU 사용률 / 메모리 사용량 측정
- C++: <chrono> 라이브러리 사용
- Python: time 혹은 timeit 등으로 측정
- 시스템 차원에서 CPU 점유율이나 메모리는 htop(리눅스) 또는 Windows Task Manager 등으로 관찰 가능
- 스레드 개수 변화
- 1개, 2개, 4개, 8개 등으로 스레드 수를 바꿔가며 측정
- 프레임 처리 속도(FPS) 또는 전체 처리 시간(초) 비교
2. C++ 예시
아래 코드는 간단히 영상의 모든 프레임에 블러를 적용한 후, 결과를 저장하지 않고(또는 별도 처리 없이) 처리하는 예시입니다. 실제로는 처리된 영상을 cv::VideoWriter로 저장하거나, 별도의 로직을 추가할 수 있습니다.
#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
#include <thread>
#include <chrono>
void processFrames(const cv::Mat& inputFrame, cv::Mat& outputFrame) {
// 예시: 가우시안 블러
cv::GaussianBlur(inputFrame, outputFrame, cv::Size(15, 15), 0);
}
int main() {
// 동영상 파일 열기
cv::VideoCapture cap("test_video.mp4");
if (!cap.isOpened()) {
std::cerr << "Failed to open video." << std::endl;
return -1;
}
// 총 프레임 수 및 fps
int totalFrames = static_cast<int>(cap.get(cv::CAP_PROP_FRAME_COUNT));
double fps = cap.get(cv::CAP_PROP_FPS);
std::cout << "Total Frames: " << totalFrames << std::endl;
std::cout << "FPS: " << fps << std::endl;
// 모든 프레임을 미리 메모리에 로드 (메모리 사용량 많아질 수 있음)
std::vector<cv::Mat> frames(totalFrames);
for (int i = 0; i < totalFrames; ++i) {
cap >> frames[i];
if (frames[i].empty()) break;
}
cap.release();
// 결과 저장할 벡터
std::vector<cv::Mat> processedFrames(totalFrames);
// 스레드 수 설정 (예: 4개)
int numThreads = 4;
// 타이머 시작
auto start = std::chrono::high_resolution_clock::now();
// 처리 함수 (람다식) - startIdx부터 endIdx-1까지 처리
auto worker = [&](int startIdx, int endIdx) {
for (int i = startIdx; i < endIdx; ++i) {
if (!frames[i].empty()) {
processFrames(frames[i], processedFrames[i]);
}
}
};
// 스레드를 담아둘 벡터
std::vector<std::thread> threads;
threads.reserve(numThreads);
// 할당할 작업량 계산
int chunkSize = totalFrames / numThreads;
int remainder = totalFrames % numThreads;
int currentStart = 0;
for (int t = 0; t < numThreads; ++t) {
int currentEnd = currentStart + chunkSize;
// 나머지 프레임이 있으면 분배
if (t < remainder) {
currentEnd += 1;
}
threads.emplace_back(worker, currentStart, currentEnd);
currentStart = currentEnd;
}
// 모든 스레드 합류
for (auto &th : threads) {
if (th.joinable()) {
th.join();
}
}
// 타이머 종료
auto end = std::chrono::high_resolution_clock::now();
double elapsedSec = std::chrono::duration<double>(end - start).count();
std::cout << "Processing time: " << elapsedSec << " seconds" << std::endl;
return 0;
}
C++ 코드 설명
- 동영상 로드
- cv::VideoCapture cap("test_video.mp4");
- 전체 프레임 수, FPS 등 메타정보 얻기
- 프레임 일괄 로드
- std::vector<cv::Mat> frames(totalFrames);
- 동영상에서 각 프레임을 벡터에 저장
- 멀티스레드 적용
- std::thread를 여러 개 생성
- 각 스레드는 일부 구간(startIdx~endIdx)의 프레임만 처리
- 처리 함수
- processFrames()에서 cv::GaussianBlur를 적용
- 시간 측정
- <chrono>로 시작~종료 시점까지 걸린 시간 계산
- 결과 저장
- 여기서는 단순히 processedFrames 벡터에 저장하고 끝.
실제로는 cv::VideoWriter를 통해 영상 파일로 저장 가능
- 여기서는 단순히 processedFrames 벡터에 저장하고 끝.
3. Python 예시
아래 예시는 기본적으로 같은 로직을 Python에 옮긴 코드입니다. Python은 GIL(Global Interpreter Lock) 때문에 CPU-bound 연산의 경우, 멀티스레드를 쓰더라도 스레드 간에 실제 병렬 실행이 아닌 시분할(Thread Switching) 방식이 될 수 있습니다.
따라서 멀티프로세싱을 사용하거나, 내부적으로 C/C++로 작성된 NumPy/ OpenCV 부분이 병렬 처리되도록 기대할 수 있습니다.
import cv2
import time
import multiprocessing
def process_frame(frame):
# 가우시안 블러
return cv2.GaussianBlur(frame, (15, 15), 0)
def worker(frames_slice, output_dict, index):
processed = []
for f in frames_slice:
if f is not None:
processed.append(process_frame(f))
else:
processed.append(None)
output_dict[index] = processed
if __name__ == "__main__":
cap = cv2.VideoCapture("test_video.mp4")
if not cap.isOpened():
print("Failed to open video.")
exit(1)
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
fps = cap.get(cv2.CAP_PROP_FPS)
print("Total Frames:", total_frames)
print("FPS:", fps)
# 모든 프레임 메모리에 로드
frames = []
for _ in range(total_frames):
ret, f = cap.read()
if not ret:
frames.append(None)
else:
frames.append(f)
cap.release()
# 멀티프로세싱: CPU 코어 수에 맞춰 설정
num_processes = 4
start_time = time.time()
# 나눠 처리할 구간 계산
chunk_size = total_frames // num_processes
remainder = total_frames % num_processes
processes = []
manager = multiprocessing.Manager()
output_dict = manager.dict()
current_start = 0
for i in range(num_processes):
current_end = current_start + chunk_size
if i < remainder:
current_end += 1
slice_frames = frames[current_start:current_end]
p = multiprocessing.Process(target=worker, args=(slice_frames, output_dict, i))
processes.append(p)
p.start()
current_start = current_end
for p in processes:
p.join()
# output_dict에 각 프로세스별 결과가 저장됨
# 최종적으로 합치려면 index 순서대로 병합
processed_frames = []
for i in range(num_processes):
processed_frames.extend(output_dict[i])
elapsed_time = time.time() - start_time
print("Processing time:", elapsed_time, "seconds")
Python 코드 설명
- 동영상 로드
- cv2.VideoCapture("test_video.mp4")
- 모든 프레임을 리스트 frames에 저장
- 멀티프로세싱
- multiprocessing.Process를 여러 개 띄우고, 각 프로세스가 일부 프레임 구간을 처리
- 결과는 multiprocessing.Manager().dict()를 통해 공유 영역에 저장
- 처리 함수
- process_frame()에서 cv2.GaussianBlur 적용
- 결과 병합
- 프로세스별로 나뉘어 처리된 데이터를 모아서 processed_frames에 확장
- 시간 측정
- time.time() 사용
4. 성능 비교 아이디어
- 스레드(프로세스) 개수 변화
- C++: 1, 2, 4, 8개 등으로 numThreads를 바꿔가며 테스트
- Python: num_processes를 1, 2, 4, 8 등으로 바꿔가며 측정
- 다양한 해상도 / 다양한 영상 길이
- HD(1280×720), Full HD(1920×1080), 4K(3840×2160) 등
- 1분짜리, 10분짜리 등 영상 길이도 달리 테스트
- 처리 알고리즘 변경
- 단순 블러 → 영상 내 객체 탐지, 얼굴 인식, 에지 검출 등 CPU 부담이 큰 작업
- OpenCV DNN 모듈 등을 이용해 딥러닝 추론 테스트
- 프로파일링
- C++: perf, gprof, Visual Studio Performance Tools 등
- Python: cProfile, line_profiler, py-spy 등
- 결과 정리
- 총 처리 시간(초)
- 평균 FPS(= 총 프레임 수 / 총 처리 시간)
- CPU 사용률(%)
- 메모리 사용량(MB)
5. 주의사항
- 메모리 사용량: 모든 프레임을 한꺼번에 메모리에 읽어들이면 시스템 메모리 많이 소모 가능. 상황에 따라 스트리밍 방식(프레임 하나씩 읽고 처리 후 저장)을 써야 할 수도 있음.
- 디버깅 난이도: 멀티스레딩/멀티프로세싱은 디버그가 복잡하므로, 간단한 버전부터 차근차근 확장
- GPU 활용: CUDA, OpenCL 등의 GPU 가속을 사용하면 CPU-바운드 성능 비교와는 또 다른 결과가 나올 수 있음
- GIL: Python은 GIL 때문에 CPU 연산을 멀티스레드로 병렬 처리하기 어렵습니다. 멀티프로세싱을 활용하거나, C/C++ 라이브러리 자체에서 멀티스레딩을 해주는 방식을 사용해야 합니다.
C++ (1,4,8,16)
Python (1,4,8,16)
360p영상 | C++(스레드) | Python(멀티프로세싱) |
1개 | 0.233199 | 1.980214 |
4개 | 0.255602 | 1.808919 |
8개 | 0.199378 | 1.896996 |
16개 | 0.182297 | 1.727693 |
C++과 파이썬은 거의 10배의 속도차이가 났다. 하지만 각각 스레드나 프로세스의 개수에 따라 유의미한 큰차이는 나지 않았다.
4k영상 | C++(스레드) | Python(멀티프로세싱) |
1개 | 1.14439 | 17.99318814277649 |
16개 | 1.14424 | 14.656228303909302 |
4k영상을 돌려봤을때 C++과 Python의 차이는 더욱 커졌다. 그리고 C++의 멀티스레드 같은 경우엔 1개나 16개나 차이가 나지 않았지만 Python의 멀티 프로세싱은 3초라는 큰 차이가 났다.
왜 Python은 이렇게 차이 나는가?
- 네이티브 코드 vs. 인터프리터: C++은 컴파일된 바이너리를 직접 실행하는 반면, Python은 인터프리터가 중간에서 동작.
- 멀티프로세스 오버헤드: Python에선 “스레드”가 아니라 “프로세스”를 늘려야 GIL을 우회 → 프로세스 생성/통신 비용이 큼.
- 데이터 구조/관리: Python의 자료 구조(list, dict, pickle 등)은 C++ STL에 비해 추상화 비용이 더 커서, 대량의 데이터(예: 4K 프레임)를 처리할 때 오버헤드가 쌓임.
전체 결론
- C++ 측
- 가우시안 블러 같은 영상 처리 연산은 내부적으로 매우 최적화된 네이티브 코드여서, 1스레드만으로도 1.14초 정도면 끝남.
- 스레드를 16개로 늘려도 작업량이 크지 않거나 병렬화 오버헤드가 커서 처리 시간이 거의 그대로.
- Python 측
- 1개 프로세스로 처리하면 C++ 대비 훨씬 더 큰(약 18초) 시간이 걸리지만,
- 멀티프로세싱(16개)으로 바꾸니 약 14.66초로 줄어들어 병렬화 이득은 존재.
- 다만, C++ 네이티브 성능에 비해 여전히 큰 격차가 있다.
'c,c++' 카테고리의 다른 글
멀티 스레드 관련 궁금증 (Python vs C++) (0) | 2024.12.12 |
---|---|
로봇공학에서 Python 대신 C++을 많이 쓰는 이유 (0) | 2024.10.17 |