12.8. Transmitindo quadros¶
O uso real mais comum de um canal personalizado é transmitir quadros de imagem da câmera para um programa host na taxa de quadros da câmera. A mecânica é mais sutil do que parece: um JPEG pode chegar a 25 KB ou mais, então o host o lê como vários fragmentos, e o laço de captura da câmera deve ser impedido de sobrescrever o buffer no meio da leitura. O padrão correto – mostrado aqui e usado pelas ferramentas em openmv-projects/tools/ – trava o buffer até que o host termine de puxar o último byte.
12.8.1. O lado da câmera¶
Um canal de quadros que captura em um único framebuffer, o trava na primeira leitura do host e só tira o próximo snapshot depois que o host consumiu a imagem completa:
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
Quatro peças fazem o trabalho de verdade aqui:
frame_availableé a trava. O laço de captura só tira um novo snapshot quando ele éFalse– ou seja, quando o host puxou o último byte do quadro anterior. A leitura do host o redefine de volta paraFalsede dentro dereadpassim que o offset final é servido. Sem essa proteção, o próximocsi0.snapshot()sobrescreveria o buffer no meio da leitura e o host receberia um quadro costurado a partir de duas capturas.readpem vez dereadé o que o backend implementa. A biblioteca do protocolo trata o buffer retornado como autoritativo e lê seus bytes diretamente para o pacote de saída – sem cópia. Para payloads do tamanho de um quadro,readpé notavelmente mais rápido queread, que força uma cópia intermediária.sizeretorna o comprimento do JPEG em cache sem recalcular nada; o laço de captura o mantém sempre que atualiza o buffer. O host chamasizeentrepollereadppara saber quantos bytes deve puxar.send_event()notifica o host no instante em que um novo quadro chega, para que ele possa começar a puxar sem fazer polling. O ID de evento0x01é definido pela aplicação (“frame ready” neste caso); use um inteiro pequeno diferente para cada tipo de notificação.
12.8.2. Fragmentação¶
QVGA RGB565 com qualidade JPEG 85 comprime para aproximadamente 10-25 KB, dependendo da cena – muito maior que o payload máximo negociado em qualquer câmera (consulte a tabela por placa em protocol.init()). Uma leitura de JPEG não cabe em um único pacote, e tudo bem, porque a biblioteca do protocolo o fragmenta de forma transparente.
Quando o host solicita channel_read('frame', 12000):
O
readpda câmera é chamado uma vez comoffset=0e a requisição completa de 12000 bytes. Ele retorna uma única memoryview cobrindo toda a faixa.A biblioteca do protocolo quebra essa memoryview em fragmentos do tamanho do payload máximo na linha, um pacote de resposta
CHANNEL_READpor fragmento, cada um com seu próprio cabeçalho e CRC. Os bytes são transmitidos diretamente do buffer do backend – sem cópia.O host recebe os fragmentos em ordem, a camada de confiabilidade retransmite qualquer pedaço que falhe no CRC, e o SDK do host cola os pedaços no resultado de 12000 bytes retornado ao chamador.
Nota
Essa é a diferença prática chave entre readp e read. readp é chamado uma vez por requisição do host; a camada de protocolo fragmenta e transmite a partir do único buffer retornado. read é chamado uma vez por fragmento, e a biblioteca copia cada pedaço retornado para seu próprio buffer de pacote. Para payloads do tamanho de um quadro, readp economiza tanto a sobrecarga de chamada em nível de Python por fragmento quanto a cópia.
Dica
Quer ver a diferença você mesmo? Renomeie o método readp do backend para read – nada mais muda; a biblioteca passará a usar a capacidade read – e compare o contador de taxa de quadros do host antes e depois. O número mais lento é o custo de cópia por fragmento e de chamada Python que você evita ao usar readp.
A trava em FrameChannel.readp libera o buffer quando offset + size == img_size – o momento em que o host puxou o último byte. Até lá, o buffer deve permanecer válido, e é por isso que o laço de captura só tira o próximo snapshot quando frame_available volta para False.
12.8.3. O lado do host¶
O host puxa quadros em um laço apertado:
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
A chamada channel_size() funciona também como uma verificação de “há algo pronto” – zero significa que a câmera ainda não capturou – de modo que o laço pula tentativas de leitura em um buffer vazio. Para aplicações com interface gráfica que já fazem polling em um timer, esse é o padrão natural.
O Image.open do Pillow decodifica o JPEG; a câmera já o comprimiu em JPEG, então o host não precisa refazer o caro empacotamento de bits do RGB565. O script do host poderia, com a mesma facilidade, salvar os bytes em disco, entregá-los ao OpenCV ou enviá-los através de uma visualização web.
12.8.4. Pensando em throughput¶
Três coisas limitam a taxa de quadros alcançável:
A taxa de captura da câmera. O protocolo não consegue entregar quadros mais rápido do que o sensor os produz; qualquer limite que o formato de pixel e o tamanho de quadro escolhidos imponham à captura é o teto.
O payload máximo negociado. Payloads maiores significam menos fragmentos por quadro e menos sobrecarga de enquadramento, então câmeras com buffers de protocolo maiores movem bytes mais rápido que as menores.
Sobrecarga de CRC e ACK. Cada pacote custa 14 bytes de enquadramento mais um round-trip de ACK. Para fragmentos longos, a sobrecarga por payload é pequena; para payloads minúsculos, ela domina.
Para a maior parte do trabalho de interface gráfica entre câmera e laptop, o fator limitante é o tempo de captura e de compressão JPEG da câmera, não a pilha do protocolo. Onde o protocolo de fato se torna o gargalo – transmitindo quadros brutos não comprimidos em altas taxas de quadros, por exemplo – as alavancas são desligar os ACKs (protocol.init(ack=False)), aumentar o buffer do protocolo se a câmera o suportar, ou capturar em GRAYSCALE para que cada JPEG comprimido carregue um canal em vez de três e o quadro codificado acabe ficando notavelmente menor na linha.
O canal de quadros é o fluxo de dados canônico de câmera para host. A mesma interface de backend, com um método write adicionado, permite que o host envie dados no sentido oposto também – que é o que uma ferramenta de câmera interativa precisa assim que o operador quer mudar algo em vez de apenas observar.