12.8. Transmissão de fotogramas¶
A utilização real mais comum de um canal personalizado é a transmissão de fotogramas de imagem da câmara para um programa anfitrião à taxa de fotogramas da câmara. A mecânica é mais subtil do que parece: um JPEG pode atingir 25 KB ou mais, pelo que o anfitrião o lê em vários fragmentos, e o ciclo de captura da câmara deve ser impedido de sobrescrever o buffer durante a leitura. O padrão correto – apresentado aqui e utilizado pelas ferramentas em openmv-projects/tools/ – bloqueia o buffer até o anfitrião terminar de ler o último byte.
12.8.1. O lado da câmara¶
Um canal de fotogramas que captura para um único framebuffer, bloqueia-o na primeira leitura do anfitrião e só tira a próxima captura de imagem após o anfitrião ter consumido 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 componentes estão a fazer trabalho real aqui:
frame_availableé o bloqueio. O ciclo de captura só tira uma nova captura de imagem quando estáFalse– o que significa que o anfitrião já leu o último byte do fotograma anterior. A leitura do anfitrião repõe-o aFalsedentro dereadpassim que o deslocamento final for servido. Sem esta guarda, o próximocsi0.snapshot()sobrescreveria o buffer durante a leitura e o anfitrião receberia um fotograma composto por duas capturas.readpem vez dereadé o que o backend implementa. A biblioteca de protocolo trata o buffer devolvido como autoritativo e lê os seus bytes diretamente para o pacote de saída – sem cópia. Para payloads do tamanho de fotogramas,readpé visivelmente mais rápido do queread, que força uma cópia intermédia.sizedevolve o comprimento JPEG em cache sem recalcular nada; o ciclo de captura mantém-no sempre que atualiza o buffer. O anfitrião chamasizeentrepollereadppara saber quantos bytes deve ler.send_event()notifica o anfitrião no instante em que um novo fotograma chega, para que possa começar a ler sem fazer polling. O ID de evento0x01é definido pela aplicação («fotograma pronto» 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 do que o payload máximo negociado em qualquer câmara (consulte a tabela por placa em protocol.init()). Uma leitura JPEG não cabe num único pacote, o que é aceitável, porque a biblioteca de protocolo fragmenta-o de forma transparente.
Quando o anfitrião pede channel_read('frame', 12000):
O
readpda câmara é chamado uma vez comoffset=0e o pedido completo de 12000 bytes. Devolve uma memoryview que abrange todo o intervalo.A biblioteca de protocolo divide essa memoryview em fragmentos do tamanho máximo do payload no fio, um pacote de resposta
CHANNEL_READpor fragmento, cada um com o seu próprio cabeçalho e CRC. Os bytes são transmitidos diretamente do buffer do backend – sem cópia.O anfitrião recebe os fragmentos por ordem, a camada de fiabilidade retransmite qualquer fragmento que falhe o seu CRC, e o SDK do anfitrião agrupa os fragmentos no resultado de 12000 bytes devolvido ao chamador.
Nota
Esta é a diferença prática fundamental entre readp e read. readp é chamado uma vez por pedido do anfitrião; a camada de protocolo fragmenta e transmite a partir do único buffer devolvido. read é chamado uma vez por fragmento, e a biblioteca copia cada fragmento devolvido para o seu próprio buffer de pacote. Para payloads do tamanho de fotogramas, readp poupa tanto a sobrecarga de chamada ao nível Python por fragmento como a cópia.
Dica
Quer ver a diferença por si mesmo? Renomeie o método readp do backend para read – mais nada muda; a biblioteca escolherá a capacidade read em vez disso – e compare o contador de taxa de fotogramas do anfitrião antes e depois. O número mais baixo é o custo de cópia por fragmento e de chamada Python que evita ao usar readp.
O bloqueio em FrameChannel.readp liberta o buffer quando offset + size == img_size – no momento em que o anfitrião leu o último byte. Até então, o buffer deve permanecer válido, razão pela qual o ciclo de captura só tira a próxima captura de imagem após frame_available voltar a False.
12.8.3. O lado do anfitrião¶
O anfitrião lê fotogramas num ciclo 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() serve também como verificação de «há algo pronto» – zero significa que a câmara ainda não capturou – pelo que o ciclo ignora tentativas de leitura num buffer vazio. Para aplicações com GUI que já fazem polling num temporizador, este é o padrão natural.
O Image.open do Pillow descodifica o JPEG; a câmara já o comprimiu em JPEG, pelo que o anfitrião não precisa de refazer o dispendioso empacotamento de bits em RGB565. O script do anfitrião poderia igualmente guardar os bytes em disco, passá-los ao OpenCV, ou enviá-los através de uma vista web.
12.8.4. Considerações sobre débito¶
Três fatores limitam a taxa de fotogramas alcançável:
A taxa de captura da câmara. O protocolo não pode entregar fotogramas mais rapidamente do que o sensor os produz; o limite imposto pelo formato de pixel e pelo tamanho de fotograma escolhidos à captura é o teto.
O payload máximo negociado. Payloads maiores significam menos fragmentos por fotograma e menos sobrecarga de enquadramento, pelo que câmaras com buffers de protocolo maiores movem bytes mais rapidamente do que as mais pequenas.
Sobrecarga de CRC e ACK. Cada pacote custa 14 bytes de enquadramento mais uma ronda de ACK. Para fragmentos longos, a sobrecarga por payload é pequena; para payloads pequenos, domina.
Para a maior parte do trabalho com GUI de câmara para portátil, o fator limitante é o tempo de captura e compressão JPEG da câmara, não a pilha de protocolos. Onde o protocolo se torna o estrangulamento – por exemplo, transmitir fotogramas brutos não comprimidos a altas taxas de fotogramas – as alavancas são desativar os ACKs (protocol.init(ack=False)), aumentar o buffer de protocolo se a câmara o suportar, ou capturar em GRAYSCALE para que cada JPEG comprimido transporte um canal em vez de três e o fotograma codificado acabe visivelmente menor no fio.
O canal de fotogramas é o fluxo de dados canónico da câmara para o anfitrião. A mesma interface de backend, com um método write adicionado, permite ao anfitrião enviar dados no sentido inverso também – o que uma ferramenta de câmara interativa necessita assim que o operador quer alterar algo em vez de apenas observar.