12.8. 프레임 스트리밍¶
커스텀 채널의 가장 흔한 실제 용도는 카메라에서 호스트 프로그램으로 카메라의 프레임 레이트에 맞춰 이미지 프레임을 스트리밍하는 것입니다. 그 동작 방식은 겉보기보다 미묘합니다. JPEG는 25 KB 이상까지 커질 수 있어서 호스트는 이를 여러 조각으로 나누어 읽으며, 카메라의 캡처 루프는 읽는 도중에 버퍼를 덮어쓰지 못하도록 막아야 합니다. 올바른 패턴 – 여기서 보여주고 openmv-projects/tools/ 의 도구들이 사용하는 – 은 호스트가 마지막 바이트까지 가져가기 전까지 버퍼를 래치(latch) 합니다.
12.8.1. 카메라 측¶
단일 프레임버퍼로 캡처하고, 호스트의 첫 읽기에서 이를 래치하며, 호스트가 전체 이미지를 다 소비한 후에야 다음 스냅샷을 찍는 프레임 채널:
import csi
import protocol
csi0 = csi.CSI()
csi0.reset()
csi0.pixformat(csi.RGB565)
csi0.framesize(csi.QVGA)
csi0.framebuffers(1)
img = csi0.snapshot()
img.compress(quality=85)
img_mv = memoryview(img.bytearray())
img_size = len(img_mv)
frame_available = True
class FrameChannel:
def poll(self):
return frame_available
def size(self):
return img_size
def readp(self, offset, size):
global frame_available
end = offset + size
mv = img_mv[offset:end]
if end == img_size:
# Host has just read the last byte of this frame --
# release the buffer so the capture loop can refresh.
frame_available = False
return mv
ch = protocol.register(name='frame', backend=FrameChannel())
while True:
if not frame_available:
img = csi0.snapshot()
img.compress(quality=85)
img_mv = memoryview(img.bytearray())
img_size = len(img_mv)
frame_available = True
ch.send_event(0x01) # notify host that a new frame is ready
여기서 실제로 일을 하는 부분은 네 가지입니다:
frame_available은 래치 입니다. 캡처 루프는 이 값이False일 때 – 즉 호스트가 이전 프레임의 마지막 바이트를 가져갔을 때 – 에만 새 스냅샷을 찍습니다. 호스트의 읽기는 마지막 오프셋이 제공된 후readp내부에서 이 값을 다시False로 설정합니다. 이 가드가 없으면 다음csi0.snapshot()이 읽는 도중에 버퍼를 덮어써서 호스트는 두 캡처가 합쳐진 프레임을 받게 됩니다.백엔드가 구현하는 것은
read가 아니라readp입니다. 프로토콜 라이브러리는 반환된 버퍼를 권위 있는 것으로 취급하여 그 바이트를 복사 없이 직접 발신 패킷으로 읽어 들입니다. 프레임 크기의 페이로드에서는readp가 중간 복사를 강제하는read보다 눈에 띄게 빠릅니다.size는 아무것도 다시 계산하지 않고 캐시된 JPEG 길이를 반환합니다. 캡처 루프는 버퍼를 갱신할 때마다 이를 유지합니다. 호스트는 몇 바이트를 가져와야 하는지 알기 위해poll과readp사이에서size를 호출합니다.send_event()는 새 프레임이 도착하는 순간 호스트에게 알려서, 호스트가 폴링 없이 즉시 데이터를 가져오기 시작할 수 있게 합니다. 이벤트 ID0x01은 애플리케이션이 정의하는 값입니다(이 경우 “프레임 준비됨”). 알림 종류마다 서로 다른 작은 정수를 사용하세요.
12.8.2. 단편화(Fragmentation)¶
JPEG 품질 85의 QVGA RGB565는 장면에 따라 대략 10-25 KB로 압축되며, 이는 어떤 카메라에서든 협상된 최대 페이로드보다 훨씬 큽니다(보드별 표는 protocol.init() 참조). 한 번의 JPEG 읽기는 하나의 패킷에 들어가지 않지만, 프로토콜 라이브러리가 이를 투명하게 단편화하므로 괜찮습니다.
호스트가 channel_read('frame', 12000) 을 요청하면:
카메라의
readp는offset=0과 전체 12000바이트 요청으로 한 번 호출됩니다. 이는 전체 범위를 커버하는 하나의 memoryview를 반환합니다.프로토콜 라이브러리는 그 memoryview를 전송선상에서 최대 페이로드 크기의 조각들로 나누어, 조각마다 하나의
CHANNEL_READ응답 패킷을 보내며, 각각 고유한 헤더와 CRC를 가집니다. 바이트는 백엔드의 버퍼에서 복사 없이 직접 스트리밍됩니다.호스트는 조각들을 순서대로 받고, 신뢰성 계층이 CRC에 실패한 조각이 있으면 재전송하며, 호스트 SDK는 조각들을 이어 붙여 호출자에게 반환할 12000바이트 결과로 만듭니다.
참고
이것이 readp 와 read 의 핵심적인 실제 차이입니다. readp 는 호스트 요청당 한 번 호출되며, 프로토콜 계층이 반환된 단일 버퍼에서 단편화하여 전송합니다. read 는 조각당 한 번 호출되며, 라이브러리가 반환된 각 조각을 자체 패킷 버퍼로 복사합니다. 프레임 크기의 페이로드에서는 readp 가 조각당 Python 수준 호출 오버헤드와 복사를 모두 줄여줍니다.
팁
그 차이를 직접 확인하고 싶으신가요? 백엔드의 readp 메서드 이름을 read 로 바꿔 보세요 – 다른 것은 아무것도 변경하지 않습니다. 라이브러리가 대신 read 기능을 사용하게 됩니다 – 그리고 변경 전후로 호스트의 프레임 레이트 카운터를 비교해 보세요. 더 느린 수치가 readp 를 사용함으로써 피할 수 있는 조각당 복사 및 Python 호출 비용입니다.
FrameChannel.readp 의 래치는 offset + size == img_size 일 때 – 즉 호스트가 마지막 바이트를 가져간 순간 – 버퍼를 해제합니다. 그때까지 버퍼는 유효하게 유지되어야 하며, 그렇기 때문에 캡처 루프는 frame_available 이 다시 False 로 바뀐 후에야 다음 스냅샷을 찍습니다.
12.8.3. 호스트 측¶
호스트는 빡빡한 루프에서 프레임을 가져옵니다:
import io
from PIL import Image
from openmv.camera import Camera
with Camera('/dev/ttyACM0', baudrate=921600) as cam:
cam.update_channels()
while True:
size = cam.channel_size('frame')
if not size:
continue
data = cam.channel_read('frame', size)
img = Image.open(io.BytesIO(data))
img.show() # or feed to a GUI
channel_size() 호출은 “준비된 것이 있는가” 검사 역할도 겸합니다 – 0은 카메라가 아직 캡처하지 않았다는 뜻입니다 – 따라서 루프는 빈 버퍼에 대한 읽기 시도를 건너뜁니다. 이미 타이머로 폴링하는 GUI 애플리케이션에서는 이것이 자연스러운 패턴입니다.
Pillow의 Image.open 은 JPEG를 디코딩합니다. 카메라가 이미 JPEG로 압축했으므로 호스트는 RGB565에 대한 값비싼 비트 패킹을 다시 할 필요가 없습니다. 호스트 스크립트는 마찬가지로 손쉽게 바이트를 디스크에 저장하거나, OpenCV에 넘기거나, 웹 뷰로 전달할 수 있습니다.
12.8.4. 처리량 관점¶
달성 가능한 프레임 레이트를 제한하는 것은 세 가지입니다:
카메라의 캡처 속도. 프로토콜은 센서가 생성하는 것보다 빠르게 프레임을 전달할 수 없습니다. 선택한 픽셀 형식과 프레임 크기가 캡처에 부과하는 한계가 무엇이든 그것이 천장입니다.
협상된 최대 페이로드. 페이로드가 클수록 프레임당 조각 수가 적어지고 프레이밍 오버헤드가 줄어들므로, 프로토콜 버퍼가 큰 카메라가 작은 카메라보다 바이트를 더 빠르게 옮깁니다.
CRC 및 ACK 오버헤드. 각 패킷은 14바이트의 프레이밍과 한 번의 ACK 왕복 비용이 듭니다. 긴 조각의 경우 페이로드당 오버헤드는 작지만, 작은 페이로드의 경우 이것이 지배적입니다.
대부분의 카메라-노트북 GUI 작업에서 제한 요인은 프로토콜 스택이 아니라 카메라의 캡처 및 JPEG 압축 시간입니다. 프로토콜이 병목이 되는 경우 – 예를 들어 압축되지 않은 원시 프레임을 높은 프레임 레이트로 스트리밍하는 경우 – 의 지렛대는 ACK를 끄거나(protocol.init(ack=False)), 카메라가 지원한다면 프로토콜 버퍼를 늘리거나, GRAYSCALE로 캡처하여 각 압축 JPEG가 세 채널 대신 한 채널을 담아 인코딩된 프레임이 전송선상에서 눈에 띄게 작아지게 하는 것입니다.
프레임 채널은 카메라-호스트 데이터 흐름의 표준입니다. write 메서드를 추가한 동일한 백엔드 인터페이스로 호스트가 반대 방향으로 데이터를 보낼 수도 있는데 – 이는 운영자가 단지 보기만 하는 것이 아니라 무언가를 변경 하고 싶어지는 순간 대화형 카메라 도구에 필요한 것입니다.