paint-brush
Usei IA generativa para detectar raças de gatos: foi assim que aconteceupor@raymondcamden
658 leituras
658 leituras

Usei IA generativa para detectar raças de gatos: foi assim que aconteceu

por Raymond Camden9m2023/12/19
Read on Terminal Reader

Muito longo; Para ler

Para o front end, decidi utilizar um recurso nativo da plataforma web para acessar a câmera do usuário através de um simples campo de formulário HTML. Ao usar capture="camera" em uma tag de entrada, você obtém acesso direto à câmera do dispositivo. Existem maneiras mais avançadas de fazer isso, mas de forma rápida e simples, funciona bem. Melhor ainda, em um desktop, ele simplesmente atua como um seletor de arquivos.
featured image - Usei IA generativa para detectar raças de gatos: foi assim que aconteceu
Raymond Camden HackerNoon profile picture

Sejamos honestos: que outra utilidade existe para a IA generativa além de trabalhar com gatos? Se você leu minha postagem anterior sobre o lançamento do Gemini AI do Google, deve ter visto minhas instruções de teste pedindo para identificar o tipo de gato mostrado em uma imagem.


Decidi transformar isso em um aplicativo web adequado como um exemplo real da API em ação. Aqui está o que eu descobri.

O front-end

Para o front end, decidi utilizar um recurso nativo da plataforma web para acessar a câmera do usuário através de um simples campo de formulário HTML. Ao usar capture="camera" em uma tag input , você obtém acesso direto à câmera do dispositivo.


Existem maneiras mais avançadas de fazer isso, mas de forma rápida e simples, funciona bem. Melhor ainda, em um desktop, ele simplesmente atua como um seletor de arquivos.


Meu pensamento era fornecer uma maneira de obter uma imagem (por meio da câmera ou da seleção de arquivo), exibir a imagem e enviá-la para o back-end. Embora JS incrivelmente simples e básico fosse bom, fui em frente e usei Alpine.js para interatividade.


Primeiro, o HTML precisa fornecer a UI para a imagem, um local para exibir a imagem e outro local para exibir o resultado.


 <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="app.css"> <title></title> </head> <body> <h2>🐈 Detector</h2> <div x-data="catDetector"> <input type="file" capture="camera" accept="image/*" @change="gotPic" :disabled="working"> <template x-if="imageSrc"> <p> <img :src="imageSrc"> </p> </template> <div x-html="status"></div> </div> <script defer src="https://unpkg.com/[email protected]/dist/cdn.min.js"></script> <script src="app.js"></script> </body> </html>


Agora, vamos voltar para JavaScript. Começarei compartilhando a versão inicial, pois é mais simples do que explicar como ela falhou. Tudo o que precisei fazer, inicialmente, foi perceber quando um arquivo foi selecionado, ou uma foto tirada, e renderizá-la para o DOM.


(Usei um pouco de CSS não compartilhado aqui para manter o tamanho visível sob controle. Mais sobre isso em breve.) Também precisei enviar uma versão base64 do arquivo para o código do lado do servidor. Aqui está a versão inicial:


 //const IMG_FUNC = 'http://localhost:8787/'; const IMG_FUNC = 'https://catdetector.raymondcamden.workers.dev'; document.addEventListener('alpine:init', () => { Alpine.data('catDetector', () => ({ imageSrc:null, working:false, status:'', async init() { console.log('init'); }, async gotPic(e) { let file = e.target.files[0]; if(!file) return; let reader = new FileReader(); reader.readAsDataURL(file); reader.onload = async e => { this.imageSrc = e.target.result; this.working = true; this.status = '<i>Sending image data to Google Gemini...</i>'; let body = { imgdata:this.imageSrc } let resp = await fetch(IMG_FUNC, { method:'POST', body: JSON.stringify(body) }); let result = await resp.json(); this.working = false; this.status = result.text; } } })) });


O método gotPic é acionado sempre que o campo input dispara um evento onchange . Pego o arquivo/imagem usado, leio-o como uma URL de dados (base64), atribuo-o à imagem no DOM e envio-o ao servidor. Bonito e simples, certo?


Bem, tudo funcionou bem no desktop, mas quando mudei para minha câmera, a Samsung S22 Ultra Magnus Extreme 200 Camera Lens Edition (nome fictício), me deparei com problemas em que a API do Google reclamava que eu estava enviando muitos dados. Lembrei então que minha câmera tira fotos muito detalhadas e precisei redimensionar a imagem antes de enviá-la.


Eu já estava redimensionando em CSS, mas obviamente isso não é o mesmo que redimensionar de verdade . Encontrei este excelente artigo no site do ImageKit: Como redimensionar imagens em Javascript? Neste artigo, eles descrevem o uso de um elemento HTML canvas para fazer o redimensionamento.


Eu não usava o Canvas provavelmente há quase uma década, mas consegui redirecionar o código deles para o meu front-end muito bem:


 //const IMG_FUNC = 'http://localhost:8787/'; const IMG_FUNC = 'https://catdetector.raymondcamden.workers.dev'; // Resize logic: https://imagekit.io/blog/how-to-resize-image-in-javascript/ const MAX_WIDTH = 400; const MAX_HEIGHT = 400; document.addEventListener('alpine:init', () => { Alpine.data('catDetector', () => ({ imageSrc:null, working:false, status:'', async init() { console.log('init'); }, async gotPic(e) { let file = e.target.files[0]; if(!file) return; let reader = new FileReader(); reader.readAsDataURL(file); reader.onload = async e => { let img = document.createElement('img'); img.onload = async e => { let width = img.width; let height = img.height; if(width > height) { if(width > MAX_WIDTH) { height = height * (MAX_WIDTH / width); width = MAX_WIDTH; } } else { if (height > MAX_HEIGHT) { width = width * (MAX_HEIGHT / height); height = MAX_HEIGHT; } } let canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; let ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, width, height); this.imageSrc = canvas.toDataURL(file.type); this.working = true; this.status = '<i>Sending image data to Google Gemini...</i>'; let body = { imgdata:this.imageSrc } let resp = await fetch(IMG_FUNC, { method:'POST', body: JSON.stringify(body) }); let result = await resp.json(); this.working = false; this.status = result.text; }; img.src = e.target.result; } } })) });


Você notará que o código usa uma dimensão máxima para largura e altura e lida corretamente com o redimensionamento, mantendo a mesma proporção. Mais uma vez, não posso levar o crédito por nada disso, obrigado a Manu Chaudhary por sua postagem no blog.


O resultado líquido dessa mudança é que agora estou enviando uma imagem muito menor para o serviço de back-end e finalmente chegou a hora de dar uma olhada nisso.

O back-end

Para meu back-end, decidi usar Cloudflare Workers novamente. Fiquei um pouco hesitante, pois tive alguns problemas com pacotes NPM e minhas demos antes, mas desta vez não houve nenhum problema. Se você se lembra da minha última postagem , o AI Studio do Google permite gerar facilmente códigos de amostra a partir de seus prompts, então tudo que precisei fazer foi incorporar isso ao Cloudflare Worker.


Aqui está o código completo:


 const { GoogleGenerativeAI, HarmCategory, HarmBlockThreshold, } = require("@google/generative-ai"); const MODEL_NAME = "gemini-pro-vision"; export default { async fetch(request, env, ctx) { const API_KEY = env.GEMINI_KEY; console.log('begin serverless logic'); const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS", "Access-Control-Max-Age": "86400", }; let { imgdata } = await request.json(); imgdata = imgdata.replace(/data:.*?;base64,/, ''); const genAI = new GoogleGenerativeAI(API_KEY); const model = genAI.getGenerativeModel({ model: MODEL_NAME }); const generationConfig = { temperature: 0.4, topK: 32, topP: 1, maxOutputTokens: 4096, }; const safetySettings = [ { category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, }, { category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, }, { category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, }, { category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, }, ]; const parts = [ {text: "Look at this picture and if you see a cat, return the breed of the cat."}, { inlineData: { mimeType: "image/jpeg", data: imgdata } } ]; console.log('calling google'); const result = await model.generateContent({ contents: [{ role: "user", parts }], generationConfig, safetySettings, }); const response = result.response; let finalResult = { text: response.text() }; return new Response(JSON.stringify(finalResult), { headers: {...corsHeaders}}); }, };


A maior parte do código é padrão da exportação do AI Studio, exceto que agora, obtenho meus dados de imagem das informações postadas para o trabalhador. Quero destacar especialmente o prompt:


 Look at this picture and if you see a cat, return the breed of the cat.


Inicialmente, tentei muito obter um resultado em JSON e fazer com que ele retornasse uma string em branco se a imagem não fosse um gato. Mas então percebi algo legal - Gemini fez um ótimo trabalho ao lidar com fotos que não eram de gatos. Tipo... chocantemente bom.


Na verdade, eu realmente gostei que o aplicativo não dissesse apenas "Não é um gato", mas explicasse o que era. Obviamente, há espaço para ambos os estilos em um aplicativo como este, mas mantive as respostas detalhadas e úteis do Google.


Agora, a parte divertida... os resultados.

Os resultados

Vamos começar com algumas fotos de gatos.


O primeiro é Pig, meu gato favorito que não é gordo e nem se parece com Jabba the Hut:

Uma foto de um gato malhado reconhecido corretamente


A seguir, uma foto de Luna. Nesse caso, a raça está incorreta – mas pelo menos próxima.


Uma foto de um considerado Maine Coon


Agora, vamos lançar uma bola curva ao Google:


A imagem de um regador identificado corretamente.


Isso realmente me surpreendeu. A descrição é 100% precisa e, honestamente, se eu tivesse visto essa foto e não soubesse, a teria reconhecido como a escultura de um gato, não como um regador. Quer dizer, acho que é meio óbvio, mas sinceramente não acho que eu mesmo teria notado isso.


Agora, vamos enlouquecer:


Uma imagem de uma árvore de Natal identificada corretamente.

Sim, isso mesmo, Google. Que tal imagens de gatos geradas por IA?


Uma foto de um gato como DJ.


Acho que lidou com isso muito bem.


Próximo...

Uma imagem de uma figura de ação do Pé Grande


Sim, é o Pé Grande, certo. Imagine só, todos aqueles “pesquisadores” do Pé Grande poderiam se aposentar e simplesmente conectar suas câmeras de trilha à IA!


Uma imagem de uma figura de ação do Esqueleto

Devo dizer: estou impressionado que o Google não apenas tenha reconhecido a franquia, mas também o personagem real, mas, para ser justo, Skeletor tem uma aparência bastante distinta.


E finalmente, já que comparei (injustamente, é claro) meu gato ao Jabba, vamos ver como isso é tratado:


Jabba, o Hutt


Ah, eu sei que disse que tinha terminado, mas é claro que tive que testar um cachorro:


Cachorro no sofá


Muito bom trabalho, Gêmeos. Infelizmente, não hospedarei esta demonstração ao vivo (o URL 'ao vivo' da Cloudflare anteriormente no código não funcionará), mas posso compartilhar o código.