205 lines
5.0 KiB
Markdown
205 lines
5.0 KiB
Markdown
# 👁️ Camada de Visão por IA
|
|
|
|
## Objetivo
|
|
|
|
Usar IA de visão computacional para **identificar automaticamente** o que está na foto tirada pelo utilizador, sugerindo:
|
|
- Nome do item
|
|
- Categoria
|
|
- Tags relevantes
|
|
|
|
Isto reduz o esforço do utilizador ao mínimo.
|
|
|
|
---
|
|
|
|
## Serviço escolhido: Google Vision API
|
|
|
|
### Porquê Google Vision?
|
|
- Fácil integração com Firebase (mesmo ecosistema Google)
|
|
- Excelente deteção de objetos do quotidiano
|
|
- Labels em múltiplos idiomas
|
|
- Tier gratuito generoso (1.000 unidades/mês)
|
|
- Documentação extensa
|
|
|
|
### Funcionalidades usadas no MVP
|
|
|
|
| Feature | Uso |
|
|
|---------|-----|
|
|
| `LABEL_DETECTION` | Identificar o objeto (ex: "T-shirt", "Laptop") |
|
|
| `OBJECT_LOCALIZATION` | Confirmar que existe um objeto na foto |
|
|
|
|
> No MVP **não** usamos OCR, Safe Search ou outras features avançadas.
|
|
|
|
---
|
|
|
|
## Integração técnica
|
|
|
|
### Chamada à API (exemplo React Native)
|
|
|
|
```javascript
|
|
// services/visionApi.js
|
|
|
|
const VISION_API_KEY = process.env.GOOGLE_VISION_API_KEY;
|
|
const VISION_API_URL = `https://vision.googleapis.com/v1/images:annotate?key=${VISION_API_KEY}`;
|
|
|
|
export async function analyzeImage(base64Image) {
|
|
const requestBody = {
|
|
requests: [
|
|
{
|
|
image: { content: base64Image },
|
|
features: [
|
|
{ type: 'LABEL_DETECTION', maxResults: 10 },
|
|
{ type: 'OBJECT_LOCALIZATION', maxResults: 5 }
|
|
]
|
|
}
|
|
]
|
|
};
|
|
|
|
const response = await fetch(VISION_API_URL, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(requestBody)
|
|
});
|
|
|
|
const data = await response.json();
|
|
return parseVisionResponse(data);
|
|
}
|
|
```
|
|
|
|
### Parsing da resposta
|
|
|
|
```javascript
|
|
// services/visionApi.js (continuação)
|
|
|
|
export function parseVisionResponse(data) {
|
|
const labels = data.responses[0]?.labelAnnotations || [];
|
|
const objects = data.responses[0]?.localizedObjectAnnotations || [];
|
|
|
|
// Extrair labels com score > 0.7
|
|
const highConfidenceLabels = labels
|
|
.filter(l => l.score > 0.7)
|
|
.map(l => l.description.toLowerCase());
|
|
|
|
// Mapear para categoria da app
|
|
const category = mapLabelsToCategory(highConfidenceLabels);
|
|
|
|
// Sugerir nome baseado no objeto mais confiante
|
|
const suggestedName = objects[0]?.name || labels[0]?.description || 'Item';
|
|
|
|
return {
|
|
suggestedName,
|
|
category,
|
|
tags: highConfidenceLabels.slice(0, 5),
|
|
rawLabels: highConfidenceLabels
|
|
};
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Mapeamento de Labels → Categorias
|
|
|
|
```javascript
|
|
// constants/categoryMapping.js
|
|
|
|
export const LABEL_TO_CATEGORY = {
|
|
// Roupa
|
|
't-shirt': 'clothing',
|
|
'shirt': 'clothing',
|
|
'dress': 'clothing',
|
|
'jacket': 'clothing',
|
|
'coat': 'clothing',
|
|
'jeans': 'clothing',
|
|
'trousers': 'clothing',
|
|
'sweater': 'clothing',
|
|
'clothing': 'clothing',
|
|
'fashion': 'clothing',
|
|
|
|
// Eletrónica
|
|
'laptop': 'electronics',
|
|
'computer': 'electronics',
|
|
'smartphone': 'electronics',
|
|
'tablet': 'electronics',
|
|
'headphones': 'electronics',
|
|
'camera': 'electronics',
|
|
'charger': 'electronics',
|
|
'cable': 'electronics',
|
|
'electronics': 'electronics',
|
|
|
|
// Calçado
|
|
'shoe': 'footwear',
|
|
'boot': 'footwear',
|
|
'sneaker': 'footwear',
|
|
'sandal': 'footwear',
|
|
'footwear': 'footwear',
|
|
|
|
// Acessórios
|
|
'bag': 'accessories',
|
|
'backpack': 'accessories',
|
|
'watch': 'accessories',
|
|
'sunglasses': 'accessories',
|
|
'hat': 'accessories',
|
|
'belt': 'accessories',
|
|
|
|
// Documentos
|
|
'passport': 'documents',
|
|
'document': 'documents',
|
|
'book': 'documents',
|
|
'notebook': 'documents',
|
|
};
|
|
|
|
export function mapLabelsToCategory(labels) {
|
|
for (const label of labels) {
|
|
if (LABEL_TO_CATEGORY[label]) {
|
|
return LABEL_TO_CATEGORY[label];
|
|
}
|
|
}
|
|
return 'other'; // fallback
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Fluxo de UX ao adicionar item
|
|
|
|
```
|
|
1. Utilizador tira foto
|
|
2. App mostra loading ("A analisar item...")
|
|
3. Google Vision responde com labels
|
|
4. App mostra:
|
|
- Nome sugerido (editável)
|
|
- Categoria sugerida (editável)
|
|
- Tags sugeridas (checkboxes, editáveis)
|
|
5. Utilizador confirma ou ajusta
|
|
6. Item guardado
|
|
```
|
|
|
|
> O utilizador tem **sempre** controlo final. A IA apenas sugere.
|
|
|
|
---
|
|
|
|
## Tratamento de erros
|
|
|
|
| Erro | Comportamento |
|
|
|------|---------------|
|
|
| API indisponível | Mostrar formulário manual, sem sugestões |
|
|
| Foto sem objeto reconhecível | Mostrar sugestão "Outro" e pedir nome manual |
|
|
| Score muito baixo (< 0.5) | Ignorar label, não sugerir |
|
|
| Timeout | Retry 1x, depois formulário manual |
|
|
|
|
---
|
|
|
|
## Evolução futura desta camada
|
|
|
|
- Fase 2: Treinar modelo custom com os dados dos utilizadores
|
|
- Fase 3: Identificar marcas e modelos específicos
|
|
- Fase 3: Reconhecimento de cor dominante para filtros de outfit
|
|
|
|
---
|
|
|
|
## Notas para o agente IA
|
|
|
|
- A chave da API (`GOOGLE_VISION_API_KEY`) nunca deve aparecer em código client-side em produção — usar Firebase Cloud Functions como proxy
|
|
- No MVP, a chamada pode ser feita diretamente do cliente para simplicidade
|
|
- Nunca guardar `rawLabels` como `name` sem confirmação do utilizador
|
|
- O mapeamento de labels é iterativo — quando encontrar labels não mapeadas, adicionar ao ficheiro `categoryMapping.js`
|