Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions backend/export_to_onnx.py
Original file line number Diff line number Diff line change
@@ -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."
)

Comment thread
vedant-kawale-27 marked this conversation as resolved.
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()
24 changes: 23 additions & 1 deletion frontend/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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],
},
},
},
Comment thread
vedant-kawale-27 marked this conversation as resolved.
{
// Cache API responses for farms and crop-scans
// Use matchCallback: Workbox tests RegExp against full URL, not just pathname
Expand Down Expand Up @@ -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,
Expand Down
125 changes: 125 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added frontend/public/wasm/ort-wasm-simd-threaded.wasm
Binary file not shown.
Loading