paint-brush
고양이 품종을 감지하기 위해 생성 AI를 사용했습니다. 진행 과정은 다음과 같습니다.~에 의해@raymondcamden
658 판독값
658 판독값

고양이 품종을 감지하기 위해 생성 AI를 사용했습니다. 진행 과정은 다음과 같습니다.

~에 의해 Raymond Camden9m2023/12/19
Read on Terminal Reader

너무 오래; 읽다

프런트 엔드의 경우 간단한 HTML 양식 필드를 통해 사용자의 카메라에 액세스하기 위해 기본 웹 플랫폼 기능을 사용하기로 결정했습니다. 입력 태그에 Capture="camera"를 사용하면 장치 카메라에 직접 액세스할 수 있습니다. 이를 수행하는 더 진보된 방법이 있지만 빠르고 간단하게 수행하면 잘 작동합니다. 더 좋은 점은 데스크톱에서는 단순히 파일 선택기 역할을 한다는 것입니다.
featured image - 고양이 품종을 감지하기 위해 생성 AI를 사용했습니다. 진행 과정은 다음과 같습니다.
Raymond Camden HackerNoon profile picture

솔직히 말해서, 고양이와 함께 작업하는 것 외에 생성 AI에 다른 용도가 있습니까? Google의 Gemini AI 출시에 대한 이전 게시물을 읽으셨다면 사진에 표시된 고양이의 종류를 식별하라는 테스트 메시지를 보셨을 것입니다.


나는 API의 실제 사례로서 이것을 적절한 웹 애플리케이션으로 바꾸기로 결정했습니다. 내가 생각 해낸 것은 다음과 같습니다.

프런트엔드

프런트 엔드의 경우 간단한 HTML 양식 필드를 통해 사용자의 카메라에 액세스하기 위해 기본 웹 플랫폼 기능을 활용하기로 결정했습니다. input 태그에 capture="camera" 사용하면 장치 카메라에 직접 액세스할 수 있습니다.


이를 수행하는 더 진보된 방법이 있지만 빠르고 간단하게 수행하면 잘 작동합니다. 더 좋은 점은 데스크톱에서는 단순히 파일 선택기 역할을 한다는 것입니다.


내 생각은 - 이미지를 가져오고(카메라나 파일 선택을 통해) 이미지를 표시하고 백엔드로 보내는 방법을 제공하는 것이었습니다. 믿을 수 없을 정도로 단순하고 바닐라 JS라면 괜찮았겠지만, 저는 계속해서 상호작용을 위해 Alpine.js를 사용했습니다.


먼저 HTML은 이미지에 대한 UI, 이미지를 표시할 위치, 결과를 표시할 다른 위치를 제공해야 합니다.


 <!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>


이제 JavaScript를 살펴보겠습니다. 어떻게 실패했는지 설명하는 것보다 간단하기 때문에 초기 버전을 공유하는 것부터 시작하겠습니다. 처음에 내가 해야 할 일은 파일이 선택되거나 사진이 찍힐 때 이를 확인하고 이를 DOM으로 렌더링하는 것뿐이었습니다.


(표시되는 크기를 확인하기 위해 여기에 공유되지 않은 약간의 CSS를 사용했습니다. 이에 대해서는 나중에 자세히 설명합니다.) 또한 파일의 base64 버전을 서버 측 코드로 보내야 했습니다. 초기 버전은 다음과 같습니다.


 //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; } } })) });


gotPic 메소드는 input 필드가 onchange 이벤트를 실행할 때마다 실행됩니다. 사용된 파일/이미지를 가져와 데이터 URL(base64)로 읽은 다음 이를 DOM의 이미지에 할당하고 서버로 보냅니다. 멋지고 간단하죠?


글쎄, 데스크톱에서는 모든 것이 잘 작동했지만 카메라인 Samsung S22 Ultra Magnus Extreme 200 Camera Lens Edition(실명은 아님)으로 전환했을 때 Google API에서 내가 너무 많은 데이터를 전송하고 있다고 불평하는 문제가 발생했습니다. 그러다가 내 카메라가 매우 상세한 사진을 찍기 때문에 이미지를 보내기 전에 크기를 조정해야 한다는 사실을 기억했습니다.


이미 CSS에서 크기를 조정하고 있었지만 실제로 는 크기를 조정하는 것과는 다릅니다. ImageKit 사이트에서 How to resize Images in Javascript?라는 훌륭한 기사를 찾았습니다. 이 기사에서는 크기 조정을 수행하기 위해 HTML canvas 요소를 사용하는 방법을 설명합니다.


나는 거의 10년 동안 Canvas를 사용하지 않았지만 그들의 코드를 내 프런트 엔드에 충분히 잘 재활용할 수 있었습니다.


 //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; } } })) });


코드가 너비와 높이 모두에 최대 크기를 사용하고 동일한 가로 세로 비율을 유지하면서 크기 조정을 올바르게 처리한다는 것을 알 수 있습니다. 다시 한 번 말씀드리지만, 블로그 게시물을 올려주신 Manu Chaudhary 에게 감사드립니다.


이 변경의 최종 결과는 이제 백엔드 서비스에 훨씬 더 작은 이미지를 보내고 있다는 것입니다. 이제 이를 살펴볼 시간입니다.

뒷머리

백엔드에는 Cloudflare Workers를 다시 사용하기로 결정했습니다. 이전에 NPM 패키지와 데모에 몇 가지 문제가 있었기 때문에 조금 주저했지만 이번에는 아무런 문제도 없었습니다. 내 지난 게시물을 기억하신다면 Google의 AI Studio를 사용하면 프롬프트에서 샘플 코드를 쉽게 출력할 수 있으므로 제가 해야 할 일은 이를 Cloudflare Worker에 통합하는 것뿐입니다.


전체 코드는 다음과 같습니다.


 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}}); }, };


대부분의 코드는 AI Studio 내보내기의 상용구입니다. 단, 지금은 작업자에게 게시된 정보에서 이미지 데이터를 가져옵니다. 특히 다음과 같은 메시지를 강조하고 싶습니다.


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


처음에는 JSON으로 결과를 얻고 그림이 고양이가 아닌 경우 빈 문자열을 반환하도록 열심히 노력했습니다. 그런데 뭔가 멋진 사실을 발견했습니다. 쌍둥이자리는 고양이가 아닌 사진을 처리하는 데 정말 능숙했습니다. 마치... 놀라울 정도로 좋아요.


저는 실제로 앱이 "고양이가 아닙니다"라고만 말하는 것이 아니라 그것이 무엇인지 설명한다는 점에 정말 감사했습니다. 분명히 이와 같은 애플리케이션에는 두 가지 스타일을 모두 사용할 수 있는 여지가 있지만 저는 Google의 장황하고 유용한 답변을 그대로 유지했습니다.


이제 재미있는 부분은... 결과입니다.

결과

고양이 사진 몇 장부터 시작해 보겠습니다.


첫 번째는 내가 가장 좋아하는 고양이인 돼지입니다. 뚱뚱하지도 않고 Jabba the Hut처럼 전혀 보이지도 않습니다.

삼색고양이 사진이 올바르게 인식되었습니다.


다음은 루나 사진입니다. 이 경우 품종이 올바르지 않지만 적어도 가깝습니다.


메인쿤으로 간주되는 사진


이제 Google에 변화구를 던져 보겠습니다.


물을 주는 사진을 보면 정확하게 식별할 수 있습니다.


이것은 정말 놀랐습니다. 설명은 100% 정확하고, 솔직히 이 사진을 보고 몰랐다면 물뿌리개가 아닌 고양이 조각품으로 인식했을 것입니다. 내 말은, 그건 당연한 일이라고 생각하지만, 솔직히 나 자신도 그것을 알아차리지 못했을 것이라고 생각합니다.


이제 완전히 미쳐 봅시다.


올바르게 식별된 크리스마스 트리 사진.

네, 바로 구글입니다. AI가 생성한 고양이 이미지는 어떻습니까?


DJ로서의 고양이 사진.


나는 그것이 꽤 잘 처리되었다고 생각합니다.


다음...

빅풋 액션 피규어 사진


네, 빅풋 맞습니다. 빅풋 "연구자들"이 모두 은퇴하고 트레일 캠을 AI에 연결할 수 있다고 생각해보세요!


스켈레톤 액션 피규어 사진

나는 말하고 싶습니다 - Google이 프랜차이즈뿐만 아니라 실제 캐릭터도 인식했다는 사실에 깊은 인상을 받았습니다. 공정하게 말하면 Skeletor는 그에게 꽤 뚜렷한 모습을 가지고 있습니다.


그리고 마지막으로 (물론 불공평하게) 내 고양이를 Jabba와 비교했으므로 고양이가 어떻게 처리되는지 살펴보겠습니다.


자바 더 헛


아, 끝났다고 말했지만 물론 개를 테스트해야 했습니다.


소파에 개


잘했어, 쌍둥이자리. 안타깝게도 이 데모를 라이브로 호스팅하지는 않지만(코드 앞부분의 Cloudflare '라이브' URL은 작동하지 않음) 코드를 공유할 수 있습니다.