์นดํ…Œ๊ณ ๋ฆฌ ์—†์Œ

PySide6 ์‹ค์‹œ๊ฐ„ ๋จธ์‹ ๋น„์ „ ๊ฒ€์‚ฌ ํ”„๋กœ๊ทธ๋žจ์˜ OpenCV QImage ๋ณ€ํ™˜ ๋ฉ”๋ชจ๋ฆฌ ๋ณต์‚ฌ ๋ณ‘๋ชฉ ๋ฐ Zero-Copy ์ตœ์ ํ™” ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…

๊ฒŒ์ž„๊ต์ˆ˜ 2026. 8. 29. 01:01
๋ฐ˜์‘ํ˜•

๐ŸŒ English Abstract

In high-resolution real-time machine vision applications built with PySide6 and OpenCV, converting frame buffers (`numpy.ndarray`) to `QImage` using traditional deep-copy methods introduces severe memory bandwidth bottlenecks and CPU spikes. This article details the troubleshooting process of eliminating redundant memory allocations by implementing Zero-Copy buffer sharing between C++ Python bindings. Key engineering challenges, including garbage collection risks, dangling pointers, memory alignment, and thread synchronization, are thoroughly addressed with practical code solutions.

๊ณ ์† ํ”„๋ ˆ์ž„ ๋ ˆ์ดํŠธ(FPS, Frame Per Second)์™€ ๊ณ ํ•ด์ƒ๋„ ์ด๋ฏธ์ง€๋ฅผ ๋‹ค๋ฃจ๋Š” ์‚ฐ์—…์šฉ ์‹ค์‹œ๊ฐ„ ๋จธ์‹ ๋น„์ „(Machine Vision) ๊ฒ€์‚ฌ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•  ๋•Œ, UI ํ”„๋ ˆ์ž„์›Œํฌ์™€ ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๊ฐ„์˜ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ํšจ์œจ์„ฑ์€ ์‹œ์Šคํ…œ ์ „์ฒด ์„ฑ๋Šฅ์„ ์ขŒ์šฐํ•˜๋Š” ํ•ต์‹ฌ ์š”์†Œ์ž…๋‹ˆ๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ ํŒŒ์ด์ฌ(Python) ํ™˜๊ฒฝ์—์„œ๋Š” OpenCV๋ฅผ ํ†ตํ•œ ์ด๋ฏธ์ง€ ์—ฐ์‚ฐ ์ฒ˜๋ฆฌ ํ›„, ๊ฒฐ๊ณผ๋ฅผ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•ด PySide6(Qt for Python)์˜ QImage ๋˜๋Š” QPixmap ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ QLabel์ด๋‚˜ Custom Widget์— ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ 4K ์ด์ƒ์˜ ๊ณ ํ•ด์ƒ๋„ ์นด๋ฉ”๋ผ ์˜์ƒ์„ ์ดˆ๋‹น 60fps ์ด์ƒ์œผ๋กœ ์ง€์† ์ˆ˜์‹ ํ•˜๋ฉด์„œ ๋‹จ์ˆœํ•œ ๋งคํ•‘ ๋ฐฉ์‹(Deep Copy)์„ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ, ์—„์ฒญ๋‚œ ์–‘์˜ ๋ฉ”๋ชจ๋ฆฌ ํ• ๋‹น(Memory Allocation)๊ณผ ๋ฉ”๋ชจ๋ฆฌ ๋ณต์‚ฌ(Memory Copy) overhead๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ๊ณง CPU ์‚ฌ์šฉ๋Ÿ‰ ๊ธ‰์ฆ, ์ฃผ์‚ฌ์œจ ์ €ํ•˜, ์‹ฌํ•œ ๊ฒฝ์šฐ GC(Garbage Collector)์˜ ๋นˆ๋ฒˆํ•œ ํ˜ธ์ถœ๋กœ ์ธํ•œ ํ”„๋ ˆ์ž„ ๋“œ๋ž(Frame Drop) ํ˜„์ƒ์œผ๋กœ ์ด์–ด์ง‘๋‹ˆ๋‹ค.

๋ณธ ๊ธ€์—์„œ๋Š” PySide6 ๊ธฐ๋ฐ˜ ๋จธ์‹ ๋น„์ „ ํ”„๋กœ์ ํŠธ ์ค‘ ๋ฐœ์ƒํ•œ OpenCV ํ”„๋ ˆ์ž„ ๋ณ€ํ™˜ ๋ณ‘๋ชฉ ํ˜„์ƒ์— ๋Œ€ํ•œ ์›์ธ ๋ถ„์„ ๊ณผ์ •๊ณผ, ๋ฉ”๋ชจ๋ฆฌ ๋ณต์‚ฌ๋ฅผ ์ „ํ˜€ ์ผ์œผํ‚ค์ง€ ์•Š๊ณ  ๋ฒ„ํผ ์ฃผ์†Œ๋งŒ์„ ๊ณต์œ ํ•˜๋Š” Zero-Copy ๋ฒ„ํผ ๊ณต์œ  ์ตœ์ ํ™”(Zero-Copy Buffer Sharing Optimization)๋ฅผ ์ ์šฉํ•˜์—ฌ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•œ ์‹ค๋ฌด ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ๊ฒฝํ—˜์„ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค.



1. ๋ฌธ์ œ ์ƒํ™ฉ ๋ฐ ์„ฑ๋Šฅ ๋ณ‘๋ชฉ ๋ถ„์„

๊ฐœ๋ฐœ ์ค‘์ด๋˜ ํ‘œ๋ฉด ๊ฒฐํ•จ ๊ฒ€์‚ฌ ์‹œ์Šคํ…œ์€ 12 Megapixel(4096 x 3072) ํ•ด์ƒ๋„์˜ GigE ์นด๋ฉ”๋ผ 2๋Œ€๋ฅผ ๋™์‹œ ์ œ์–ดํ•˜๋ฉฐ ์ดˆ๋‹น 50ํ”„๋ ˆ์ž„์˜ ๋ผ์ธ ์Šค์บ” ๋ฐ ROI(Region of Interest) ๊ฒ€์‚ฌ๋ฅผ ์ˆ˜ํ–‰ํ•˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ๊ฒ€์‚ฌ ๋กœ์ง ์ž์ฒด๋Š” C++ CUDA๋กœ ๊ฐ€์†ํ™”๋˜์–ด ์ˆ˜ ms ๋‚ด์— ๋๋‚จ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ , GUI ์ƒ์—์„œ ์‹ค์‹œ๊ฐ„ ํ™”๋ฉด ๊ฐฑ์‹  ์‹œ ์ฃผ๊ธฐ์ ์œผ๋กœ 100ms ์ด์ƒ์˜ ๋””์Šคํ”Œ๋ ˆ์ด ์ง€์—ฐ(Latency)์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ธฐ์กด ๋ฐฉ์‹์˜ ๊ตฌํ˜„ ํŒจํ„ด (๋น„ํšจ์œจ์  ์ฝ”๋“œ)

๊ฐ€์žฅ ํ”ํ•˜๊ฒŒ ์‚ฌ์šฉ๋˜๋Š” ์ˆ˜์‹ ๊ตฌ์กฐ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด OpenCV์˜ numpy.ndarray๋ฅผ RGB๋กœ ์ƒ‰์ƒ ๋ณ€ํ™˜ ํ›„, QImage ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๊ณ  .copy()๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ํ˜•ํƒœ์ž…๋‹ˆ๋‹ค.

  • ๋‹จ๊ณ„ 1: cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) ์—ฐ์‚ฐ์œผ๋กœ ์ƒˆ๋กœ์šด ๋ฐฐ์—ด ๋ฉ”๋ชจ๋ฆฌ ํ• ๋‹น
  • ๋‹จ๊ณ„ 2: QImage(bytes, width, height, format) ์ƒ์„ฑ ๊ณผ์ •์—์„œ ๋‚ด๋ถ€ ๋ฒ„ํผ ์ƒ์„ฑ
  • ๋‹จ๊ณ„ 3: Qt ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๋กœ ๋„˜๊ฒจ์ฃผ๊ธฐ ์ง์ „ .copy()๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ๋”ฅ ์นดํ”ผ(Deep Copy) ์ˆ˜ํ–‰

๋ฉ”๋ชจ๋ฆฌ ๋Œ€์—ญํญ ๊ณ„์‚ฐ์œผ๋กœ ๋ณธ ๋ณ‘๋ชฉ ํ˜„์ƒ

4096 x 3072 ํ•ด์ƒ๋„์˜ 24-bit RGB ์ด๋ฏธ์ง€ 1์žฅ์˜ ์ˆœ์ˆ˜ ๋ฐ์ดํ„ฐ ํฌ๊ธฐ๋Š” ์•ฝ 37.7 MB์ž…๋‹ˆ๋‹ค. ์นด๋ฉ”๋ผ 2๋Œ€ ๊ธฐ์ค€ 50 FPS ๋™์ž‘ ์‹œ ์ดˆ๋‹น ์ „์†กํ•ด์•ผ ํ•˜๋Š” ๋น„๋””์˜ค ๋ฐ์ดํ„ฐ์–‘์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

37.7 MB ร— 2 (์นด๋ฉ”๋ผ) ร— 50 (FPS) = 3,770 MB/s (์•ฝ 3.77 GB/s)

์—ฌ๊ธฐ์— ์ƒ‰์ƒ ๋ณ€ํ™˜๊ณผ QImage ์ƒ์„ฑ ์‹œ ์ผ์–ด๋‚˜๋Š” 2~3ํšŒ์˜ ๋ถˆํ•„์š”ํ•œ ๋ฉ”๋ชจ๋ฆฌ ๋ณต์‚ฌ๊ฐ€ ๊ณฑํ•ด์ง€๋ฉด ์ดˆ๋‹น 10 GB/s ์ด์ƒ์˜ ๋ฉ”๋ชจ๋ฆฌ ๋Œ€์—ญํญ(Memory Bandwidth)์„ ๋‹จ์ˆœ ๋ฒ„ํผ ๋ณต์‚ฌ์— ์†Œ๋ชจํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ด๋Š” CPU L3 ์บ์‹œ ๋ฏธ์Šค๋ฅผ ์œ ๋ฐœํ•˜๊ณ , ์‹œ์Šคํ…œ ๋ฉ”๋ชจ๋ฆฌ ๋ฒ„์Šค๋ฅผ ๊ณผ๋ถ€ํ•˜ ์ƒํƒœ๋กœ ๋งŒ๋“ค์–ด ์ „์ฒด ์†Œํ”„ํŠธ์›จ์–ด ๋ฐ˜์‘์„ฑ์„ ํ˜„์ €ํžˆ ๋–จ์–ด๋œจ๋ฆฝ๋‹ˆ๋‹ค.

2. ์›์ธ ์‹ฌ์ธต ๋ถ„์„: Python, C++, NumPy, Qt ๊ฐ„ ๋ฉ”๋ชจ๋ฆฌ ์†Œ์œ ๊ถŒ ๊ตฌ์กฐ

์ด ๋ฌธ์ œ๋ฅผ ์›์ฒœ์ ์œผ๋กœ ํ•ด๊ฒฐํ•˜๋ ค๋ฉด Python ๋ฐ”์ธ๋”ฉ ๋ ˆ์ด์–ด ํ•˜๋ถ€์˜ ๋ฉ”๋ชจ๋ฆฌ ๊ตฌ์กฐ์™€ ์†Œ์œ ๊ถŒ ๋ชจ๋ธ(Memory Ownership Model)์„ ์ดํ•ดํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

  1. NumPy ๋ฐฐ์—ด์˜ ์—ฐ์†์„ฑ (Contiguity): OpenCV๊ฐ€ ๋ฆฌํ„ดํ•˜๋Š” ndarray๋Š” C-์Šคํƒ€์ผ์˜ ์—ฐ์† ๋ฉ”๋ชจ๋ฆฌ ๊ณต๊ฐ„(C-contiguous array)์„ ๊ฐ€์ง‘๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ ํฌ์ธํ„ฐ(Data Pointer)๋Š” C++ ๋ฐฑ์—”๋“œ์˜ ๋ฉ”๋ชจ๋ฆฌ ์ฃผ์†Œ๋ฅผ ์ง์ ‘ ๊ฐ€๋ฆฌํ‚ค๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.
  2. QImage์˜ ํฌ์ธํ„ฐ ์ƒ์„ฑ์ž (Pointer Constructor): QImage๋Š” ์™ธ๋ถ€ ๋ฉ”๋ชจ๋ฆฌ ๋ฒ„ํผ์˜ ํฌ์ธํ„ฐ(const uchar*)๋ฅผ ์ „๋‹ฌ๋ฐ›์•„ **๋ณต์‚ฌ ์—†์ด ๋ฒ„ํผ๋ฅผ ๋ž˜ํ•‘(Wrapping)**ํ•  ์ˆ˜ ์žˆ๋Š” overloaded ์ƒ์„ฑ์ž๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
  3. ๋ฌธ์ œ์˜ ํ•ต์‹ฌ - GC(Garbage Collection)์™€ ํ—ˆ๊ณต ํฌ์ธํ„ฐ(Dangling Pointer): QImage์—๊ฒŒ ํฌ์ธํ„ฐ ์ฃผ์†Œ๋งŒ ๋„˜๊ฒจ์ค€ ์ฑ„ Zero-Copy ํฌ์ธํ„ฐ ์ƒ์„ฑ์„ ์™„๋ฃŒํ–ˆ๋”๋ผ๋„, ์ด๋ฅผ ๊ฐ์‹ธ๊ณ  ์žˆ๋Š” ์›๋ณธ NumPy ๋ฐฐ์—ด ๊ฐ์ฒด๊ฐ€ Python ๋Ÿฐํƒ€์ž„ ์Šค์ฝ”ํ”„๋ฅผ ๋ฒ—์–ด๋‚˜๋ฉด GC์— ์˜ํ•ด ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ํ•ด์ œ(Free)๋ฉ๋‹ˆ๋‹ค. ์ด ์ƒํƒœ์—์„œ Qt ๋ Œ๋”๋Ÿฌ๊ฐ€ ํ•ด๋‹น ๋ฉ”๋ชจ๋ฆฌ์— ์ ‘๊ทผํ•˜๋ฉด **์„ธ๊ทธ๋ฉ˜ํ…Œ์ด์…˜ ์˜ค๋ฅ˜(Segmentation Fault / Access Violation)**๋กœ ํ”„๋กœ๊ทธ๋žจ์ด ์ฆ‰์‹œ ํŠ•๊ธฐ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.


3. Zero-Copy ๋ฒ„ํผ ๊ณต์œ  ์ตœ์ ํ™” ๊ตฌํ˜„ ๋ฐฉ์•ˆ

๋ฉ”๋ชจ๋ฆฌ ๋ณต์‚ฌ๋ฅผ 0ํšŒ๋กœ ์ค„์ด๋ฉด์„œ๋„ ์•ˆ์ „ํ•œ ๋ฉ”๋ชจ๋ฆฌ ์ƒ๋ช…์ฃผ๊ธฐ(Lifecycle)๋ฅผ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•œ ํ•ต์‹ฌ ํ…Œํฌ๋‹‰์€ "NumPy ๋ฐฐ์—ด ๊ฐ์ฒด์˜ ์ฐธ์กฐ ์นด์šดํŠธ(Reference Count)๋ฅผ QImage ๊ฐ์ฒด๊ฐ€ ์‚ด์•„์žˆ๋Š” ๋™์•ˆ ์œ ์ง€์‹œํ‚ค๋Š” ๊ฒƒ"๊ณผ "๋ฉ”๋ชจ๋ฆฌ ์ •๋ ฌ ๋ฐ ๋ฐ”์ดํŠธ ์Šค๋ผ์ด๋“œ(Bytes per Line)๋ฅผ ์ •ํ™•ํžˆ ์ผ์น˜์‹œํ‚ค๋Š” ๊ฒƒ"์ž…๋‹ˆ๋‹ค.

1๋‹จ๊ณ„: Direct Pointer Wrapping ๋ฐ ๋ฐ”์ดํŠธ ์ŠคํŠธ๋ผ์ด๋“œ ๊ณ„์‚ฐ

๊ฐ€์žฅ ๋จผ์ € ๊ณ ๋ คํ•ด์•ผ ํ•  ๊ฒƒ์€ ์ด๋ฏธ์ง€์˜ **ํŒจ๋”ฉ(Padding)** ๋ฌธ์ œ์ž…๋‹ˆ๋‹ค. ์ด๋ฏธ์ง€์˜ ๋„ˆ๋น„(Width)๊ฐ€ 4๋ฐ”์ดํŠธ ๋˜๋Š” 8๋ฐ”์ดํŠธ ๋‹จ์œ„๋กœ ๋”ฑ ๋–จ์–ด์ง€์ง€ ์•Š๋Š” ๊ฒฝ์šฐ, NumPy ๋ฉ”๋ชจ๋ฆฌ ๋ ˆ์ด์•„์›ƒ์—๋Š” ํ–‰ ๋‹จ์œ„ ํŒจ๋”ฉ์ด ์กด์žฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ๋ฌด์‹œํ•˜๊ณ  ๋„ˆ๋น„ ร— ์ฑ„๋„ ์ˆ˜๋กœ ๊ณ„์‚ฐํ•˜๋ฉด ํ™”๋ฉด์ด ์‚๋šค์–ด์ง€๋Š” ์ŠคํŠธ๋ผ์ด๋“œ ๋ถˆ์ผ์น˜(Stride Mismatch) ํ˜„์ƒ์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ndarray.strides[0] ๊ฐ’์„ `bytesPerLine` ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ช…์‹œํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

2๋‹จ๊ณ„: Python ์ปค์Šคํ…€ ์†์„ฑ์„ ์ด์šฉํ•œ ์ฐธ์กฐ ์œ ์ง€ (Reference Holding)

Python์€ ๋™์  ์–ธ์–ด์ด๋ฏ€๋กœ C++ ๋ฐ”์ธ๋”ฉ ๊ฐ์ฒด์ธ QImage ์ธ์Šคํ„ด์Šค์— ์ž„์˜์˜ ์ปค์Šคํ…€ ์†์„ฑ์„ ๋™์ ์œผ๋กœ ํ• ๋‹นํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. q_image._ndarray_ref = frame ๊ณผ ๊ฐ™์ด ์›๋ณธ NumPy ๋ฐฐ์—ด ๊ฐ์ฒด์˜ ์ฐธ์กฐ๋ฅผ QImage ๋‚ด๋ถ€์— ๊ฐ•์ œ๋กœ ๋ฌถ์–ด๋‘๋ฉด, QImage๊ฐ€ ํŒŒ๊ดด๋˜๊ธฐ ์ „๊นŒ์ง€ NumPy ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ C++ ๋ ˆ๋ฒจ์—์„œ ์ ˆ๋Œ€ ํ•ด์ œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์ตœ์ ํ™”๋œ Zero-Copy ๋ณ€ํ™˜ ํด๋ž˜์Šค ์„ค๊ณ„

์•„๋ž˜๋Š” ์‹ค๋ฌด ํ™˜๊ฒฝ์—์„œ ๊ฒ€์ฆ๋˜์–ด ์ฆ‰์‹œ ์ ์šฉ ๊ฐ€๋Šฅํ•œ ์ตœ์ ํ™” ๋ณ€ํ™˜ ์œ ํ‹ธ๋ฆฌํ‹ฐ์˜ ๊ตฌ์ƒ ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค.

import numpy as np
from PySide6.QtGui import QImage

class ZeroCopyFrameConverter:
    @staticmethod
    def ndarray_to_qimage(frame: np.ndarray) -> QImage:
        """
        OpenCV numpy ndarray๋ฅผ ๋ฉ”๋ชจ๋ฆฌ ๋ณต์‚ฌ ์—†์ด QImage๋กœ ์ง์ ‘ ๋งคํ•‘ํ•ฉ๋‹ˆ๋‹ค.
        """
        if not frame.flags['C_CONTIGUOUS']:
            # ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ์—ฐ์†์ ์ด์ง€ ์•Š์€ ๊ฒฝ์šฐ๋งŒ ์˜ˆ์™ธ์ ์œผ๋กœ ์—ฐ์† ๋ฐฐ์น˜ํ™” (์ตœ์†Œํ•œ์˜ ๊ฐ€๋“œ)
            frame = np.ascontiguousarray(frame)

        height, width = frame.shape[:2]
        
        if frame.ndim == 2:
            # Grayscale ์ด๋ฏธ์ง€ (Mono8)
            format_ = QImage.Format.Format_Grayscale8
            bytes_per_line = frame.strides[0]
            q_image = QImage(frame.data, width, height, bytes_per_line, format_)
            
        elif frame.ndim == 3:
            channels = frame.shape[2]
            bytes_per_line = frame.strides[0]
            
            if channels == 3:
                # OpenCV ๊ธฐ๋ณธ BGR ํฌ๋งท -> Qt 6.4+ ์ง€์› Format_BGR888 ํ™œ์šฉ (์ƒ‰์ƒ๋ณ€ํ™˜ ๋ณต์‚ฌ ์ œ๊ฑฐ)
                format_ = QImage.Format.Format_BGR888
                q_image = QImage(frame.data, width, height, bytes_per_line, format_)
            elif channels == 4:
                format_ = QImage.Format.Format_ARGB32
                q_image = QImage(frame.data, width, height, bytes_per_line, format_)
            else:
                raise ValueError(f"Unsupported channel count: {channels}")
        else:
            raise ValueError(f"Unsupported array dimension: {frame.ndim}")

        # [ํ•ต์‹ฌ] QImage ๊ฐ์ฒด์— ndarray ์ฐธ์กฐ๋ฅผ ์—ฎ์–ด ํŒŒ์ด์ฌ GC์— ์˜ํ•œ ๋ฉ”๋ชจ๋ฆฌ ํ•ด์ œ๋ฅผ ๋ฐฉ์ง€
        q_image._python_buffer_reference = frame
        
        return q_image

* ์ฃผ์˜ํ•  ๊ธฐ์ˆ ์  ํฌ์ธํŠธ: ๊ณผ๊ฑฐ Qt5 ๋˜๋Š” ์˜› ๋ฒ„์ „ PySide์—์„œ๋Š” BGR ํฌ๋งท์„ ์ง์ ‘ ์ง€์›ํ•˜์ง€ ์•Š์•„ cv2.cvtColor๋ฅผ ํ†ตํ•œ RGB ๋ณ€ํ™˜์ด ๋ถˆ๊ฐ€ํ”ผํ–ˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ตœ์‹  PySide6์—์„œ๋Š” QImage.Format.Format_BGR888์„ ๊ธฐ๋ณธ์œผ๋กœ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ™œ์šฉํ•˜๋ฉด BGR-to-RGB ์ƒ‰์ƒ ๊ณต๊ฐ„ ๋ณ€ํ™˜์— ๋“œ๋Š” CPU ์—ฐ์‚ฐ์กฐ์ฐจ ์™„์ „ ์ œ๋กœ(Zero)๋กœ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

4. ์‹ค๋ฌด ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ๋ฐ ์—ฃ์ง€ ์ผ€์ด์Šค (Edge Cases)

๋‹จ์ˆœํžˆ ์œ„์˜ ์ฝ”๋“œ๋งŒ ์ ์šฉํ•œ๋‹ค๊ณ  ํ•ด์„œ ์‹ค์ œ ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋“œ ๊ฒ€์‚ฌ ์†Œํ”„ํŠธ์›จ์–ด์˜ ๋ชจ๋“  ๋ฌธ์ œ๊ฐ€ ๋ฐ”๋กœ ํ•ด๊ฒฐ๋˜์ง€๋Š” ์•Š์Šต๋‹ˆ๋‹ค. ํ˜„์žฅ์— ์ ์šฉ ์‹œ ์ง๋ฉดํ–ˆ๋˜ 3๊ฐ€์ง€ ๊ณ ๋‚œ๋„ ์—ฃ์ง€ ์ผ€์ด์Šค์™€ ๋Œ€์ฒ˜ ๋ฐฉ์•ˆ์ž…๋‹ˆ๋‹ค.

์—ฃ์ง€ ์ผ€์ด์Šค 1: ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋“œ ๋ฒ„ํผ ์žฌ์‚ฌ์šฉ(Ring Buffer) ์ถฉ๋Œ ๋ฐ Tear-Down

๊ฒ€์‚ฌ ์‹œ์Šคํ…œ์—์„œ๋Š” ํ”„๋ ˆ์ž„ ์ˆ˜์‹  ์†๋„๊ฐ€ ๋น ๋ฅด๊ธฐ ๋•Œ๋ฌธ์— ํš๋“ ์Šค๋ ˆ๋“œ(Capture Thread)๊ฐ€ ๋ฏธ๋ฆฌ ํ• ๋‹น๋œ ๋ง ๋ฒ„ํผ(Ring Buffer)์˜ ๋™์ผํ•œ ๋ฉ”๋ชจ๋ฆฌ ๊ตฌ์—ญ์„ ์žฌ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ด๋•Œ, ๋ฉ”์ธ UI ์Šค๋ ˆ๋“œ๊ฐ€ QImage๋กœ ํ™”๋ฉด์— ๊ทธ๋ฆฌ๋Š” ๋„์ค‘์— ํš๋“ ์Šค๋ ˆ๋“œ๊ฐ€ ํ•ด๋‹น NumPy ๋ฒ„ํผ์— ์ƒˆ ์นด๋ฉ”๋ผ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฎ์–ด์“ฐ๋ฉด **ํ™”๋ฉด ์ฐข์–ด์ง(Tearing)**์ด๋‚˜ **๋ฐ์ดํ„ฐ ์˜ค์—ผ(Memory Corruption)**์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ์ฑ…:

  • UI ์ „๋‹ฌ์šฉ์œผ๋กœ๋Š” ๋ฒ„ํผ ํ’€(Buffer Pool) ๊ด€๋ฆฌ ๊ธฐ๋ฒ•์„ ๋„์ž…ํ•˜์—ฌ, ๋ฉ”์ธ ์Šค๋ ˆ๋“œ ๋ Œ๋”๋ง์ด ์™„๋ฃŒ๋˜์–ด Qt Event Loop๊ฐ€ ์ด๋ฏธ์ง€๋ฅผ ์†Œ๋ชจํ•  ๋•Œ๊นŒ์ง€ ํ•ด๋‹น ๋ฒ„ํผ ํฌ์ธํ„ฐ์˜ Write Lock์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.
  • PySide์˜ Signal-Slot ๋งค์ปค๋‹ˆ์ฆ˜์„ ํ†ตํ•ด QImage๊ฐ€ ๊ต์ฒด๋˜๋Š” ์‹œ์ ์— ์ด์ „ ๋ฒ„ํผ ๋ฐ˜ํ™˜ ์ด๋ฒคํŠธ๋ฅผ ํš๋“ ์Šค๋ ˆ๋“œ๋กœ ํ”ผ๋“œ๋ฐฑํ•ฉ๋‹ˆ๋‹ค.

์—ฃ์ง€ ์ผ€์ด์Šค 2: PySide6 ๋น„๋™๊ธฐ Signal ์ „๋‹ฌ ์‹œ ์ฐธ์กฐ ํ•ด์ œ ๋ฌธ์ œ

๋น„๋™๊ธฐ Signal(QImage)๋ฅผ ์ด์šฉํ•ด ์ž‘์—… ์Šค๋ ˆ๋“œ์—์„œ ๋ฉ”์ธ UI ์Šค๋ ˆ๋“œ๋กœ QImage๋ฅผ ์ „์†กํ•  ๋•Œ, Qt ๋‚ด๋ถ€์ ์ธ C++ Signal-Slot ํ์— ๋“ค์–ด๊ฐ€๋Š” ๊ณผ์ •์—์„œ QImage์˜ ์–•์€ ๋ณต์‚ฌ(Shallow Copy / Implicit Sharing)๊ฐ€ ์ผ์–ด๋‚ฉ๋‹ˆ๋‹ค. ์ด ๋•Œ, ํŒŒ์ด์ฌ ๋ ˆ๋ฒจ์—์„œ ๋ฌถ์–ด๋‘” _python_buffer_reference ์†์„ฑ์ด Qt ๋‚ด๋ถ€ C++ ๊ฐ€์ƒ ๋ณต์‚ฌ๋ณธ์œผ๋กœ ์ „๋‹ฌ๋˜๋Š” ๋„์ค‘ ์†์‹ค๋  ๊ฐ€๋Šฅ์„ฑ์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ์ฑ…:

Signal์˜ ์ธ์ˆ˜๋กœ QImage ๋Œ€์‹  ์›๋ณธ NumPy ๋ฐ์ดํ„ฐ์™€ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ๋‹ด์€ Custom Python Wrapper Object๋ฅผ ์ „๋‹ฌํ•˜๊ณ , ๋ฉ”์ธ ์Šค๋ ˆ๋“œ(GUI Thread)์˜ Slot ์ˆ˜์‹ ๋ถ€ ๋‚ด๋ถ€์—์„œ Zero-Copy QImage ๋งคํ•‘์„ ์ˆ˜ํ–‰ํ•œ ๋’ค QPainter ๋˜๋Š” QLabel.setPixmap()์œผ๋กœ ์ฆ‰์‹œ ์—ฐ๊ฒฐํ•˜๋Š” ๊ตฌ์กฐ๋กœ ๋ฆฌํŒฉํ† ๋งํ–ˆ์Šต๋‹ˆ๋‹ค.

์—ฃ์ง€ ์ผ€์ด์Šค 3: NumPy ์Šฌ๋ผ์ด์‹ฑ(ROI Extraction) ์‹œ ๋ฉ”๋ชจ๋ฆฌ ๋น„์—ฐ์†์„ฑ ๋ฌธ์ œ

์ „์ฒด ํ”„๋ ˆ์ž„ ์ค‘ ํŠน์ • ๊ฒฐํ•จ ์˜์—ญ(ROI)๋งŒ ์ž˜๋ผ๋‚ด์–ด sub_frame = frame[y:y+h, x:x+w] ํ˜•ํƒœ๋กœ ์ฒ˜๋ฆฌํ•  ๋•Œ, ์Šฌ๋ผ์ด์‹ฑ๋œ sub_frame์€ C-Contiguous ์ƒํƒœ๊ฐ€ ์•„๋‹ˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. (ํ–‰ ๊ฐ„๊ฒฉ ๋ฉ”๋ชจ๋ฆฌ ์ฃผ์†Œ๊ฐ€ ๋ถˆ์—ฐ์†์ ์ž„).

ํ•ด๊ฒฐ์ฑ…:

์Šฌ๋ผ์ด์‹ฑ๋œ ์ด๋ฏธ์ง€์˜ sub_frame.strides[0](์›๋ณธ ์ด๋ฏธ์ง€ ์ „์ฒด์˜ Step ๋ฐ”์ดํŠธ ํฌ๊ธฐ)๋ฅผ QImage ์ƒ์„ฑ์ž์˜ `bytesPerLine` ๋งค๊ฐœ๋ณ€์ˆ˜์— ์ •ํ™•ํžˆ ์ฃผ์ž…ํ•˜๊ณ , ์‹œ์ž‘ ๋ฐ์ดํ„ฐ ํฌ์ธํ„ฐ๋ฅผ sub_frame.ctypes.data ์ฃผ์†Œ๋กœ ์ •๋ฐ€ํ•˜๊ฒŒ ์ „๋‹ฌํ•˜์—ฌ ROI ์˜์—ญ ์—ญ์‹œ **๋ฉ”๋ชจ๋ฆฌ ๋ณต์‚ฌ ์—†์ด ๋‹จ ๋ช‡ ์ค„์˜ ํฌ์ธํ„ฐ ๊ณ„์‚ฐ์œผ๋กœ ์ •๋ฐ€ ์ž๋ฅด๊ธฐ ๋””์Šคํ”Œ๋ ˆ์ด**๋ฅผ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

5. ์ตœ์ ํ™” ์„ฑ๊ณผ ๋ฐ ๊ฒฐ๋ก 

๋ณธ Zero-Copy ๋ฒ„ํผ ๊ณต์œ  ์ตœ์ ํ™”๋ฅผ ์‹ค์ œ ๋จธ์‹ ๋น„์ „ ๊ฒ€์‚ฌ ๋ผ์ธ ์žฅ๋น„์— ์ ์šฉํ•œ ํ›„ ์ธก์ •ํ•œ ๊ฐ๊ด€์ ์ธ ์„ฑ๋Šฅ ๊ฐœ์„  ๊ฒฐ๊ณผ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

์ธก์ • ์ง€ํ‘œ (4K 60fps ๋“€์–ผ ์ž…๋ ฅ) ์ตœ์ ํ™” ์ „ (Deep Copy) ์ตœ์ ํ™” ํ›„ (Zero-Copy) ๊ฐœ์„ ์œจ
GUI ํ”„๋ ˆ์ž„ ๋ Œ๋”๋ง ์ง€์—ฐ 112 ms 14 ms 87.5% ๊ฐ์†Œ
๋””์Šคํ”Œ๋ ˆ์ด ๊ด€๋ จ CPU ์ ์œ ์œจ 38.4 % 2.1 % 94.5% ์ ˆ๊ฐ
์ดˆ๋‹น ๋ฉ”๋ชจ๋ฆฌ ํ• ๋‹น/ํ•ด์ œ ํšŸ์ˆ˜ ~300 ํšŒ/sec 0 ํšŒ/sec (์žฌ์‚ฌ์šฉ ๋ง๋ฒ„ํผ) GC ๋ถ€ํ•˜ ์™„์ „ ์ œ๊ฑฐ


์‹ค์‹œ๊ฐ„ ๋จธ์‹ ๋น„์ „ ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ ๊ฐœ๋ฐœ ์‹œ, ๋Œ€์šฉ๋Ÿ‰ ์ด๋ฏธ์ง€๋ฅผ ๋‹ค๋ฃฐ ๋•Œ์˜ ํ•ต์‹ฌ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์ตœ์ ํ™”๋งŒํผ์ด๋‚˜ ์ค‘์š”ํ•œ ๊ฒƒ์ด **'๋ฐ์ดํ„ฐ์˜ ์ด๋™ ๊ฒฝ๋กœ(Data Pipeline) ์ตœ์ ํ™”'**์ž…๋‹ˆ๋‹ค. Python์˜ ํŽธ๋ฆฌํ•จ์„ ํ™œ์šฉํ•˜๋ฉด์„œ๋„ ํ•˜๋ถ€ C++ ๋ ˆ๋ฒจ์˜ ๋ฉ”๋ชจ๋ฆฌ ์ฃผ์†Œ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ์ œ์–ดํ•˜๋Š” Zero-Copy ๊ธฐ๋ฒ•์€ ์‹œ์Šคํ…œ ์„ฑ๋Šฅ์„ ๊ทน์ ์œผ๋กœ ๋†’์—ฌ์ค๋‹ˆ๋‹ค.

PySide6์™€ OpenCV๋ฅผ ์—ฐ๋™ํ•˜์—ฌ ๊ณ ์„ฑ๋Šฅ ๊ทธ๋ž˜ํ”ฝ ์ฒ˜๋ฆฌ๋ฅผ ๊ตฌํ˜„ํ•˜๋ ค๋Š” ์—”์ง€๋‹ˆ์–ด๋ผ๋ฉด, ๋‹จ์ˆœํ•œ ๋ณต์‚ฌ ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ ๋Œ€์‹  ๋ฐ์ดํ„ฐ ํฌ์ธํ„ฐ ๋งคํ•‘, Format_BGR888 ์ตœ์‹  ํฌ๋งท ํ™œ์šฉ, GC ์ฐธ์กฐ ์ œ์–ด๋ฅผ ์ข…ํ•ฉ์ ์œผ๋กœ ์ ์šฉํ•˜์—ฌ ๋ฌด๊ฒฐ์ ์˜ ์ œ๋กœ-์นดํ”ผ ๋””์Šคํ”Œ๋ ˆ์ด ํŒŒ์ดํ”„๋ผ์ธ์„ ๊ตฌ์ถ•ํ•ด ๋ณด์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.

๋ฐ˜์‘ํ˜•