# 03. Image Upload Component and Preprocessing Pipeline
meta:
id: hyper-specific-plant-disease-id-03
feature: hyper-specific-plant-disease-id
priority: P1
depends_on: [hyper-specific-plant-disease-id-01]
tags: [frontend, image-processing, component]
objective:
- Build a drag-and-drop / file-picker image upload component with client-side preview, validation (size, type, dimensions), and a preprocessing pipeline that resizes, normalizes, and formats images for the ML inference engine.
deliverables:
- `components/ImageUpload.tsx` — drop zone + file picker with preview thumbnail, progress indicator, and error messages
- `lib/image-processing.ts` — client-side image preprocessing: resize to 224×224, convert to RGB tensor (Float32Array), normalize to model-expected range, return as tensor or base64
- `app/api/upload/route.ts` — server endpoint that accepts multipart image, runs preprocessing server-side, and returns `{ imageId, tensorShape, previewUrl }`
- `lib/api/upload.ts` — client helper to POST image and get back image metadata
steps:
1. Build `components/ImageUpload.tsx`:
- Drag-and-drop zone with dashed border and "drop here" / "click to browse" states.
- File picker accept `image/*` (PNG, JPG, WebP) with max 10 MB client-side check.
- Preview thumbnail rendered as `
` from `URL.createObjectURL()`.
- Clear / retry button.
- Loading spinner during upload.
- Inline error display (wrong type, too large, upload failed).
2. Implement `lib/image-processing.ts`:
- `resizeImage(file: File, size: number): Promise` — draws to offscreen canvas, bilinear resize to 224×224.
- `imageToTensor(imageData: ImageData): Float32Array` — converts RGBA to RGB, normalizes to [0,1] or model-specific range, returns flat Float32Array.
- `tensorToBase64(tensor: Float32Array): string` — for transmitting to server.
3. Build `app/api/upload/route.ts`:
- Accept `multipart/form-data` with field `image`.
- Validate MIME type, file size (10 MB limit), and minimum dimensions (150×150).
- Save uploaded file to `public/uploads/{uuid}.{ext}`.
- Run server-side preprocessing pipeline (same resize + normalize logic).
- Return `{ imageId: uuid, tensorShape: [1, 3, 224, 224], previewUrl: "/uploads/{uuid}.{ext}" }`.
- Cleanup: ephemeral uploads (keep last 100, purge older via cron or on-demand).
4. Wire `ImageUpload` component to call `lib/api/upload.ts` on file selection.
5. Add loading skeleton while upload is in progress.
6. Test with sample images of various sizes, types, and orientations.
tests:
- **Unit:** `resizeImage()` produces 224×224 output for any input aspect ratio.
- **Unit:** `imageToTensor()` output length equals `3 * 224 * 224`.
- **Unit:** Normalization produces values in [0, 1] range.
- **Integration:** Upload a valid JPG → `POST /api/upload` returns 200 with expected shape.
- **Integration:** Upload a 12 MB file → returns 413 or validation error.
- **Integration:** Upload a `.txt` file → returns 400 with MIME error.
acceptance_criteria:
- User can drag-and-drop or click to select an image.
- Preview thumbnail shows before upload.
- Upload progress indicator shows.
- Server returns imageId and previewUrl.
- Non-image files are rejected with clear message.
- Images >10 MB are rejected.
- Output tensor has shape `[1, 3, 224, 224]`.
validation:
```bash
# Upload a sample plant photo
curl -X POST -F "image=@test-assets/tomato-leaf.jpg" http://localhost:3000/api/upload
# → {"imageId":"uuid","tensorShape":[1,3,224,224],"previewUrl":"/uploads/..."}
```
notes:
- The tensor shape `[1, 3, 224, 224]` matches standard MobileNet / ResNet input expectations.
- If the user picks a custom model architecture later, the shape constants in `lib/image-processing.ts` should be configurable via env var.
- Uploaded files are ephemeral — stored in `public/uploads/` which is gitignored.