diff --git a/backend/export_to_onnx.py b/backend/export_to_onnx.py new file mode 100644 index 0000000..b38942b --- /dev/null +++ b/backend/export_to_onnx.py @@ -0,0 +1,89 @@ +import os +import json +import torch +import torch.nn as nn +from torchvision import models + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +MODEL_DIR = os.path.join(BASE_DIR, "model") +CLASS_NAMES_PATH = os.path.join(MODEL_DIR, "class_names.json") +MODEL_PATH = os.path.join(MODEL_DIR, "plant_disease_resnet18.pth") + +# Output paths +FRONTEND_PUBLIC_DIR = os.path.join(os.path.dirname(BASE_DIR), "frontend", "public") +ONNX_MODEL_DIR = os.path.join(FRONTEND_PUBLIC_DIR, "model") +os.makedirs(ONNX_MODEL_DIR, exist_ok=True) +ONNX_PATH = os.path.join(ONNX_MODEL_DIR, "plant_disease_resnet18.onnx") + +def export_model(): + # Load class names + with open(CLASS_NAMES_PATH, "r") as f: + class_names = json.load(f) + num_classes = len(class_names) + + print(f"[INFO] Loaded {num_classes} class names from {CLASS_NAMES_PATH}") + + # Instantiate the ResNet18 model + model = models.resnet18(weights=None) + model.fc = nn.Linear(model.fc.in_features, num_classes) + + # Load local fine-tuned weights if available + if os.path.exists(MODEL_PATH) and os.path.getsize(MODEL_PATH) > 0: + print(f"[INFO] Loading fine-tuned weights from {MODEL_PATH}...") + try: + model.load_state_dict(torch.load(MODEL_PATH, map_location="cpu")) + print("[INFO] Weights loaded successfully.") + except Exception as e: + print(f"[ERROR] Failed to load model weights: {e}") + raise + else: + raise FileNotFoundError( + f"Fine-tuned model weights not found at {MODEL_PATH}. " + "Please ensure you have placed the trained ResNet18 model (.pth file) in the backend/model/ directory before exporting." + ) + + model.eval() + + # Preprocessing dummy input matching ImageNet requirements: 1 image, 3 channels, 224x224 shape + dummy_input = torch.randn(1, 3, 224, 224, requires_grad=False) + + print(f"[INFO] Exporting PyTorch model to ONNX format at {ONNX_PATH}...") + torch.onnx.export( + model, + dummy_input, + ONNX_PATH, + export_params=True, + opset_version=12, + do_constant_folding=True, + input_names=["input"], + output_names=["output"], + dynamic_axes={"input": {0: "batch_size"}, "output": {0: "batch_size"}}, + ) + print(f"[SUCCESS] ONNX model exported to {ONNX_PATH}") + + # Apply 8-bit dynamic quantization if onnxruntime is available + try: + import onnxruntime + from onnxruntime.quantization import quantize_dynamic, QuantType + + QUANT_ONNX_PATH = os.path.join(ONNX_MODEL_DIR, "plant_disease_resnet18_quant.onnx") + print(f"[INFO] onnxruntime is installed. Performing 8-bit dynamic quantization to {QUANT_ONNX_PATH}...") + + quantize_dynamic( + model_input=ONNX_PATH, + model_output=QUANT_ONNX_PATH, + weight_type=QuantType.QUInt8 + ) + print("[SUCCESS] Quantization complete.") + + # Replace the larger float32 file with the quantized version to save space (46MB -> 11MB) + if os.path.exists(QUANT_ONNX_PATH) and os.path.getsize(QUANT_ONNX_PATH) > 0: + os.replace(QUANT_ONNX_PATH, ONNX_PATH) + print(f"[INFO] Replaced {ONNX_PATH} with the quantized model (~11.6MB).") + except ImportError: + print("[WARN] onnxruntime is not installed. Skipping dynamic quantization. The output model will be standard float32 (~46.8MB).") + except Exception as e: + print(f"[ERROR] Quantization failed: {e}") + +if __name__ == "__main__": + export_model() diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs index 1cc8a95..b4d49dd 100644 --- a/frontend/next.config.mjs +++ b/frontend/next.config.mjs @@ -20,6 +20,21 @@ const withPWA = withPWAInit({ // Runtime caching rules - these are added to the auto-generated Workbox SW // They tell the SW how to handle specific request types at runtime runtimeCaching: [ + { + // Cache ONNX and WASM static assets (models and runtimes) for offline execution + urlPattern: ({ url }) => url.pathname.endsWith('.onnx') || url.pathname.endsWith('.wasm'), + handler: 'CacheFirst', + options: { + cacheName: 'agronavis-ml-assets-v1', + expiration: { + maxEntries: 10, + maxAgeSeconds: 30 * 24 * 60 * 60, // Cache for 30 days + }, + cacheableResponse: { + statuses: [0, 200], + }, + }, + }, { // Cache API responses for farms and crop-scans // Use matchCallback: Workbox tests RegExp against full URL, not just pathname @@ -84,10 +99,17 @@ const nextConfig = { ], formats: ['image/webp', 'image/avif'], }, - transpilePackages: ['react-leaflet', 'leaflet'], + transpilePackages: ['react-leaflet', 'leaflet', 'onnxruntime-web'], experimental: { optimizePackageImports: ['@supabase/supabase-js'], }, + webpack: (config) => { + config.resolve.alias = { + ...config.resolve.alias, + 'onnxruntime-web': path.resolve(process.cwd(), 'node_modules/onnxruntime-web/dist/ort.all.min.js'), + }; + return config; + }, env: { NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 481a75c..da37faf 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -21,6 +21,7 @@ "i18next-browser-languagedetector": "^8.2.0", "leaflet": "^1.9.4", "next": "^14.1.0", + "onnxruntime-web": "^1.26.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hook-form": "^7.48.2", @@ -3440,6 +3441,63 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "license": "BSD-3-Clause" + }, "node_modules/@react-leaflet/core": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz", @@ -9635,6 +9693,12 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/flatbuffers": { + "version": "25.9.23", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz", + "integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==", + "license": "Apache-2.0" + }, "node_modules/flatted": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", @@ -10093,6 +10157,12 @@ "dev": true, "license": "MIT" }, + "node_modules/guid-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz", + "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==", + "license": "ISC" + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -12323,6 +12393,12 @@ "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", "license": "MIT" }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -13719,6 +13795,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/onnxruntime-common": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.26.0.tgz", + "integrity": "sha512-qVyMR4lcWgbkc4getFV+GQijsTnbg/siteoqcDwa3sI/LxbrMSNw4ePyvCq/ymdQaRomCA7YuWmhzsswxvymdw==", + "license": "MIT" + }, + "node_modules/onnxruntime-web": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.26.0.tgz", + "integrity": "sha512-LbRr/8zZt2xilI2smrVQGGKINo0U46i8qJp+UXyMBGfqN7KjnH1BiwCwLwyNIVV4i9CKFv7Sf4PwLKWnT8/bEA==", + "license": "MIT", + "dependencies": { + "flatbuffers": "^25.1.24", + "guid-typescript": "^1.0.9", + "long": "^5.2.3", + "onnxruntime-common": "1.26.0", + "platform": "^1.3.6", + "protobufjs": "^7.2.4" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -14028,6 +14124,12 @@ "node": ">=8" } }, + "node_modules/platform": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", + "license": "MIT" + }, "node_modules/point-in-polygon": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/point-in-polygon/-/point-in-polygon-1.1.0.tgz", @@ -14296,6 +14398,29 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/protobufjs": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz", + "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 27e3998..cd90f52 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,7 +7,8 @@ "build": "next build", "start": "next start", "lint": "next lint", - "test": "jest" + "test": "jest", + "postinstall": "node -e \"const fs=require('fs');const path=require('path');const src=path.join(__dirname,'node_modules','onnxruntime-web','dist');const dest=path.join(__dirname,'public','wasm');if(fs.existsSync(src)){fs.mkdirSync(dest,{recursive:true});fs.readdirSync(src).filter(f=>f.endsWith('.wasm')).forEach(f=>fs.copyFileSync(path.join(src,f),path.join(dest,f)));console.log('Copied onnxruntime-web WASM files to public/wasm')} else {console.error('Error: onnxruntime-web node_modules not found. WASM files are required for offline inference.');process.exit(1)}\"" }, "dependencies": { "@ducanh2912/next-pwa": "^10.2.9", @@ -23,6 +24,7 @@ "i18next-browser-languagedetector": "^8.2.0", "leaflet": "^1.9.4", "next": "^14.1.0", + "onnxruntime-web": "^1.26.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hook-form": "^7.48.2", diff --git a/frontend/public/wasm/ort-wasm-simd-threaded.asyncify.wasm b/frontend/public/wasm/ort-wasm-simd-threaded.asyncify.wasm new file mode 100644 index 0000000..c7918f6 Binary files /dev/null and b/frontend/public/wasm/ort-wasm-simd-threaded.asyncify.wasm differ diff --git a/frontend/public/wasm/ort-wasm-simd-threaded.jsep.wasm b/frontend/public/wasm/ort-wasm-simd-threaded.jsep.wasm new file mode 100644 index 0000000..b2d8187 Binary files /dev/null and b/frontend/public/wasm/ort-wasm-simd-threaded.jsep.wasm differ diff --git a/frontend/public/wasm/ort-wasm-simd-threaded.jspi.wasm b/frontend/public/wasm/ort-wasm-simd-threaded.jspi.wasm new file mode 100644 index 0000000..70c6e69 Binary files /dev/null and b/frontend/public/wasm/ort-wasm-simd-threaded.jspi.wasm differ diff --git a/frontend/public/wasm/ort-wasm-simd-threaded.wasm b/frontend/public/wasm/ort-wasm-simd-threaded.wasm new file mode 100644 index 0000000..7ab49a2 Binary files /dev/null and b/frontend/public/wasm/ort-wasm-simd-threaded.wasm differ diff --git a/frontend/src/components/CropScanTab.tsx b/frontend/src/components/CropScanTab.tsx index dc64411..dd27de7 100644 --- a/frontend/src/components/CropScanTab.tsx +++ b/frontend/src/components/CropScanTab.tsx @@ -21,6 +21,8 @@ import { type CropScan, } from '../utils/cropScanApi' import { farmApi } from '../utils/farmApi' +import { runLocalONNXInference } from '../utils/onnxInference' +import { addOfflineCropScan } from '../lib/offlineStorage' // ── Icons (inline SVG) ──────────────────────────────────────────────────────── @@ -88,6 +90,7 @@ const CropScanTab: React.FC = () => { const [result, setResult] = useState(null) const [error, setError] = useState(null) const [saved, setSaved] = useState(false) + const [scanSource, setScanSource] = useState<'on-device' | 'server' | null>(null) // History const [history, setHistory] = useState([]) @@ -168,20 +171,62 @@ const CropScanTab: React.FC = () => { setError(null) setResult(null) setSaved(false) + setScanSource(null) + + let diagnosis: DiagnosisResult + let source: 'on-device' | 'server' = 'server' + try { - const diagnosis = await diagnoseImage( - selectedFile, - selectedFarmId || undefined - ) - setResult(diagnosis) - if (selectedFarmId) setSaved(true) - // Refresh history after scan - await loadHistory(selectedFarmId || undefined) + if (typeof window !== 'undefined' && navigator.onLine) { + // Attempt online server-side scan + diagnosis = await diagnoseImage( + selectedFile, + selectedFarmId || undefined + ) + source = 'server' + } else { + throw new Error('Device is offline. Running local scanner.') + } } catch (err: any) { - setError(err.message || 'Scan failed. Make sure the Python backend is running.') - } finally { - setLoading(false) + console.log('Online scan failed or device is offline. Falling back to local ONNX inference:', err) + try { + const localResult = await runLocalONNXInference(selectedFile) + diagnosis = { + predicted_disease_name: localResult.predicted_disease_name, + confidence_score: localResult.confidence_score, + is_healthy: localResult.is_healthy, + crop_type: localResult.crop_type, + symptoms: localResult.symptoms, + recommended_action: localResult.recommended_action + } + source = 'on-device' + + // Save scan result to IndexedDB local history if a farm is selected + if (selectedFarmId) { + const offlineScan = { + id: Math.random().toString(36).substring(2, 11) + Date.now().toString(), + farm_id: selectedFarmId, + detected_disease: diagnosis.predicted_disease_name, + confidence_score: diagnosis.confidence_score, + recommendation: diagnosis.recommended_action.join(' | '), + scan_date: new Date().toISOString() + } + await addOfflineCropScan(offlineScan) + } + } catch (localErr: any) { + setError(localErr.message || 'Scan failed. Local ONNX model couldn\'t be loaded or executed.') + setLoading(false) + return + } } + + setResult(diagnosis) + setScanSource(source) + if (selectedFarmId) setSaved(true) + + // Refresh history after scan + await loadHistory(selectedFarmId || undefined) + setLoading(false) } // ── Confidence colour ──────────────────────────────────────────────────── @@ -318,9 +363,14 @@ const CropScanTab: React.FC = () => {
{result.predicted_disease_name}
- {result.crop_type && ( -
Crop: {result.crop_type}
- )} +
+ {result.crop_type ? `Crop: ${result.crop_type}` : 'Crop: Unknown'} + {scanSource && ( + + · {scanSource === 'on-device' ? '⚡ On-Device' : '☁️ Server'} + + )} +
{severityBadge}
diff --git a/frontend/src/lib/offlineStorage.ts b/frontend/src/lib/offlineStorage.ts index fb78869..da0317a 100644 --- a/frontend/src/lib/offlineStorage.ts +++ b/frontend/src/lib/offlineStorage.ts @@ -208,4 +208,13 @@ export async function saveOfflineCropScans(scans: OfflineCropScan[], farmId?: st } catch (error) { console.error('Error saving offline crop scans:', error); } +} + +export async function addOfflineCropScan(scan: OfflineCropScan): Promise { + try { + const db = await getDB(); + await db.put('crop-scans', scan); + } catch (error) { + console.error('Error adding single offline crop scan:', error); + } } \ No newline at end of file diff --git a/frontend/src/utils/diseaseData.ts b/frontend/src/utils/diseaseData.ts new file mode 100644 index 0000000..4a499e4 --- /dev/null +++ b/frontend/src/utils/diseaseData.ts @@ -0,0 +1,272 @@ +// Crop disease symptoms and treatments database (mirrors backend/main.py) + +export const DISEASE_SYMPTOMS: Record = { + "apple_apple_scab": ["Olive-green to black spots on leaves", "Crusty scabby lesions on fruit", "Premature leaf drop"], + "apple_black_rot": ["Brown circular leaf lesions with purple halos", "Black rot on fruit surface", "Cankers on branches"], + "apple_cedar_apple_rust": ["Bright orange-yellow spots on upper leaf surface", "Tube-like projections on leaf underside", "Premature defoliation"], + "bean_angular_leaf_spot": ["Angular water-soaked leaf spots", "Brown necrotic patches", "Pod discoloration"], + "bean_rust": ["Small reddish-brown pustules on leaves", "Yellow halos around pustules", "Leaf defoliation under heavy infection"], + "bell_pepper_bacterial_spot": ["Water-soaked lesions on leaves", "Brown necrotic spots with yellow halos", "Fruit scab lesions"], + "cherry_powdery_mildew": ["White powdery fungal growth on leaves", "Leaf curling and distortion", "Stunted shoot growth"], + "corn_cercospora_leaf_spot": ["Gray to tan rectangular lesions", "Lesions parallel to leaf veins", "Premature leaf dying"], + "corn_common_rust": ["Brick-red pustules scattered on both leaf surfaces", "Pustules darken with age", "Leaf yellowing"], + "corn_gray_leaf_spot": ["Rectangular gray to brown lesions", "Lesions delimited by leaf veins", "Severe blighting in humid conditions"], + "corn_northern_leaf_blight": ["Large cigar-shaped gray-green lesions", "Lesions 1-6 inches long", "Premature plant death in severe cases"], + "cotton_aphids": ["Distorted curled leaves", "Sticky honeydew on leaf surfaces", "Sooty mold growth"], + "cotton_army_worm": ["Ragged leaf edges from feeding", "Visible caterpillars on plants", "Defoliation in severe infestations"], + "cotton_bacterial_blight": ["Angular water-soaked leaf spots", "Brown lesions with yellow halos", "Boll rot and stem cankers"], + "cotton_powdery_mildew": ["White powdery coating on leaves", "Yellowing of affected tissue", "Stunted plant growth"], + "cotton_target_spot": ["Circular brown lesions with concentric rings", "Target-like appearance", "Premature leaf drop"], + "diseased_cucumber": ["Yellowing of leaves", "Water-soaked lesions on foliage", "Wilting and vine collapse"], + "diseased_rice": ["Discolored or spotted leaves", "Water-soaked leaf sheaths", "Stunted plant growth"], + "grape_black_rot": ["Brown circular lesions with black borders on leaves", "Mummified shriveled berries", "Black pycnidia dots in lesions"], + "grape_esca_black_measles": ["Interveinal chlorosis and necrosis", "Tiger-stripe pattern on leaves", "Internal wood discoloration"], + "grape_leaf_blight": ["Large irregular brown leaf lesions", "Lesions coalesce leading to defoliation", "Berry shriveling"], + "groundnut_early_leaf_spot": ["Circular dark brown spots on upper leaf surface", "Yellow halos around spots", "Premature defoliation"], + "groundnut_late_leaf_spot": ["Dark brown to black circular spots", "Spots appear more on lower leaf surface", "Severe defoliation"], + "groundnut_nutrition_deficiency": ["Interveinal yellowing", "Stunted growth", "Pale green to yellow leaf color"], + "groundnut_rosette": ["Stunted plant growth", "Mosaic pattern on leaves", "Leaf curling and bunching"], + "groundnut_rust": ["Orange-brown pustules on lower leaf surface", "Yellow spots on upper leaf surface", "Premature defoliation"], + "guava_fruit_fly": ["Puncture marks on fruit surface", "Rotting flesh inside fruit", "Premature fruit drop"], + "guava_stylosa_disease": ["Dark lesions on fruit and leaves", "Fruit cracking and decay", "Leaf spotting"], + "guava_wilt": ["Sudden wilting of leaves", "Yellowing starting from older leaves", "Root and stem rotting"], + "lemon_bacterial_blight": ["Water-soaked leaf spots turning brown", "Leaf yellowing and drop", "Twig dieback"], + "lemon_citrus_canker": ["Raised corky lesions on leaves, fruit, and stems", "Yellow halos around lesions", "Premature fruit drop"], + "lemon_dry_leaf": ["Dry and brittle leaves", "Marginal leaf scorch", "Twig dieback"], + "lemon_greening": ["Asymmetric mottling (blotchy mottle)", "Yellow shoots (lemon shoots)", "Small misshapen bitter fruit"], + "lemon_powdery_mildew": ["White powdery fungal growth on young leaves", "Leaf distortion and curling", "Premature leaf drop"], + "lemon_spider_mites": ["Fine webbing on leaf undersides", "Stippled yellowing of leaves", "Bronzing of leaf surface"], + "peach_bacterial_spot": ["Water-soaked angular leaf spots", "Dark brown lesions with yellow halos", "Fruit spotting and cracking"], + "potato_early_blight": ["Circular dark brown spots with concentric rings on older leaves", "Yellow halos around lesions", "Lesions start on lower older leaves"], + "potato_late_blight": ["Water-soaked pale green to dark brown lesions", "White fungal growth on leaf undersides", "Rapid destruction of foliage"], + "pumpkin_bacterial_leaf_spot": ["Water-soaked angular spots on leaves", "Brown dried lesions with yellow borders", "Fruit surface lesions"], + "pumpkin_downy_mildew": ["Yellow angular spots on upper leaf surface", "Gray-purple fuzzy growth on leaf underside", "Rapid leaf blighting"], + "pumpkin_powdery_mildew": ["White powdery spots on leaf surfaces", "Leaf yellowing and browning", "Premature aging of plant"], + "rice_bacterial_blight": ["Water-soaked wave-like lesions on leaf edges", "Lesions turn yellow then white", "Bacterial ooze in early morning"], + "strawberry_leaf_scorch": ["Purple to reddish-brown spots on leaves", "Spots coalesce leading to blighted appearance", "Leaf edges scorch and dry"], + "sugarcane_bacterial_blight": ["Water-soaked reddish stripe on leaves", "Leaf margins become necrotic", "Stalk rot in severe cases"], + "sugarcane_red_rot": ["Red discoloration of internal stalk tissue", "White patches with red margins in cross-section", "Plant wilting and death"], + "sugarcane_rust": ["Orange-brown pustules on leaf surfaces", "Yellow halos surrounding pustules", "Leaf drying in severe infections"], + "sugarcane_yellow_leaf_disease": ["Yellowing of midrib on lower leaf surface", "Leaf roll and wilting", "Stunted growth and reduced yield"], + "tomato_bacterial_spot": ["Small water-soaked lesions on leaves and fruit", "Dark brown spots with yellow halos", "Fruit spots with raised margins"], + "tomato_early_blight": ["Dark concentric ring lesions (bullseye pattern)", "Lesions start on older lower leaves", "Yellow area around lesions"], + "tomato_late_blight": ["Large water-soaked pale to dark green lesions", "White mold on undersides in humid weather", "Rapid plant collapse"], + "tomato_leaf_mold": ["Pale yellow spots on upper leaf surface", "Olive-green to gray mold on undersides", "Leaf drop under severe infection"], + "tomato_septoria_leaf_spot": ["Circular spots with dark margins and light centers", "Small black pycnidia inside spots", "Progressive yellowing and leaf drop"], + "tomato_spider_mites": ["Fine stippling and bronzing on leaves", "Fine webbing on undersides", "Leaf curling and drying"], + "tomato_target_spot": ["Circular brown spots with concentric rings", "Spots coalesce in wet conditions", "Premature defoliation"], + "tomato_tomato_mosaic_virus": ["Light-dark green mosaic pattern on leaves", "Leaf distortion and cupping", "Stunted plant growth"], + "tomato_tomato_yellow_leaf_curl_virus": ["Upward leaf curling and yellowing", "Stunted compact plant growth", "Flower drop and low fruit set"], + "wheat_black_rust": ["Dark brown to black pustules on stems and leaves", "Pustules rupture releasing dark spores", "Severe lodging in heavy infection"], + "wheat_blast": ["Bleached or light-colored ear (head)", "Partially filled grain", "Diamond-shaped lesions on leaves"], + "wheat_brown_rust": ["Orange-brown pustules on upper leaf surface", "Pustules circular and scattered", "Leaf yellowing in severe cases"], + "wheat_common_root_rot": ["Browning of roots and crown", "Reduced tillering and plant vigor", "Premature ripening (whiteheads)"], + "wheat_head_blight": ["Pink-salmon fungal growth on spikelets", "Bleached spikelets (Fusarium)", "Shriveled grain"], + "wheat_loose_smut": ["Grain replaced by powdery black mass of spores", "Smut head visible before other heads emerge", "Spores released at flowering"], + "wheat_mildew": ["White powdery fungal colonies on leaves and stems", "Yellowing of tissue under colonies", "Reduced photosynthesis"], + "wheat_mite": ["Silvery streaking on leaves", "Leaf curling and withering", "Stunted plant growth"], + "wheat_septoria": ["Irregular tan to brown lesions with dark borders", "Small black pycnidia inside lesions", "Lesions coalesce in wet weather"], + "wheat_stem_fly": ["Dead heart in young plants", "Stem discoloration inside", "Whiteheads at heading stage"], + "wheat_yellow_rust": ["Bright yellow pustules in stripes along leaf veins", "Cool-weather disease", "Severe early attack causes complete leaf yellowing"], +}; + +export const DISEASE_TREATMENTS: Record = { + "apple_apple_scab": ["Apply preventive fungicides (captan, mancozeb) in spring", "Rake and destroy fallen leaves", "Plant resistant varieties"], + "apple_black_rot": ["Remove and destroy infected fruit and cankered wood", "Apply copper-based fungicides", "Improve air circulation through pruning"], + "apple_cedar_apple_rust": ["Remove nearby juniper/cedar trees if possible", "Apply fungicides from pink bud stage", "Use resistant apple varieties"], + "bean_angular_leaf_spot": ["Use certified disease-free seeds", "Apply copper-based bactericides", "Avoid overhead irrigation"], + "bean_rust": ["Apply mancozeb or triazole fungicides at first sign", "Improve plant spacing for air circulation", "Remove and destroy infected plant debris"], + "bell_pepper_bacterial_spot": ["Apply copper bactericides preventively", "Use disease-free transplants", "Avoid working in field when wet"], + "cherry_powdery_mildew": ["Apply sulfur or myclobutanil fungicides", "Prune to improve air circulation", "Avoid excess nitrogen fertilization"], + "corn_cercospora_leaf_spot": ["Plant resistant hybrids", "Apply strobilurin or triazole fungicides", "Rotate crops away from corn"], + "corn_common_rust": ["Plant resistant corn hybrids", "Apply foliar fungicides if infection is early and severe", "Scout fields regularly"], + "corn_gray_leaf_spot": ["Plant resistant hybrids", "Rotate crops", "Apply triazole or strobilurin fungicides"], + "corn_northern_leaf_blight": ["Use resistant hybrids", "Apply foliar fungicides at tasseling", "Practice crop rotation"], + "cotton_aphids": ["Release beneficial insects (ladybugs, lacewings)", "Apply insecticidal soap or neem oil", "Use systemic insecticides if severe"], + "cotton_army_worm": ["Apply Bt (Bacillus thuringiensis) or chemical insecticides", "Monitor with pheromone traps", "Practice crop rotation"], + "cotton_bacterial_blight": ["Use disease-free seeds", "Apply copper bactericides", "Remove and destroy infected plants"], + "cotton_powdery_mildew": ["Apply sulfur or myclobutanil fungicides", "Improve plant spacing", "Avoid high nitrogen levels"], + "cotton_target_spot": ["Apply triazole or strobilurin fungicides", "Remove crop debris after harvest", "Rotate crops"], + "diseased_cucumber": ["Identify specific disease and apply appropriate fungicide/bactericide", "Improve drainage and air circulation", "Remove infected plant parts"], + "diseased_rice": ["Identify specific disease; apply appropriate management", "Ensure proper water management", "Use resistant varieties"], + "grape_black_rot": ["Apply myclobutanil or mancozeb from budbreak", "Remove mummified berries and infected canes", "Improve air circulation through pruning"], + "grape_esca_black_measles": ["No complete cure; manage through pruning infected wood", "Protect pruning wounds with paste", "Maintain vine vigor with proper nutrition"], + "grape_leaf_blight": ["Apply copper fungicides preventively", "Remove and destroy infected leaves", "Ensure good vineyard sanitation"], + "groundnut_early_leaf_spot": ["Apply chlorothalonil or mancozeb fungicides", "Rotate crops with non-host plants", "Use resistant varieties"], + "groundnut_late_leaf_spot": ["Apply tebuconazole or chlorothalonil", "Maintain proper plant spacing", "Remove crop debris after harvest"], + "groundnut_nutrition_deficiency": ["Test soil and apply deficient nutrients", "Apply balanced NPK fertilizers", "Use foliar micronutrient sprays"], + "groundnut_rosette": ["Control aphid vectors with insecticides", "Use resistant varieties", "Remove and destroy infected plants early"], + "groundnut_rust": ["Apply mancozeb or triazole fungicides", "Plant early to avoid peak rust season", "Use resistant varieties"], + "guava_fruit_fly": ["Use protein bait traps", "Apply cover sprays of insecticide", "Bag fruits when young"], + "guava_stylosa_disease": ["Remove and destroy infected plant parts", "Apply copper-based fungicides", "Maintain tree vigor through proper nutrition"], + "guava_wilt": ["No effective cure; remove infected trees", "Improve soil drainage", "Plant resistant rootstocks"], + "lemon_bacterial_blight": ["Apply copper bactericides preventively", "Prune infected branches", "Avoid overhead irrigation"], + "lemon_citrus_canker": ["Apply copper bactericides", "Remove and destroy infected plant material", "Use certified disease-free nursery stock"], + "lemon_dry_leaf": ["Improve irrigation management", "Apply balanced fertilizers", "Check for root problems"], + "lemon_greening": ["Control Asian citrus psyllid vector with insecticides", "Remove infected trees immediately", "Use certified disease-free planting material"], + "lemon_powdery_mildew": ["Apply sulfur-based fungicides on new growth", "Improve air circulation", "Avoid excess nitrogen"], + "lemon_spider_mites": ["Apply miticides (abamectin, bifenazate)", "Encourage natural predators", "Use water sprays to reduce populations"], + "peach_bacterial_spot": ["Apply copper bactericides during dormancy", "Use resistant varieties", "Avoid overhead irrigation"], + "potato_early_blight": ["Apply mancozeb or chlorothalonil fungicides preventively", "Rotate crops", "Ensure proper plant nutrition especially potassium"], + "potato_late_blight": ["Apply metalaxyl + mancozeb or cymoxanil based fungicides", "Destroy infected volunteers and cull piles", "Plant certified disease-free seed"], + "pumpkin_bacterial_leaf_spot": ["Apply copper-based bactericides", "Use disease-free seeds", "Avoid overhead irrigation"], + "pumpkin_downy_mildew": ["Apply metalaxyl-based fungicides preventively", "Improve field drainage", "Use resistant varieties"], + "pumpkin_powdery_mildew": ["Apply sulfur or myclobutanil fungicides", "Improve air circulation", "Avoid excessive nitrogen"], + "rice_bacterial_blight": ["Use resistant varieties", "Apply copper bactericides", "Avoid excess nitrogen and flood water from infected areas"], + "strawberry_leaf_scorch": ["Apply captan or myclobutanil fungicides", "Remove infected leaves", "Improve plant spacing and air circulation"], + "sugarcane_bacterial_blight": ["Use disease-free setts for planting", "Apply copper bactericides", "Remove and destroy infected plant material"], + "sugarcane_red_rot": ["Plant resistant varieties", "Use disease-free setts", "Treat setts with fungicide before planting"], + "sugarcane_rust": ["Apply mancozeb or triadimefon fungicides", "Plant resistant varieties", "Scout fields regularly"], + "sugarcane_yellow_leaf_disease": ["Control aphid vectors", "Use clean planting material", "Remove and destroy infected plants"], + "tomato_bacterial_spot": ["Apply copper bactericides preventively", "Use disease-free transplants", "Avoid overhead irrigation"], + "tomato_early_blight": ["Apply chlorothalonil or mancozeb fungicides", "Remove lower infected leaves", "Maintain adequate plant nutrition"], + "tomato_late_blight": ["Apply metalaxyl + mancozeb or cymoxanil based fungicides immediately", "Remove and destroy infected plants", "Avoid overhead irrigation"], + "tomato_leaf_mold": ["Improve greenhouse ventilation", "Apply fungicides (chlorothalonil, mancozeb)", "Use resistant varieties"], + "tomato_septoria_leaf_spot": ["Apply chlorothalonil or mancozeb fungicides", "Remove infected lower leaves", "Avoid overhead irrigation"], + "tomato_spider_mites": ["Apply miticides (abamectin, spiromesifen)", "Use predatory mites for biological control", "Maintain plant water stress to minimum"], + "tomato_target_spot": ["Apply chlorothalonil or mancozeb fungicides", "Improve air circulation", "Remove plant debris"], + "tomato_tomato_mosaic_virus": ["Remove and destroy infected plants", "Control insect vectors", "Disinfect tools and hands frequently"], + "tomato_tomato_yellow_leaf_curl_virus": ["Control whitefly vectors with insecticides", "Use reflective mulches to deter whiteflies", "Plant resistant varieties"], + "wheat_black_rust": ["Apply triazole fungicides at first pustule appearance", "Plant resistant varieties", "Monitor fields regularly during warm humid weather"], + "wheat_blast": ["Apply tebuconazole at heading", "Use resistant varieties where available", "Avoid planting in areas with high disease pressure"], + "wheat_brown_rust": ["Apply triazole or strobulurin fungicides", "Plant resistant varieties", "Scout fields regularly"], + "wheat_common_root_rot": ["Seed treatment with fungicides (thiram, carboxin)", "Rotate crops", "Reduce soil compaction"], + "wheat_head_blight": ["Apply tebuconazole or metconazole at flowering", "Avoid planting after corn or in areas with high Fusarium", "Use resistant varieties"], + "wheat_loose_smut": ["Treat seeds with systemic fungicides (carboxin, tebuconazole)", "Use certified smut-free seeds", "Hot water seed treatment"], + "wheat_mildew": ["Apply triazole or sulfur-based fungicides", "Plant resistant varieties", "Avoid excess nitrogen fertilization"], + "wheat_mite": ["Apply miticides or acaricides", "Remove crop debris", "Monitor edges of fields first"], + "wheat_septoria": ["Apply triazole fungicides from GS31", "Use resistant varieties", "Avoid dense sowing"], + "wheat_stem_fly": ["Apply systemic insecticides at tillering", "Early sowing to escape peak infestation", "Remove and destroy stubble after harvest"], + "wheat_yellow_rust": ["Apply triazole fungicides immediately on detection", "Plant resistant varieties", "Scout fields regularly in cool wet spring weather"], +}; + +export const CLASS_NAMES: string[] = [ + "apple_apple_scab", + "apple_black_rot", + "apple_cedar_apple_rust", + "bean_angular_leaf_spot", + "bean_rust", + "bell_pepper_bacterial_spot", + "cherry_powdery_mildew", + "corn_cercospora_leaf_spot", + "corn_common_rust", + "corn_gray_leaf_spot", + "corn_northern_leaf_blight", + "cotton_aphids", + "cotton_army_worm", + "cotton_bacterial_blight", + "cotton_powdery_mildew", + "cotton_target_spot", + "diseased_cucumber", + "diseased_rice", + "grape_black_rot", + "grape_esca_black_measles", + "grape_leaf_blight", + "groundnut_early_leaf_spot", + "groundnut_late_leaf_spot", + "groundnut_nutrition_deficiency", + "groundnut_rosette", + "groundnut_rust", + "guava_fruit_fly", + "guava_stylosa_disease", + "guava_wilt", + "healthy_apple", + "healthy_bean", + "healthy_cherry", + "healthy_corn", + "healthy_cotton", + "healthy_cucumber", + "healthy_grapes", + "healthy_groundnut", + "healthy_guava", + "healthy_lemon", + "healthy_peach", + "healthy_pepper", + "healthy_potato", + "healthy_pumpkin", + "healthy_rice", + "healthy_strawberry", + "healthy_sugarcane", + "healthy_tomato", + "healthy_wheat", + "lemon_bacterial_blight", + "lemon_citrus_canker", + "lemon_dry_leaf", + "lemon_greening", + "lemon_powdery_mildew", + "lemon_spider_mites", + "peach_bacterial_spot", + "potato_early_blight", + "potato_late_blight", + "pumpkin_bacterial_leaf_spot", + "pumpkin_downy_mildew", + "pumpkin_powdery_mildew", + "rice_bacterial_blight", + "strawberry_leaf_scorch", + "sugarcane_bacterial_blight", + "sugarcane_red_rot", + "sugarcane_rust", + "sugarcane_yellow_leaf_disease", + "tomato_bacterial_spot", + "tomato_early_blight", + "tomato_late_blight", + "tomato_leaf_mold", + "tomato_septoria_leaf_spot", + "tomato_spider_mites", + "tomato_target_spot", + "tomato_tomato_mosaic_virus", + "tomato_tomato_yellow_leaf_curl_virus", + "wheat_black_rust", + "wheat_blast", + "wheat_brown_rust", + "wheat_common_root_rot", + "wheat_head_blight", + "wheat_loose_smut", + "wheat_mildew", + "wheat_mite", + "wheat_septoria", + "wheat_stem_fly", + "wheat_yellow_rust" +]; + +export function formatClassName(raw: string): string { + return raw.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase()); +} + +export function extractCropType(className: string): string { + // Strip common prefixes + let cleanName = className; + if (className.startsWith("healthy_")) { + cleanName = className.substring(8); + } else if (className.startsWith("diseased_")) { + cleanName = className.substring(9); + } + + // Handle multi-word crop names (e.g. bell_pepper) + let cropRaw = ""; + if (cleanName.startsWith("bell_pepper_")) { + cropRaw = "bell_pepper"; + } else { + // Standard crops: first segment before underscore + cropRaw = cleanName.split("_")[0] || "Unknown"; + } + + // Capitalize each token and format + return cropRaw + .replace(/_/g, " ") + .replace(/\b\w/g, c => c.toUpperCase()); +} + +export function getSymptoms(className: string): string[] { + if (className.startsWith("healthy_")) { + return ["No disease symptoms detected", "Plant appears healthy"]; + } + return DISEASE_SYMPTOMS[className] || [`Consult an agronomist for ${formatClassName(className)} symptoms`]; +} + +export function getTreatments(className: string): string[] { + if (className.startsWith("healthy_")) { + return ["Continue current management practices", "Monitor regularly for early disease signs"]; + } + return DISEASE_TREATMENTS[className] || [`Consult an agronomist for ${formatClassName(className)} treatment`]; +} diff --git a/frontend/src/utils/onnxInference.ts b/frontend/src/utils/onnxInference.ts new file mode 100644 index 0000000..e0ae8f7 --- /dev/null +++ b/frontend/src/utils/onnxInference.ts @@ -0,0 +1,210 @@ +import { + formatClassName, + extractCropType, + getSymptoms, + getTreatments, + CLASS_NAMES +} from './diseaseData'; + +export interface LocalDiagnosisResult { + predicted_disease_name: string; + confidence_score: number; + is_healthy: boolean; + crop_type?: string; + symptoms: string[]; + recommended_action: string[]; +} + +let session: any = null; +let sessionPromise: Promise | null = null; + +/** + * Lazy loads and returns the ONNX runtime session. + * Configures the WebAssembly binary runtime paths to point locally to /wasm/ + * to ensure that the app can run fully offline inside the service worker cache. + * Uses a cached promise to prevent race conditions during concurrent startup calls. + */ +async function getInferenceSession(): Promise { + if (session) { + return session; + } + if (sessionPromise) { + return sessionPromise; + } + + if (typeof window === 'undefined') { + throw new Error('ONNX Runtime Web is only supported in browser environments.'); + } + + sessionPromise = (async () => { + try { + const ort = await import('onnxruntime-web'); + + // Crucial: Set WASM path locally for PWA offline support + ort.env.wasm.wasmPaths = '/wasm/'; + + // Load model from public folder + const activeSession = await ort.InferenceSession.create('/model/plant_disease_resnet18.onnx', { + executionProviders: ['wasm'] + }); + session = activeSession; + console.log('[LocalInference] ONNX Inference Session loaded successfully.'); + return session; + } catch (error) { + sessionPromise = null; // Clear cached promise on failure to allow retry + console.error('[LocalInference] Failed to load ONNX session:', error); + throw new Error('Could not initialize the local disease scanner model.'); + } + })(); + + return sessionPromise; +} + +/** + * Converts a browser File object to an HTMLImageElement + */ +function fileToImage(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (e) => { + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = () => reject(new Error('Failed to load image element')); + img.src = e.target?.result as string; + }; + reader.onerror = () => reject(new Error('Failed to read image file')); + reader.readAsDataURL(file); + }); +} + +/** + * Resizes the input image element to 224x224 using an offscreen canvas + */ +function resizeImageToCanvas(img: HTMLImageElement): HTMLCanvasElement { + const canvas = document.createElement('canvas'); + canvas.width = 224; + canvas.height = 224; + const ctx = canvas.getContext('2d'); + if (!ctx) { + throw new Error('Failed to get 2D canvas context for resizing'); + } + + // Draw image scaled to 224x224 + ctx.drawImage(img, 0, 0, 224, 224); + return canvas; +} + +/** + * Extracts raw RGBA pixels from canvas, normalizes using ImageNet stats, + * and converts to a planar CHW Float32Array format expected by the model. + */ +function preprocessCanvasToFlatArray(canvas: HTMLCanvasElement): Float32Array { + const ctx = canvas.getContext('2d'); + if (!ctx) { + throw new Error('Failed to get 2D canvas context for pixel extraction'); + } + + const imgData = ctx.getImageData(0, 0, 224, 224); + const data = imgData.data; // Uint8ClampedArray: R,G,B,A,R,G,B,A... + + const mean = [0.485, 0.456, 0.406]; + const std = [0.229, 0.224, 0.225]; + + // Allocate space for 3 channels * 224 width * 224 height + const floatArray = new Float32Array(3 * 224 * 224); + + // Convert interleaved RGBA to planar RGB and normalize + for (let i = 0; i < 224 * 224; i++) { + const r = data[i * 4]; + const g = data[i * 4 + 1]; + const b = data[i * 4 + 2]; + + // Compute standard ImageNet normalization: (x / 255.0 - mean) / std + floatArray[i] = (r / 255.0 - mean[0]) / std[0]; // Red plane + floatArray[224 * 224 + i] = (g / 255.0 - mean[1]) / std[1]; // Green plane + floatArray[2 * 224 * 224 + i] = (b / 255.0 - mean[2]) / std[2]; // Blue plane + } + + return floatArray; +} + +/** + * Performs client-side machine learning inference on an uploaded plant image file. + * Returns a standardized DiagnosisResult locally with zero network requests. + */ +export async function runLocalONNXInference(file: File): Promise { + try { + const ort = await import('onnxruntime-web'); + const activeSession = await getInferenceSession(); + + // 1. Process image file + const img = await fileToImage(file); + const canvas = resizeImageToCanvas(img); + const floatArray = preprocessCanvasToFlatArray(canvas); + + // 2. Create multi-dimensional ONNX tensor [1, 3, 224, 224] + const inputTensor = new ort.Tensor('float32', floatArray, [1, 3, 224, 224]); + + // 3. Execute inference using WASM execution provider with dynamic input/output names + const inputName = activeSession.inputNames[0] || 'input'; + const outputName = activeSession.outputNames[0] || 'output'; + + const feeds = { [inputName]: inputTensor }; + const results = await activeSession.run(feeds); + + // 4. Retrieve logits output tensor dynamically + const outputTensor = results[outputName]; + if (!outputTensor || !outputTensor.data) { + throw new Error('Model inference returned an empty output tensor.'); + } + const output = outputTensor.data as Float32Array; + + // Assert that logits output size matches the local CLASS_NAMES database size + if (output.length !== CLASS_NAMES.length) { + throw new Error( + `ONNX model output shape mismatch: got logits length of ${output.length}, ` + + `but expected ${CLASS_NAMES.length} classes based on local database.` + ); + } + + // 5. Postprocess: Argmax + numerically stable Softmax + let maxLogit = -Infinity; + let maxIdx = 0; + + for (let i = 0; i < output.length; i++) { + if (output[i] > maxLogit) { + maxLogit = output[i]; + maxIdx = i; + } + } + + let sumExp = 0.0; + const exps = new Float32Array(output.length); + for (let i = 0; i < output.length; i++) { + exps[i] = Math.exp(output[i] - maxLogit); + sumExp += exps[i]; + } + + const confidence = (exps[maxIdx] / sumExp) * 100.0; + + // 6. Map predictions to local classes + const className = CLASS_NAMES[maxIdx]; + if (!className) { + throw new Error(`Inference returned class index ${maxIdx} which is out of bounds.`); + } + + const isHealthy = className.startsWith('healthy_'); + + return { + predicted_disease_name: formatClassName(className), + confidence_score: parseFloat(confidence.toFixed(2)), + is_healthy: isHealthy, + crop_type: extractCropType(className), + symptoms: getSymptoms(className), + recommended_action: getTreatments(className) + }; + } catch (error) { + console.error('[LocalInference] Error during on-device inference:', error); + throw error; + } +}