# 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.