No guia da câmera térmica de celular mostramos que a TOPDON TC001 e a InfiRay P2 Pro são webcams UVC: o Linux as enxerga sem driver nenhum. Este post vai um passo além do que o aplicativo faz: gravar a matriz de temperatura bruta (192 × 256 pontos, 25 vezes por segundo) e extrair, por um script Python de 50 linhas, o ponto mais quente de cada segundo, com posição e temperatura, num CSV que você abre no gnuplot ou no Calc.

Última verificação: 15 de outubro de 2026. O formato do quadro vem do código-fonte dos projetos citados, aberto por nós nesta data; o script foi testado aqui com uma gravação sintética de demonstração no mesmo formato (não tínhamos a câmera ligada neste Linux, e dizemos isso em cada número que vem dela). Não vendemos nenhuma dessas câmeras.

Conteúdo da Página

O que o Linux recebe da câmera

Ligada na porta USB, a câmera aparece como um dispositivo de vídeo comum. Numa issue aberta em fevereiro de 2025 no repositório do PyThermalCamera, um usuário de Debian 12 colou o lsusb da TC001: ID 3474:4321 “Thermal Cam Co.,Ltd Camera”, classe 239/2/1 (dispositivo composto UVC), autoalimentada com 100 mA declarados. A P2 Pro usa outro par: o código do P2Pro-Viewer procura por 0bda:5830. Nos dois casos o módulo do kernel é o uvcvideo, o mesmo do microscópio USB, e o nó é /dev/videoN.

O segredo está nos modos de vídeo. O autor do PyThermalCamera deixou no próprio arquivo tc001-RAW.py a saída do v4l2-ctl --list-formats-ext da sua máquina: formato YUYV, em dois tamanhos, 256 × 192 e 256 × 384, ambos a 25 quadros por segundo. O primeiro é só a imagem. O segundo é a imagem com um “porão”: a metade de cima são os 192 × 256 pixels de vídeo, a metade de baixo são os mesmos 192 × 256 pontos como temperatura em 16 bits, little-endian. A fórmula, reconstruída por engenharia reversa por LeoDJ no fórum EEVblog e usada nos dois projetos, é:

T (°C) = valor / 64 − 273,15

Ou seja, 1 unidade equivale a 1/64 de kelvin (0,0156 °C) e o máximo de 16 bits (65.535) daria 750,8 °C, folga de sobra para a faixa de 550 °C da câmera. Cada quadro de 256 × 384 ocupa 196.608 bytes; a 25 por segundo são 4,9 MB/s, 295 MB por minuto, 17,7 GB por hora. Logo adiante mostramos como comprimir sem perder nada.

Câmera térmica TOPDON TC001 azul, com conector USB-C e a marca TCView, sobre fundo branco
A TOPDON TC001: no Linux ela aparece como “Thermal Cam Co.,Ltd Camera” (3474:4321) e entrega dois modos de vídeo, 256 × 192 e 256 × 384. Foto: Laserlicht, Wikimedia Commons, CC BY-SA 4.0.

O que instalar (tudo pelo gerenciador de pacotes)

Nada vem de site de terceiros. Os três pacotes são os da sua distribuição, nas versões que conferimos hoje nos repositórios oficiais:

Pacote Para quê Ubuntu 24.04 Debian 12 Debian 13
v4l-utils v4l2-ctl: listar modos e gravar os quadros crus 1.26.1 1.22.1 1.30.1
ffmpeg gravar e comprimir sem perda 6.1.1 5.1.9 7.1.5
python3-opencv + python3-numpy ler a câmera ao vivo pelo script OpenCV 4.6.0 4.6.0 4.10.0
sudo apt install v4l-utils ffmpeg python3-opencv python3-numpy
sudo usermod -aG video $USER     # e saia/entre de novo na sessão

Se preferir o pip, o pacote oficial é o opencv-python-headless (PyPI): a versão atual é a 5.0.0.93, de 2 de julho de 2026, wheel manylinux2014 de 56,6 MB, SHA-256 começando em 09a872a1. Nosso teste rodou com a 4.12.0.88 e numpy 2.2.6, dentro de um venv. Para só analisar gravações, o script dispensa o OpenCV: basta numpy.

Passo 1: confirmar o modo 256 × 384

v4l2-ctl --list-devices
v4l2-ctl -d /dev/video0 --list-formats-ext

O primeiro comando lista as câmeras e o nó de cada uma (se houver webcam embutida no notebook, a térmica costuma ser /dev/video2, não a 0). O segundo precisa mostrar o YUYV em 256×384; se mostrar só 256×192 ou outra coisa, você está no dispositivo errado. O ffmpeg -f v4l2 -list_formats all -i /dev/video0 dá a mesma lista.

Passo 2: gravar os quadros crus

Duas receitas, as duas sem Python. Pelo v4l2-ctl, que grava os buffers exatamente como saem da câmera (as opções são as do manual da versão 1.26: --stream-mmap captura por mmap com 3 buffers, --stream-count é o número de quadros, --stream-to o arquivo):

v4l2-ctl -d /dev/video0 --set-fmt-video=width=256,height=384,pixelformat=YUYV 
  --stream-mmap --stream-count=1500 --stream-to=grav.yuv     # 1500 quadros = 60 s

Pelo ffmpeg, que permite copiar sem recodificar (a opção -c:v copy com saída rawvideo; conferimos aqui que o arquivo de saída sai byte a byte igual ao de entrada):

ffmpeg -f v4l2 -input_format yuyv422 -video_size 256x384 -framerate 25 
  -i /dev/video0 -t 60 -c:v copy -f rawvideo grav.yuv

Para gravar horas, o truque é o do P2Pro-Viewer: ele separa a metade térmica como vídeo gray16le e comprime com FFV1, um codec sem perda que o ffmpeg traz de fábrica. Os 16 bits de cada ponto voltam intactos na leitura, e uma placa parada comprime muito, porque quase nada muda entre dois quadros.

Passo 3: o script que acha o ponto mais quente

Salve como ponto-quente.py. Ele lê uma gravação (--arquivo) ou a câmera ao vivo (--dispositivo N), agrupa os quadros por segundo e guarda, para cada segundo, o quadro em que a temperatura máxima foi maior: segundo, temperatura e posição (x, y) do ponto mais quente, além do mínimo, da média e do centro da imagem.

#!/usr/bin/env python3
# ponto-quente.py: TC001 / P2 Pro, quadros 256x384 (metade de baixo = 16 bits LE)
import argparse, csv, sys
import numpy as np

W, H = 256, 192                 # sensor
QUADRO = W * (2 * H) * 2        # bytes por quadro YUYV 256x384 = 196.608

def temperaturas(quadro):
    """Matriz 192x256 em °C a partir dos bytes de um quadro 256x384."""
    bruto = np.frombuffer(quadro, dtype='<u2', offset=W * H * 2, count=W * H)
    return bruto.reshape(H, W).astype(np.float32) / 64.0 - 273.15

def quadros_arquivo(caminho):
    with open(caminho, 'rb') as f:
        while (q := f.read(QUADRO)) and len(q) == QUADRO:
            yield q

def quadros_camera(n):
    import cv2
    cap = cv2.VideoCapture(f'/dev/video{n}', cv2.CAP_V4L2)
    cap.set(cv2.CAP_PROP_CONVERT_RGB, 0)   # bytes YUYV crus, sem converter em BGR
    while True:
        ok, fr = cap.read()
        if not ok:
            return
        if fr.size != QUADRO:
            sys.exit(f'quadro com {fr.size} bytes, esperava {QUADRO}: o modo 256x384 não abriu')
        yield fr.tobytes()

ap = argparse.ArgumentParser()
ap.add_argument('--arquivo', help='gravação .yuv (256x384 YUYV)')
ap.add_argument('--dispositivo', type=int, help='número N de /dev/videoN')
ap.add_argument('--fps', type=int, default=25)
ap.add_argument('--saida', default='ponto-quente.csv')
a = ap.parse_args()
fonte = quadros_arquivo(a.arquivo) if a.arquivo else quadros_camera(a.dispositivo)

with open(a.saida, 'w', newline='') as f:
    w = csv.writer(f)
    w.writerow(['segundo', 't_max', 'x', 'y', 't_min', 't_media', 't_centro'])
    seg_atual, melhor = -1, None
    for i, q in enumerate(fonte):
        t = temperaturas(q)
        seg = i // a.fps
        if seg != seg_atual and melhor is not None:
            w.writerow(melhor); f.flush(); print(*melhor, sep='t')
            melhor = None
        seg_atual = seg
        y, x = np.unravel_index(t.argmax(), t.shape)
        linha = [seg, round(float(t[y, x]), 2), int(x), int(y), round(float(t.min()), 2),
                 round(float(t.mean()), 2), round(float(t[H // 2, W // 2]), 2)]
        if melhor is None or linha[1] > melhor[1]:
            melhor = linha              # guarda o quadro mais quente do segundo
    if melhor is not None:
        w.writerow(melhor); print(*melhor, sep='t')

Três decisões de projeto, cada uma por causa de um erro real relatado nos programas prontos (lista mais abaixo): a temperatura é lida direto como inteiro de 16 bits, nunca como dois bytes multiplicados; o máximo é procurado na matriz já convertida, não no byte alto; e o script aborta com mensagem clara se o quadro não tiver 196.608 bytes.

python3 ponto-quente.py --arquivo grav.yuv           # analisa a gravação
python3 ponto-quente.py --dispositivo 2 --saida bancada.csv   # ou ao vivo

O que saiu no nosso teste

Geramos uma gravação de demonstração no formato exato da câmera: 30 segundos, 750 quadros, 147.456.000 bytes. A cena simula uma placa a 24 °C com um regulador fixo em 41 °C na posição (60, 120) e um MOSFET na posição (170, 80) que aquece de 26 °C até 78 °C com constante de tempo de 12 s, mais o ruído de 0,04 °C que a ficha da câmera declara (NETD 40 mK). Os números abaixo são dessa simulação, não de uma placa real; o formato dos bytes, sim, é o real.

O script processou os 147 MB em 0,24 s nesta máquina e escreveu 30 linhas. Nos segundos 0 a 3, o ponto mais quente é o regulador: 41,4 °C em (60, 120). No segundo 4 a posição salta para (170, 80), com 44,4 °C: o MOSFET passou o regulador. No segundo 29 ele chega a 74,6 °C. Essa coluna x,y é a informação que o aplicativo do celular não registra: ela diz qual componente venceu e quando.

Mapa de calor de 256 por 192 pontos em tons de preto, roxo e laranja, com um círculo branco marcando o ponto mais quente a 74,5 graus em x igual a 170 e y igual a 80 e uma mancha menor marcada como regulador a 41 graus
A matriz de temperatura do último quadro da gravação de demonstração, reconstruída pelo script a partir dos 16 bits de cada ponto: o círculo marca o ponto mais quente (74,5 °C em x = 170, y = 80); a mancha menor é o regulador a 41 °C. Figura nossa.
Gráfico de linha da temperatura máxima por segundo subindo de 41 para 75 graus em 30 segundos, com uma linha tracejada no segundo 4 marcando a troca de posição do ponto mais quente
A coluna t_max do CSV, segundo a segundo: a curva fica plana enquanto o regulador é o ponto mais quente e começa a subir no segundo 4, quando o MOSFET assume. Para desenhar o gráfico, vale a receita do post do gnuplot e do Calc. Figura nossa.

Os 5 problemas mais comuns e a correção

  1. “OverflowError: Python integer 256 out of bounds for uint8”. Relatado em novembro de 2024 na issue 21 do PyThermalCamera: o código original multiplica o byte alto por 256 ainda como uint8, e o numpy 2 passou a recusar isso. A correção sugerida lá é converter para uint16 antes; o nosso script evita o problema lendo os dois bytes de uma vez como '<u2'.
  2. “IndexError: index 96 is out of bounds for axis 0 with size 0”. Issue 15 (fevereiro de 2024, com relatos até 2025): o quadro chegou vazio. Num relato a causa foi um cabo OTG caseiro; noutro, a conversão para RGB que o OpenCV faz por padrão. Mantenha o CAP_PROP_CONVERT_RGB em 0 e ligue a câmera direto, sem hub.
  3. “IndexError: index 480 is out of bounds for axis 0 with size 192”. Issue 27 (fevereiro de 2026, Raspberry Pi 4, OpenCV 4.6): o quadro recebido não tinha 256 × 384, porque o número do dispositivo era o de outra câmera ou o modo aberto foi o de 192 linhas. O script acima para na hora com “quadro com N bytes, esperava 196608”.
  4. Mínimo travado em −209,17 °C e uma linha de artefatos no rodapé. Issue 22 (fevereiro de 2025, Debian 12). A conta explica: −209,17 °C corresponde ao valor bruto 4.095, ou seja, a última linha do quadro veio com dado inválido. Se isso aparecer na sua câmera, calcule o mínimo e a média ignorando a última linha (t[:-1]); o máximo não é afetado, porque o lixo é um valor baixo.
  5. Temperatura máxima errada de vez em quando. Issue 1, julho de 2023: o programa procurava o máximo só no byte alto e depois lia o byte baixo daquele pixel, o que erra quando dois pixels empatam no byte alto. A solução proposta na própria issue é combinar os 16 bits antes de procurar; é o que o argmax sobre a matriz convertida faz.

E um aviso que não é bug: o estalo periódico da câmera é o obturador de calibração. Como o script guarda o quadro mais quente de cada segundo, um quadro estranho desse instante aparece como um ponto fora da curva; se incomodar, troque o critério por mediana do segundo.

Perguntas frequentes

A temperatura do CSV é a mesma que o aplicativo mostra? É a temperatura que a própria câmera calcula e envia nos 16 bits, com a emissividade e a distância que estão configuradas nela. O script não corrige nada; um dissipador de alumínio polido continua “mentindo para baixo”, como no post anterior.

Funciona no Raspberry Pi? O PyThermalCamera foi escrito para ele e o Thermal-Camera-Redux relata testes em modelos do Zero 2 W ao 5. O nosso script usa só numpy (e OpenCV para o modo ao vivo), ambos disponíveis no apt do Raspberry Pi OS.

Precisa de regra udev? Não, diferente do analisador lógico: a câmera é UVC e o nó /dev/videoN já nasce acessível ao grupo video. Se der “Permission denied”, é o seu usuário que não está no grupo.

Dá para fazer o mesmo no Windows? O formato é o mesmo, mas os relatos de quadro vazio nas issues vêm justamente do Windows; o fork PyThermalCamera-Windows, citado no post anterior, é o caminho mais curto lá.

Onde comprar: as duas câmeras são vendidas no Brasil pelos revendedores oficiais e pela Amazon; para deixar a câmera parada sobre a placa por minutos, um mini tripé resolve.

Como Associados da Amazon, recebemos por compras qualificadas feitas pelos links acima — você não paga nada a mais por isso, e ajuda o site a continuar gratuito.

Veredito honesto

Com três pacotes do apt e 50 linhas de Python, a câmera térmica de celular vira um registrador: você alimenta a placa, deixa gravando e o CSV diz qual ponto esquentou primeiro e em que segundo passou os outros. É a prova que falta no defeito que só aparece quente. A parte que ninguém conta: o formato dos 16 bits é fruto de engenharia reversa, não de documentação do fabricante (a TOPDON nunca respondeu ao pedido de especificação, segundo o autor do Redux), e os nossos números de teste vieram de uma gravação sintética. Na placa de verdade, confirme o componente apontado com o método do multímetro antes de trocar qualquer coisa.

Veja também

Quando o CSV apontar o componente e a pergunta virar “o que esse estágio da placa faz”, o Mapa da Placa é o nosso material de leitura de placa; vale conhecer.