# 🌴 Palm Oil Ripeness Detection Service

A **NestJS** backend API that uses an **ONNX-based YOLOv26 model** to perform real-time ripeness classification of oil palm fresh fruit bunches (FFB). Detection results are mapped against **MPOB (Malaysian Palm Oil Board) grading standards**, persisted to a local SQLite database, and served via a REST API.

---

## 🚀 Features

- **AI Inference via ONNX Runtime** — Runs a custom-trained YOLOv8 model (`best.onnx`) using `onnxruntime-node` for zero-dependency, high-performance server-side inference.
- **MPOB-Standard Classification** — Detects and classifies palm oil bunches into 6 industry-standard grades:
  | Class | Description |
  |---|---|
  | `Ripe` | Optimal harvest quality |
  | `Underripe` | Harvested too early |
  | `Unripe` | Not yet ready |
  | `Overripe` | Past optimal harvest window |
  | `Abnormal` | ⚠️ Health alert |
  | `Empty_Bunch` | ⚠️ Health alert |
- **Industrial Summary** — Each analysis response includes a per-class count summary for field reporting.
- **Health Alert Flagging** — Detections with `Abnormal` or `Empty_Bunch` classes are automatically flagged with `is_health_alert: true`.
- **History Persistence** — All scans are saved to a local **SQLite** database via TypeORM, with the last 50 records retrievable via the history endpoint.
- **CORS Enabled** — Ready for integration with any frontend (Angular, React, etc.).
- **Image Pass-Through** — The original image is returned as a Base64 data URI in the response for frontend canvas rendering.

---

## 🏗️ Architecture Overview

```
src/
├── main.ts                          # Bootstrap (port 3000, CORS enabled)
├── app.module.ts                    # Root module
└── palm-oil/
    ├── palm-oil.controller.ts       # REST endpoints
    ├── palm-oil.service.ts          # Orchestration logic & SQLite persistence
    ├── palm-oil.module.ts           # Feature module
    ├── providers/
    │   └── scanner.provider.ts      # ONNX inference pipeline (preprocess → infer → postprocess)
    ├── entities/
    │   └── history.entity.ts        # TypeORM entity for scan history
    ├── interfaces/
    │   └── palm-analysis.interface.ts  # TypeScript types for API response
    └── constants/
        └── mpob-standards.ts        # MPOB class map, grade colors, and health alert list
```

### Inference Pipeline (`ScannerProvider`)

1. **Preprocess** — Resize image to `640×640` using `sharp`, strip alpha, extract raw pixels, and transpose from `HWC → CHW` layout, normalizing to `[0.0, 1.0]`.
2. **Inference** — Feed the `[1, 3, 640, 640]` float tensor into the ONNX session. Input key: `images`.
3. **Postprocess** — Parse the `[1, N, 6]` output tensor (`x1, y1, x2, y2, confidence, class_index`). Filter by a default confidence threshold of `0.25`. Scale normalized bounding box coordinates to the original image pixel dimensions.

---

## 📋 Prerequisites

- **Node.js** `>=18`
- **npm** `>=9`
- The ONNX model file `best.onnx` must be placed in the **project root directory**.

---

## ⚙️ Project Setup

```bash
npm install
```

---

## ▶️ Running the Service

```bash
# Development (single run)
npm run start

# Development (watch mode — auto-restarts on file changes)
npm run start:dev

# Production
npm run start:prod
```

The server starts on **`http://localhost:3000`** by default. Set the `PORT` environment variable to override.

---

## 📡 API Endpoints

### `POST /palm-oil/analyze`

Analyzes an uploaded image for palm oil ripeness.

**Request:** `multipart/form-data`

| Field | Type | Description |
|---|---|---|
| `image` | `File` | The palm oil bunch image to analyze |

**Response:** `application/json`

```json
{
  "status": "success",
  "current_threshold": 0.25,
  "total_count": 4,
  "industrial_summary": {
    "Empty_Bunch": 0,
    "Underripe": 1,
    "Abnormal": 0,
    "Ripe": 2,
    "Unripe": 0,
    "Overripe": 1
  },
  "detections": [
    {
      "bunch_id": 1,
      "class": "Ripe",
      "confidence": 0.9312,
      "is_health_alert": false,
      "box": [120.5, 88.3, 450.2, 390.1]
    }
  ],
  "image_data": "data:image/jpeg;base64,...",
  "inference_ms": 42.15,
  "processing_ms": 115.30,
  "archive_id": "palm_1712345678901_456"
}
```

---

### `GET /palm-oil/history`

Returns the last 50 scan records from the SQLite database, ordered by most recent first.

**Response:** `application/json` — Array of `History` entities.

---

## 🧪 Running Tests

```bash
# Unit tests
npm run test

# Unit tests (watch mode)
npm run test:watch

# End-to-end tests
npm run test:e2e

# Test coverage report
npm run test:cov
```

---

## 🗄️ Database

Scan history is persisted to a local **SQLite** file (`palm_history.db`) in the project root, managed by **TypeORM** with `synchronize: true`. No external database setup is required.

Each history record stores:
- `archive_id` — Unique scan identifier
- `filename` — Original uploaded file name
- `total_count` — Number of detections
- `industrial_summary` — Per-class count (JSON)
- `detections` — Full detection array with bounding boxes (JSON)
- `inference_ms` / `processing_ms` — Performance timings
- `created_at` — Auto-generated timestamp

---

## 🗂️ Key Files

| File | Purpose |
|---|---|
| `best.onnx` | YOLOv8 ONNX inference model (must be in project root) |
| `palm_history.db` | SQLite scan history database (auto-created) |
| `src/palm-oil/constants/mpob-standards.ts` | MPOB class definitions, colors, and health alert flags |
| `src/palm-oil/providers/scanner.provider.ts` | Core AI inference pipeline |
| `src/palm-oil/palm-oil.service.ts` | Business logic, summary generation, persistence |
| `src/palm-oil/palm-oil.controller.ts` | REST API layer |

---

## 📦 Core Dependencies

| Package | Purpose |
|---|---|
| `@nestjs/core` | NestJS framework |
| `onnxruntime-node` | ONNX model inference |
| `sharp` | High-performance image preprocessing |
| `typeorm` + `sqlite3` | Database ORM and SQLite driver |
| `class-validator` | DTO validation |

---

## 📄 License

UNLICENSED — Private project.
