~/notes/android-camera-pipeline
Como uma imagem da câmera chega até um app AndroidO caminho de um frame no Android, do sensor até os bytes de imagem no app, passando por driver, Camera HAL, Camera Service, Camera2, ImageReader e YUV_420_888.
Você cria uma sessão de captura, registra um ImageReader como destino e, alguns frames depois, tem bytes de imagem na mão. Entre uma coisa e outra existe um caminho com vários donos diferentes: o sensor, o driver do fabricante, o Camera HAL, o Camera Service e o framework. Esta nota segue um único frame por esse caminho, explicando cada camada e por que ela existe.
A primeira coisa que costuma surpreender quem vem de desenvolvimento de app é que o app não manda na câmera. Ele descreve o que quer (resolução, formato, controles de exposição e foco) e recebe buffers de volta. Todo o resto é negociado por baixo.
A intuição: um pipeline de pedidos e frames
A documentação do HAL3 descreve o subsistema assim: a API modela a câmera como um pipeline que converte pedidos de captura em frames, numa relação 1:1. Cada pedido carrega a configuração daquele frame (resolução, formato de pixel, controles manuais de sensor, lente e flash, modos de 3A, processamento RAW para YUV, geração de estatísticas) e a lista de destinos onde as imagens devem ser colocadas.
Pense em um pedido como uma ficha técnica: “para o próximo frame, use estes parâmetros e escreva o resultado nestas superfícies”. Você pode pedir um frame único ou registrar um pedido repetitivo. Preview é exatamente isso: um pedido repetitivo que nunca para até você cancelar.
Guardadas as proporções, o caminho de um frame é este:
app (API Camera2, ou CameraX por cima dela)
|
| Binder: ICameraService, ICameraDeviceUser
v
Camera Service (processo cameraserver)
|
| interface do HAL (HIDL nas versões antigas, AIDL nas novas)
v
Camera HAL (implementação do fabricante)
|
v
driver / kernel + ISP
|
v
sensor de imagem
Vamos descer uma camada por vez.
Camada 1: do sensor ao driver
O sensor converte luz em valores digitais. A maioria dos sensores de celular captura cada pixel através de um filtro de cor (um padrão de Bayer), então a leitura bruta é um mosaico em que cada ponto tem só um dos componentes de cor. Isso é o RAW. Transformar esse mosaico em uma imagem colorida exige interpolação de cor (demosaicing), ajuste de balanço de branco, redução de ruído e correção de cor.
Esse trabalho é feito, em boa parte, no ISP (Image Signal Processor) e no bloco de imagem do SoC. O driver do kernel configura o sensor e o ISP conforme os parâmetros que vieram do pedido.
Aqui vale uma distinção que vai reaparecer no resto do texto: o Android não padroniza o que acontece abaixo do HAL. A fronteira padronizada para o fabricante é o HAL. Driver, ISP e o barramento físico entre sensor e SoC (normalmente MIPI CSI-2) são decisões de hardware e firmware do fabricante. É por isso que dois aparelhos com a mesma API se comportam de forma diferente em qualidade, latência e quais formatos suportam.
Camada 2: o Camera HAL
O AOSP precisa rodar em sensores de dezenas de fabricantes. Seria inviável embutir um driver específico para cada um no sistema base. A solução foi definir um contrato: o Camera HAL. Ele fica entre o driver e o resto do framework e é a interface que o fabricante implementa. O contrato garante que, do ponto de vista do Android, todo aparelho exponha a câmera da mesma forma.
No HAL3, esse contrato é orientado a requisições. O framework submete pedidos, e cada pedido vira um frame capturado pelo sensor, processado e entregue nos destinos configurados. O HAL é quem traduz os parâmetros abstratos do pedido em comandos concretos para o hardware e cuida da sincronização dos frames dentro do pipeline.
A memória dos buffers não fica com o HAL. Quem aloca é o framework: até o Android 9 ele já entregava buffers pré-alocados junto do pedido, e a partir do Android 10 o HAL passa a requisitá-los sob demanda ao framework (requestStreamBuffers) em vez de manter um conjunto reservado.
Dois pontos que valem registro:
- O modelo de pipeline é virtual. A documentação deixa claro que ele não corresponde diretamente a nenhum ISP real. Ele é uma abstração para manter compatibilidade entre fabricantes e fornecedores de sensor.
- A fronteira entre o Camera Service e o HAL é tratada como fronteira de segurança. Parâmetros que chegam do serviço são considerados não confiáveis, e o HAL é obrigado a validá-los antes de usá-los.
Nas versões antigas o HAL podia ser uma biblioteca carregada dentro de outro processo. A partir do Android 8.0, cada HAL de câmera binderizado roda em um processo separado do Camera Service. Isso existe por isolamento: se o código do fabricante travar ou for comprometido, ele não derruba nem contamina o resto da pilha.
Camada 3: o Camera Service
Por que não deixar o app falar direto com o HAL? Porque a câmera é um recurso compartilhado e sensível. O Camera Service (o processo cameraserver) centraliza o acesso: é ele que enumera os dispositivos, decide quem pode abrir a câmera e serializa o uso entre apps. Sem essa camada, cada app teria que negociar o hardware sozinho, e nada impediria duas aplicações de brigar pelo sensor ao mesmo tempo.
O app conversa com o serviço por Binder. ICameraService é a interface geral do serviço, ICameraDeviceUser é a interface de um dispositivo já aberto, e há interfaces de callback para o serviço avisar o app sobre eventos e o app receber os resultados.
Também é importante saber que essa camada nem sempre foi separada. O Android 7.0 tirou o Camera Service de dentro do mediaserver justamente para endurecer a segurança da mídia e da câmera.
Um detalhe de desempenho: os buffers de imagem não são copiados ao atravessar essas fronteiras. Eles são passados por referência através de um BufferQueue, que conecta quem produz os buffers (a câmera) a quem consome (o seu app, a tela, o encoder de vídeo). Copiar um frame de vários megabytes a cada etapa seria caro demais, então o sistema passa apenas o descritor do buffer e deixa a memória onde está.
Camada 4: a API Camera2
Do lado do app, o ponto de entrada é o CameraManager, que lista os dispositivos. Você abre um CameraDevice, cria uma CameraCaptureSession com a lista de destinos e monta um CaptureRequest.
A API Camera2 é a interface de baixo nível do framework, e a recomendação atual para a maioria dos apps é usar a CameraX, uma biblioteca do Jetpack que roda por cima da Camera2 e esconde a maior parte da configuração que varia por aparelho. As duas suportam Android 5.0 (API 21) ou superior.
Um esqueleto de captura de frames em YUV, simplificado para leitura:
val reader = ImageReader.newInstance(
width, height,
ImageFormat.YUV_420_888,
/* maxImages = */ 2
)
reader.setOnImageAvailableListener({ r ->
val image = r.acquireLatestImage() ?: return@setOnImageAvailableListener
try {
val planes = image.planes // sempre Y, U e V, nessa ordem
val y = planes[0].buffer
// processa...
} finally {
image.close() // devolve o buffer para a fila
}
}, handler)
val request = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
request.addTarget(reader.surface) // o destino é a Surface do ImageReader
session.setRepeatingRequest(request.build(), null, handler)
Dois limites que aparecem rápido na prática. Primeiro, só um número pequeno de superfícies de saída fica configurado ao mesmo tempo (algo em torno de três). Se você quer preview, gravação e análise simultâneas, está disputando esses slots. Segundo, os destinos precisam ser escolhidos antes: cada superfície recebe um fluxo de buffers em resolução fixa.
Camada 5: Surface, ImageReader e os buffers
Uma Surface é o destino onde o frame é escrito. Na prática, ela é o lado do produtor de um BufferQueue: a câmera escreve, quem consumir a superfície lê. O sistema aloca a memória por baixo via gralloc, o alocador de gráficos do Android, que decide o layout conforme o uso (exibição, vídeo, CPU).
O ImageReader é a ponte entre esse mundo de buffers e o seu código Java/Kotlin. Ele oferece uma Surface que você entrega ao pedido de captura e, em troca, entrega objetos Image sempre que um frame novo fica disponível. O número passado em maxImages limita quantas imagens podem estar em voo ao mesmo tempo.
Esse limite é a origem de um bug clássico. Se você não chamar close() na imagem depois de processar, o buffer nunca volta para a fila e em poucos frames você para de receber dados. Por isso acquireLatestImage() seguido de close() dentro de um finally é o padrão comum: você pega o frame mais recente e descarta os antigos, em vez de acumular atraso.
Camada 6: YUV_420_888 e onde os pixels moram
A essa altura você tem um Image, não um array de RGB. O formato mais comum vindo da câmera é YUV_420_888, e entender o nome ajuda bastante.
YUV separa luminância (Y) de crominância (U e V, também chamadas de Cb e Cr). O “420” indica a subamostragem de cor: a informação de brilho é guardada para cada pixel, mas a de cor só para cada bloco 2x2. Como o olho é bem mais sensível a brilho do que a cor, isso reduz muito os dados sem perda visível. O “888” indica 8 bits por amostra.
Na API, esse formato é exposto como três planos. O plano 0 é o Y, o plano 1 é o U e o plano 2 é o V, nessa ordem. Um frame de W x H fica mais ou menos assim:
YUV_420_888 (4:2:0), três planos
plano 0 Y W x H um byte por pixel
plano 1 U W/2 x H/2 crominância Cb
plano 2 V W/2 x H/2 crominância Cr
memória (exemplo de layout planar)
Y[0] Y[1] Y[2] ... W*H bytes
U[0] U[1] ... (W/2)*(H/2) bytes
V[0] V[1] ... (W/2)*(H/2) bytes
Aqui está a parte que quebra código ingênuo. O YUV_420_888 é genérico de propósito: ele descreve qualquer buffer 4:2:0 planar ou semiplanar, mas não totalmente intercalado. “Semiplanar” quer dizer que U e V podem estar intercalados na mesma região de memória, um byte depois do outro, em vez de em blocos separados. Nesse caso os planos 1 e 2 apontam para a mesma memória, só com deslocamentos diferentes.
Por isso você não deve assumir que os três planos são regiões contíguas e compactas. Cada plano traz duas informações obrigatórias:
rowStride: quantos bytes existem entre o início de uma linha e o início da próxima. Pode ser maior que a largura, por causa de alinhamento.pixelStride: quantos bytes separam dois pixels vizinhos dentro da linha. Para o Y é sempre 1. Para U e V, se for 2, os dois componentes estão intercalados.
Ler um plano é andar de rowStride em rowStride e pegar um pixel a cada pixelStride. Ignorar esses valores produz imagens rasgadas ou com cores trocadas em alguns aparelhos e não em outros, o que torna o bug especialmente chato de reproduzir.
Como a maioria das bibliotecas de visão computacional e de exibição espera RGB, o passo seguinte costuma ser converter de YUV para RGB. A CameraX já oferece isso na análise de imagens, permitindo pedir a saída em RGBA_8888 em vez de YUV_420_888.
Onde acaba o contrato e começa o fabricante
Vale separar o que é garantido pela plataforma do que é escolha de implementação:
| Parte | Quem define |
|---|---|
| Existe um HAL e um Camera Service, com as interfaces Binder | Contrato do Android |
| Y, U e V como três planos, nessa ordem | Contrato da API |
| Processos separados para o HAL, a partir do Android 8.0 | Contrato do Android |
| Quais resoluções e formatos são suportados | Capacidade do aparelho |
| Layout real em memória (planar ou semiplanar), strides | Implementação do fabricante |
| Qualidade de imagem, latência, comportamento de 3A | Hardware, firmware e tuning do fabricante |
Da mesma forma, alguns comportamentos mudam com a versão do Android. O Camera Service saiu do mediaserver no Android 7.0, e o HAL binderizado ganhou processo próprio no Android 8.0. Se você estiver depurando em um aparelho antigo, isso muda quem é quem na pilha de processos.
Onde observar isso em um app real
Se você quer ver esse pipeline funcionando de verdade, o lugar natural é instrumentar um app e olhar o que ele faz: quais superfícies registra, o que pede ao CameraManager, quais chamadas de sistema aparecem quando os buffers chegam. O ARTEMIS é o projeto do site que faz exatamente esse tipo de observação em apps Android, com instrumentação e captura de syscalls e de rede.
Para a leitura oficial, estas são as fontes que sustentam a nota:
- Camera e Camera HAL, a arquitetura e o modelo de requisições do HAL3.
- HAL subsystem, sobre o pipeline virtual, o 3A e os controles.
- Camera version support, sobre a saída do
mediaserverno Android 7.0 e a separação do HAL no Android 8.0. - BufferQueue and Gralloc, sobre produtor, consumidor e alocação de buffers.
- Buffer management API, sobre quem aloca os buffers e a mudança trazida no Android 10.
- Camera2 overview e CameraX overview.
ImageReader,ImageeImageFormat, para planos, strides e o formatoYUV_420_888.