Inovação técnica · leitura objetiva do alizarol

O alizarol lido por câmera, não a olho.

O alizarol é o único teste de qualidade feito hoje na porteira, e é o motorista quem decide, olhando um tubo contra a luz, se o leite entra na carga. A câmara óptica do LactiGuard fotografa o mesmo tubo em condições controladas e devolve dois números, um índice de grumos e um tom de cor, com as fotos guardadas como prova. Esta página explica como, incluindo o código.

Em uma frase

Luz padronizada, câmera com parâmetros travados, um cartão de referência de cor no quadro e dois algoritmos simples: um mede a cor do líquido no espaço CIELAB, o outro conta partículas no filme que escorre na parede do vidro.

custo da câmara: abaixo de R$ 700 · processamento: 2 s no Raspberry Pi
O que a câmera precisa ver

O resultado visual do alizarol tem duas dimensões, e elas precisam ser medidas separadamente.

O reagente é álcool a 72% (v/v, pela IN 76) com alizarina, um indicador de pH. O álcool provoca coagulação se a proteína estiver instável; a alizarina muda de cor conforme o pH. O olho humano mistura as duas informações numa leitura só; a câmera as separa.

Dimensão 1 · a cor do líquido

Vem da alizarina e diz se o leite está ácido, normal ou alcalino. É lida como um ângulo de tom (h) no espaço de cor CIELAB, depois de corrigir o balanço de branco pelo cartão de referência.

rosado a vermelho-tijolo: normal (pH 6,6–6,8)amarelo a pardo: ácido (pH < 6,5)violeta a lilás: alcalino (mastite avançada, neutralizante)

Dimensão 2 · os grumos

Vêm do álcool e dizem se a proteína está instável. São lidos como um índice de 0 a 100 a partir da textura e da contagem de partículas na foto do tubo inclinado, onde o filme escorre pela parede e os grumos ficam evidentes.

filme liso: estávelpartículas finas: instável (LINA ou acidez)coágulo denso: instável, fora de padrão
Cor (tom)GrumospH da sonda ESP32ClasseO que significa
Normal (rosado)ausentes6,6–6,8EstávelLeite dentro do padrão da IN 76 para o alizarol 72%.
Normal (rosado)presentes6,6–6,8LINALeite instável não ácido: proteína instável por desequilíbrio mineral ou nutricional; não é acidez.
Amarelo/pardopresentes< 6,5ÁcidoFermentação por bactérias: refrigeração ou higiene falhas. Barrar.
Violeta/lilásausentes> 6,9AlcalinoMastite avançada ou neutralizante adicionado; cruzar com condutividade, CCS e kit de neutralizantes.

É este cruzamento que o olho humano não faz de forma confiável: cor normal com grumos é LINA, não acidez; grumos com pH baixo é acidez; violeta sem grumos com pH alto sugere neutralizante.

A câmara física

Uma caixa fechada, duas luzes, uma câmera travada e um gesto reproduzido por servo.

CAIXA OPACA · SEM LUZ EXTERNA · SENSOR DE TAMPA painel LED + difusor opalino 3 tubo 2 mL + 2 mL 2 berço + servo (inclina 45°) 5 Pi Camera Module 3 AWB / exposição / ganho travados 1 12–15 cm fita LED frontal CRI ≥ 90 · 5000 K · difusor 4 cartão de cor + cinza 18% 6 marcadores ArUco 7 vista lateral · fora de escala · hub Raspberry Pi fora da caixa, ligado por cabo
1
Câmera · Raspberry Pi Camera Module 3 (12 MP, foco ajustável) a 12–15 cm do tubo. Balanço de branco, exposição e ganho travados em valores fixos: é a regra mais importante do projeto, porque qualquer automatismo muda a cor entre uma foto e outra.
2
Tubo · tubo de vidro padrão com 2 mL de leite e 2 mL de alizarol 72%, dosados com pipetas de volume fixo (bomba dosadora na versão seguinte). Berço fixo, sempre na mesma posição.
3
Luz de transmissão · painel de LED branco atrás do tubo, com difusor de acrílico opalino. É nela que os grumos aparecem como pontos escuros contra o fundo claro e que a cor do líquido é lida com mais estabilidade.
4
Luz frontal · fita de LED de alto CRI (≥ 90, 5000 K) atrás de um difusor, para a segunda foto: o filme na parede do vidro. As duas luzes são comutadas pelo hub; nunca acendem juntas.
5
Servo · SG90 no berço inclina o tubo a 45° e volta, reproduzindo o gesto do motorista. Na foto inclinada o filme escorre pela parede e os grumos ficam evidentes mesmo quando poucos.
6
Referências de cor · cartão cinza neutro (18%) para corrigir o balanço de branco em toda foto, e um cartão de referência de cores (ColorChecker mini ou impressão calibrada) para a matriz de correção semanal.
7
Marcadores ArUco · dois marcadores no berço para que o software encontre o tubo, o cinza e o cartão sozinho, sem depender de posição manual.
ItemEspecificaçãoCusto aproximado
CâmeraRaspberry Pi Camera Module 3, 12 MP, autofoco travado em distância fixaR$ 250–350
Painel de luz de fundoPainel LED branco 10×10 cm + difusor de acrílico opalino 3 mmR$ 60
Luz frontalFita LED CRI ≥ 90, 5000 K, 12 V, com difusorR$ 40
CaixaImpressa em 3D (PETG preto fosco) ou MDF pintado, vedada à luz, com sensor de tampaR$ 80
Berço e servoBerço impresso + servo SG90 + driver no hubR$ 25
ReferênciasCartão cinza 18% + ColorChecker mini (ou impressão calibrada) + marcadores ArUcoR$ 80–400
Dosagem2 pipetas de volume fixo de 2 mLR$ 60
Totalsem o hub Raspberry Pi, que já faz parte do kit< R$ 700

Procedimento padronizado

O resultado visual depende do gesto. Por isso o app guia o motorista nestes quatro passos e só libera a foto quando o tempo fixo é atingido.

PASSO 1Dosar2 mL de leite da amostra + 2 mL de alizarol 72% com as pipetas de volume fixo. Nada de "mais ou menos".
PASSO 2MisturarTrês inversões suaves do tubo. Agitar faz espuma, e espuma parece grumo.
PASSO 3Encaixar e fecharTubo no berço, tampa fechada. O sensor de tampa bloqueia a captura se houver luz externa.
PASSO 4Esperar 10 sTimer do app. A coagulação avança com o tempo, então a foto é sempre no mesmo instante. Captura automática.
O processamento, passo a passo

Seis passos, dois números, duas fotos guardadas como prova.

Tudo roda no próprio Raspberry Pi, em Python com picamera2, OpenCV e NumPy, em cerca de dois segundos por amostra. O resultado vai para o motor de regras e para o comprovante.

1

Capturar e localizar

Foto em formato sem perdas (PNG ou RAW) com os parâmetros travados. O software localiza os marcadores ArUco e recorta três regiões: o tubo, o cartão cinza e o cartão de cores. Nada depende de posição manual.

saída: três recortes em coordenadas fixas
2

Corrigir a cor

Calcula o ganho de cada canal (R, G, B) que torna o cartão cinza neutro e aplica ao quadro inteiro. Uma vez por semana, com o cartão de cores completo, recalcula uma matriz de correção 3×3 que compensa a deriva do LED.

saída: imagem com branco e cores de referência estáveis
3

Medir o tom do líquido

Converte a coluna central do líquido (sem o menisco e sem o fundo) de sRGB para CIELAB e mede a média de L*, a*, b*. O tom é o ângulo h = atan2(b*, a*). Pontos de partida: amarelo-pardo 70–100°, rosado a vermelho-tijolo 20–50°, violeta acima de 300°. Os limites reais saem da calibração, porque a gordura espalha luz e o lote de alizarol varia.

saída: tom h (graus), croma, L*
4

Medir os grumos

Na foto inclinada com luz frontal (e também na de transmissão): converte para cinza; mede textura pela variância do Laplaciano ou pelo desvio padrão local em janela de 15 px; aplica limiar adaptativo e conta componentes conexos maiores que o equivalente a 0,1 mm², somando a área total; mede a densidade de bordas (Canny). Leite estável dá filme liso; instável dá dezenas de partículas.

saída: índice de grumos 0–100 (combinação ponderada das três medidas)
5

Decidir

Instável se o índice passar do limiar calibrado, com uma faixa de incerteza de ±10 em que o app pede confirmação e usa o pH e a acidez estimada da sonda como desempate. A classe de cor é cruzada com o pH: grumos com pH baixo é acidez; grumos com pH normal é LINA; violeta sem grumos com pH alto sugere neutralizante.

saída: classe (estável / LINA / ácido / alcalino) + grau de confiança
6

Registrar

As duas fotos, os números, os parâmetros da câmera, o lote do reagente e o hash da coleta entram no comprovante do produtor e no registro do laticínio. É a prova que o alizarol a olho nunca deu.

saída: evidência auditável, anexada ao laudo
alizarol_camera.py · esqueleto do pipeline · Python 3 · picamera2 + OpenCV + NumPyRaspberry Pi 4/5
# captura com parâmetros travados (a regra mais importante)
cam.set_controls({"AwbEnable": False, "AeEnable": False,
                  "ExposureTime": 8000, "AnalogueGain": 1.0,
                  "ColourGains": (g_r, g_b)})          # g_r, g_b vêm da calibração
luz.transmissao()                                      # painel atrás do tubo
img = cam.capture_array()                              # foto 1: tubo em pé
tubo, cinza, cartao = recortar_por_aruco(img)          # regiões fixas pelo berço
img = corrigir_branco(img, cinza)                      # ganho por canal até o cinza ficar neutro

# dimensão 1: tom do líquido em CIELAB
lab = cv2.cvtColor(tubo_liquido, cv2.COLOR_RGB2LAB)    # coluna central, sem menisco e sem fundo
L, a, b = lab.reshape(-1, 3).mean(axis=0)
tom = np.degrees(np.arctan2(b - 128, a - 128)) % 360   # ângulo de tom (OpenCV centra a*, b* em 128)
classe_cor = classificar_tom(tom)                      # acido / normal / alcalino, limites calibrados

# dimensão 2: grumos no filme da parede
luz.frontal(); servo.inclinar(45); time.sleep(1.5)     # foto 2: filme escorrendo na parede
img2 = cam.capture_array(); parede = recortar_parede(img2)
g = cv2.cvtColor(parede, cv2.COLOR_RGB2GRAY)
textura = cv2.Laplacian(g, cv2.CV_64F).var()
mask = cv2.adaptiveThreshold(g, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
                             cv2.THRESH_BINARY_INV, 31, 8)
n, _, stats, _ = cv2.connectedComponentsWithStats(mask)
areas = stats[1:, cv2.CC_STAT_AREA]
area_grumos = areas[areas > AREA_MIN].sum()           # AREA_MIN ≈ 0,1 mm² em pixels
bordas = cv2.Canny(g, 60, 140).mean()
indice = escalar(w1*textura + w2*area_grumos/parede.size + w3*bordas)  # 0–100, pesos da calibração
servo.inclinar(0)

# decisão cruzada com a sonda de pH do nó ESP32
instavel = indice > LIMIAR                             # faixa de incerteza: LIMIAR ± 10 pede confirmação
classe = classificar(instavel, classe_cor, ph_sonda)     # estavel / lina / acido / alcalino
registrar(img, img2, dict(tom=tom, indice=indice, classe=classe,
                          exposicao=8000, ganhos=(g_r, g_b), lote=lote_alizarol))

As funções recortar_por_aruco, corrigir_branco, classificar_tom e escalar são utilitários curtos (cv2.aruco, aritmética de ganhos, limiares e normalização). Os pesos w1–w3, LIMIAR e os limites de tom são os parâmetros que saem da calibração abaixo e ficam num arquivo de configuração versionado por lote de reagente.

Calibração e validação

Um experimento de meio dia com leite real define os limites.

Três escadas de amostras, dez réplicas por ponto, três leitores humanos e o laboratório como referência. É assim que o índice deixa de ser uma opinião da câmera.

Escada de acidezÁcido lático adicionado a um leite normal até 15, 17, 18, 19, 20 e 22 °D, medidos por titulação Dornic no laboratório. Define o limiar de "instável" e os limites de tom na faixa ácida.
Escada de LINACloreto de cálcio em três concentrações: induz instabilidade proteica sem acidificar. Define o comportamento do índice de grumos com cor normal.
Escada alcalinaBicarbonato de sódio até pH 6,9 e 7,1. Define os limites de tom na faixa violeta e o cruzamento com a sonda de pH.
> 95%
sensibilidade para "instável" contra a referência (acidez ≥ 18 °D ou coagulação confirmada pelo painel)
> 90%
especificidade: leite estável não pode ser barrado por engano
< 10%
coeficiente de variação do índice entre réplicas da mesma amostra

Com as escadas se ajustam os pesos do índice, o limiar de "instável", os limites de tom contra o pH medido, e se calculam sensibilidade, especificidade, concordância com os leitores (kappa) e repetibilidade. Depois de algumas centenas de amostras de rota com laudo pareado, dá para trocar os pesos fixos por um classificador treinado nas mesmas características; isso é fase 3 do projeto, não fase 1.

Onde o sistema erra e como evitar

Os erros conhecidos já têm resposta no procedimento.

Espuma e bolhas parecem grumosMistura por inversão suave, nunca agitação; espera fixa de 10 s antes da foto. O app só libera a captura no tempo certo.
Condensação no vidroO leite sai a 4 °C e a cabine está quente. Berço levemente aquecido ou sopro de ar seco; foto em branco com tubo vazio no início do dia detecta vidro sujo ou riscado.
Álcool que evapora do frascoA concentração cai ao longo da semana e mascara instabilidade. Frasco lacrado, troca semanal e um leite-controle diário cujo resultado o app registra como verificação do reagente.
Luz externa vazandoMuda a cor e a textura. Vedação e sensor de luz que bloqueia a captura se a tampa estiver aberta.
Nata ou gordura no tuboDesloca a cor e cria textura falsa. Leitura na coluna central e normalização pela gordura que o analisador ultrassônico já mediu na mesma amostra.
Diferença entre lotes de alizarolCada lote tem um tom ligeiramente diferente. O leite-controle diário recentra os limites; o lote fica registrado no comprovante.
Extensão barata e útil: o berço pode ter dois tubos, um com alizarol 72% e outro com 76%, e o app gradua a instabilidade (estável nos dois, instável só no 76%, instável nos dois), que é a informação que o laticínio usa para decidir entre leite para UHT e leite para queijo. Estado atual: especificação concluída; construção e calibração são a primeira entrega da fase 1 do piloto.