Spaces:
Sleeping
Sleeping
Fadhili Sumaye commited on
Commit ·
573aa2a
1
Parent(s): 04dd5f9
Remove login screen and revert to public stateless access with anonymous audit logging
Browse files- app/build.gradle.kts +4 -0
- app/src/main/AndroidManifest.xml +2 -2
- app/src/main/java/com/example/pestdetection/ApiConfig.java +19 -0
- app/src/main/java/com/example/pestdetection/MainActivity.java +25 -10
- app/src/main/res/drawable/bg_status_dot.xml +8 -0
- app/src/main/res/drawable/bg_status_pill.xml +6 -0
- app/src/main/res/layout/activity_main.xml +34 -10
- app/src/main/res/values/colors.xml +5 -0
- app/src/main/res/xml/network_security_config.xml +12 -0
- backend/app.py +79 -27
- gradle.properties +4 -1
- gradlew.bat +8 -0
app/build.gradle.kts
CHANGED
|
@@ -28,6 +28,10 @@ android {
|
|
| 28 |
)
|
| 29 |
}
|
| 30 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
compileOptions {
|
| 32 |
sourceCompatibility = JavaVersion.VERSION_11
|
| 33 |
targetCompatibility = JavaVersion.VERSION_11
|
|
|
|
| 28 |
)
|
| 29 |
}
|
| 30 |
}
|
| 31 |
+
lint {
|
| 32 |
+
checkReleaseBuilds = false
|
| 33 |
+
abortOnError = false
|
| 34 |
+
}
|
| 35 |
compileOptions {
|
| 36 |
sourceCompatibility = JavaVersion.VERSION_11
|
| 37 |
targetCompatibility = JavaVersion.VERSION_11
|
app/src/main/AndroidManifest.xml
CHANGED
|
@@ -4,7 +4,6 @@
|
|
| 4 |
|
| 5 |
<uses-permission android:name="android.permission.INTERNET" />
|
| 6 |
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
| 7 |
-
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
| 8 |
<uses-permission android:name="android.permission.CAMERA" />
|
| 9 |
<uses-feature android:name="android.hardware.camera" android:required="true" />
|
| 10 |
|
|
@@ -22,7 +21,7 @@
|
|
| 22 |
android:label="@string/app_name"
|
| 23 |
android:roundIcon="@mipmap/ic_launcher_round"
|
| 24 |
android:supportsRtl="true"
|
| 25 |
-
android:
|
| 26 |
android:theme="@style/Theme.PestDetection">
|
| 27 |
|
| 28 |
<activity
|
|
@@ -34,6 +33,7 @@
|
|
| 34 |
</intent-filter>
|
| 35 |
</activity>
|
| 36 |
|
|
|
|
| 37 |
<provider
|
| 38 |
android:name="androidx.core.content.FileProvider"
|
| 39 |
android:authorities="com.example.pestdetection.fileprovider"
|
|
|
|
| 4 |
|
| 5 |
<uses-permission android:name="android.permission.INTERNET" />
|
| 6 |
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
|
|
| 7 |
<uses-permission android:name="android.permission.CAMERA" />
|
| 8 |
<uses-feature android:name="android.hardware.camera" android:required="true" />
|
| 9 |
|
|
|
|
| 21 |
android:label="@string/app_name"
|
| 22 |
android:roundIcon="@mipmap/ic_launcher_round"
|
| 23 |
android:supportsRtl="true"
|
| 24 |
+
android:networkSecurityConfig="@xml/network_security_config"
|
| 25 |
android:theme="@style/Theme.PestDetection">
|
| 26 |
|
| 27 |
<activity
|
|
|
|
| 33 |
</intent-filter>
|
| 34 |
</activity>
|
| 35 |
|
| 36 |
+
|
| 37 |
<provider
|
| 38 |
android:name="androidx.core.content.FileProvider"
|
| 39 |
android:authorities="com.example.pestdetection.fileprovider"
|
app/src/main/java/com/example/pestdetection/ApiConfig.java
CHANGED
|
@@ -27,6 +27,18 @@ public final class ApiConfig {
|
|
| 27 |
return toHealthUrl(getPredictUrl(context));
|
| 28 |
}
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
public static void savePredictUrl(Context context, String url) {
|
| 31 |
if (url == null || url.trim().isEmpty()) {
|
| 32 |
return;
|
|
@@ -59,4 +71,11 @@ public final class ApiConfig {
|
|
| 59 |
}
|
| 60 |
return predictUrl + "/health";
|
| 61 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
}
|
|
|
|
| 27 |
return toHealthUrl(getPredictUrl(context));
|
| 28 |
}
|
| 29 |
|
| 30 |
+
public static String getLoginUrl(Context context) {
|
| 31 |
+
return toAuthUrl(getPredictUrl(context), "/auth/login");
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
public static String getRegisterUrl(Context context) {
|
| 35 |
+
return toAuthUrl(getPredictUrl(context), "/auth/register");
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
public static String getHistoryUrl(Context context) {
|
| 39 |
+
return toAuthUrl(getPredictUrl(context), "/history");
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
public static void savePredictUrl(Context context, String url) {
|
| 43 |
if (url == null || url.trim().isEmpty()) {
|
| 44 |
return;
|
|
|
|
| 71 |
}
|
| 72 |
return predictUrl + "/health";
|
| 73 |
}
|
| 74 |
+
|
| 75 |
+
private static String toAuthUrl(String predictUrl, String endpoint) {
|
| 76 |
+
if (predictUrl.endsWith("/predict")) {
|
| 77 |
+
return predictUrl.substring(0, predictUrl.length() - "/predict".length()) + endpoint;
|
| 78 |
+
}
|
| 79 |
+
return predictUrl + endpoint;
|
| 80 |
+
}
|
| 81 |
}
|
app/src/main/java/com/example/pestdetection/MainActivity.java
CHANGED
|
@@ -27,7 +27,8 @@ import androidx.core.content.FileProvider;
|
|
| 27 |
public class MainActivity extends AppCompatActivity {
|
| 28 |
|
| 29 |
ImageView imageView;
|
| 30 |
-
TextView resultText, treatmentText, tvConnectionStatus;
|
|
|
|
| 31 |
Uri imageUri;
|
| 32 |
EditText etServerUrl;
|
| 33 |
|
|
@@ -70,6 +71,9 @@ public class MainActivity extends AppCompatActivity {
|
|
| 70 |
resultText = findViewById(R.id.resultText);
|
| 71 |
treatmentText = findViewById(R.id.treatmentText);
|
| 72 |
tvConnectionStatus = findViewById(R.id.tvConnectionStatus);
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
etServerUrl = findViewById(R.id.etServerUrl);
|
| 75 |
View settingsHeader = findViewById(R.id.settingsHeader);
|
|
@@ -112,7 +116,6 @@ public class MainActivity extends AppCompatActivity {
|
|
| 112 |
@Override
|
| 113 |
protected void onStop() {
|
| 114 |
super.onStop();
|
| 115 |
-
// Reset UI to default state when the user goes out of the application
|
| 116 |
if (imageView != null) {
|
| 117 |
imageView.setImageResource(R.mipmap.ic_launcher);
|
| 118 |
}
|
|
@@ -194,19 +197,31 @@ public class MainActivity extends AppCompatActivity {
|
|
| 194 |
}
|
| 195 |
|
| 196 |
private void updateConnectionStatus(Boolean connected, String message) {
|
| 197 |
-
if (
|
| 198 |
-
tvConnectionStatus.setText("● " + message);
|
| 199 |
-
tvConnectionStatus.setTextColor(getColor(R.color.text_hint));
|
| 200 |
return;
|
| 201 |
}
|
| 202 |
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
} else {
|
| 207 |
-
|
| 208 |
-
|
|
|
|
| 209 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
}
|
| 211 |
|
| 212 |
private void openGallery() {
|
|
|
|
| 27 |
public class MainActivity extends AppCompatActivity {
|
| 28 |
|
| 29 |
ImageView imageView;
|
| 30 |
+
TextView resultText, treatmentText, tvConnectionStatus, tvAppSubtitle;
|
| 31 |
+
View statusBadgeContainer, statusIndicatorDot;
|
| 32 |
Uri imageUri;
|
| 33 |
EditText etServerUrl;
|
| 34 |
|
|
|
|
| 71 |
resultText = findViewById(R.id.resultText);
|
| 72 |
treatmentText = findViewById(R.id.treatmentText);
|
| 73 |
tvConnectionStatus = findViewById(R.id.tvConnectionStatus);
|
| 74 |
+
statusBadgeContainer = findViewById(R.id.statusBadgeContainer);
|
| 75 |
+
statusIndicatorDot = findViewById(R.id.statusIndicatorDot);
|
| 76 |
+
tvAppSubtitle = findViewById(R.id.tvAppSubtitle);
|
| 77 |
|
| 78 |
etServerUrl = findViewById(R.id.etServerUrl);
|
| 79 |
View settingsHeader = findViewById(R.id.settingsHeader);
|
|
|
|
| 116 |
@Override
|
| 117 |
protected void onStop() {
|
| 118 |
super.onStop();
|
|
|
|
| 119 |
if (imageView != null) {
|
| 120 |
imageView.setImageResource(R.mipmap.ic_launcher);
|
| 121 |
}
|
|
|
|
| 197 |
}
|
| 198 |
|
| 199 |
private void updateConnectionStatus(Boolean connected, String message) {
|
| 200 |
+
if (statusBadgeContainer == null || statusIndicatorDot == null || tvConnectionStatus == null) {
|
|
|
|
|
|
|
| 201 |
return;
|
| 202 |
}
|
| 203 |
|
| 204 |
+
int colorBg, colorDotText;
|
| 205 |
+
String statusText;
|
| 206 |
+
|
| 207 |
+
if (connected == null) {
|
| 208 |
+
colorBg = getColor(R.color.status_gray_bg);
|
| 209 |
+
colorDotText = getColor(R.color.text_hint);
|
| 210 |
+
statusText = "Checking...";
|
| 211 |
+
} else if (connected) {
|
| 212 |
+
colorBg = getColor(R.color.primary_green_light);
|
| 213 |
+
colorDotText = getColor(R.color.primary_green);
|
| 214 |
+
statusText = "Online";
|
| 215 |
} else {
|
| 216 |
+
colorBg = getColor(R.color.status_red_bg);
|
| 217 |
+
colorDotText = getColor(R.color.status_red_text);
|
| 218 |
+
statusText = "Offline";
|
| 219 |
}
|
| 220 |
+
|
| 221 |
+
statusBadgeContainer.setBackgroundTintList(android.content.res.ColorStateList.valueOf(colorBg));
|
| 222 |
+
statusIndicatorDot.setBackgroundTintList(android.content.res.ColorStateList.valueOf(colorDotText));
|
| 223 |
+
tvConnectionStatus.setTextColor(colorDotText);
|
| 224 |
+
tvConnectionStatus.setText(statusText);
|
| 225 |
}
|
| 226 |
|
| 227 |
private void openGallery() {
|
app/src/main/res/drawable/bg_status_dot.xml
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="utf-8"?>
|
| 2 |
+
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
| 3 |
+
android:shape="oval">
|
| 4 |
+
<solid android:color="#FFFFFF" />
|
| 5 |
+
<size
|
| 6 |
+
android:width="8dp"
|
| 7 |
+
android:height="8dp" />
|
| 8 |
+
</shape>
|
app/src/main/res/drawable/bg_status_pill.xml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="utf-8"?>
|
| 2 |
+
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
| 3 |
+
android:shape="rectangle">
|
| 4 |
+
<solid android:color="#FFFFFF" />
|
| 5 |
+
<corners android:radius="100dp" />
|
| 6 |
+
</shape>
|
app/src/main/res/layout/activity_main.xml
CHANGED
|
@@ -35,29 +35,53 @@
|
|
| 35 |
app:layout_constraintTop_toBottomOf="@id/tvAppName"
|
| 36 |
app:layout_constraintStart_toStartOf="parent" />
|
| 37 |
|
| 38 |
-
<
|
| 39 |
-
|
|
|
|
| 40 |
android:layout_width="wrap_content"
|
| 41 |
android:layout_height="wrap_content"
|
| 42 |
-
android:
|
| 43 |
-
android:
|
| 44 |
-
android:
|
| 45 |
-
android:
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
<!-- Server Connection Settings Card -->
|
| 50 |
<com.google.android.material.card.MaterialCardView
|
| 51 |
android:id="@+id/settingsCard"
|
| 52 |
android:layout_width="0dp"
|
| 53 |
android:layout_height="wrap_content"
|
| 54 |
-
android:layout_marginTop="
|
| 55 |
app:cardCornerRadius="16dp"
|
| 56 |
app:cardElevation="2dp"
|
| 57 |
app:cardBackgroundColor="@color/surface_card"
|
| 58 |
app:strokeColor="@color/primary_green_light"
|
| 59 |
app:strokeWidth="1dp"
|
| 60 |
-
app:layout_constraintTop_toBottomOf="@id/
|
| 61 |
app:layout_constraintStart_toStartOf="parent"
|
| 62 |
app:layout_constraintEnd_toEndOf="parent">
|
| 63 |
|
|
|
|
| 35 |
app:layout_constraintTop_toBottomOf="@id/tvAppName"
|
| 36 |
app:layout_constraintStart_toStartOf="parent" />
|
| 37 |
|
| 38 |
+
<!-- Status Pill Badge -->
|
| 39 |
+
<LinearLayout
|
| 40 |
+
android:id="@+id/statusBadgeContainer"
|
| 41 |
android:layout_width="wrap_content"
|
| 42 |
android:layout_height="wrap_content"
|
| 43 |
+
android:orientation="horizontal"
|
| 44 |
+
android:gravity="center_vertical"
|
| 45 |
+
android:paddingStart="12dp"
|
| 46 |
+
android:paddingEnd="12dp"
|
| 47 |
+
android:paddingTop="6dp"
|
| 48 |
+
android:paddingBottom="6dp"
|
| 49 |
+
android:background="@drawable/bg_status_pill"
|
| 50 |
+
app:layout_constraintTop_toTopOf="@id/tvAppName"
|
| 51 |
+
app:layout_constraintBottom_toBottomOf="@id/tvAppSubtitle"
|
| 52 |
+
app:layout_constraintEnd_toEndOf="parent">
|
| 53 |
+
|
| 54 |
+
<View
|
| 55 |
+
android:id="@+id/statusIndicatorDot"
|
| 56 |
+
android:layout_width="8dp"
|
| 57 |
+
android:layout_height="8dp"
|
| 58 |
+
android:layout_marginEnd="6dp"
|
| 59 |
+
android:background="@drawable/bg_status_dot" />
|
| 60 |
+
|
| 61 |
+
<TextView
|
| 62 |
+
android:id="@+id/tvConnectionStatus"
|
| 63 |
+
android:layout_width="wrap_content"
|
| 64 |
+
android:layout_height="wrap_content"
|
| 65 |
+
android:text="Checking..."
|
| 66 |
+
android:textColor="@color/text_hint"
|
| 67 |
+
android:textSize="11sp"
|
| 68 |
+
android:textStyle="bold"
|
| 69 |
+
android:textAllCaps="true"
|
| 70 |
+
android:letterSpacing="0.05" />
|
| 71 |
+
</LinearLayout>
|
| 72 |
|
| 73 |
<!-- Server Connection Settings Card -->
|
| 74 |
<com.google.android.material.card.MaterialCardView
|
| 75 |
android:id="@+id/settingsCard"
|
| 76 |
android:layout_width="0dp"
|
| 77 |
android:layout_height="wrap_content"
|
| 78 |
+
android:layout_marginTop="20dp"
|
| 79 |
app:cardCornerRadius="16dp"
|
| 80 |
app:cardElevation="2dp"
|
| 81 |
app:cardBackgroundColor="@color/surface_card"
|
| 82 |
app:strokeColor="@color/primary_green_light"
|
| 83 |
app:strokeWidth="1dp"
|
| 84 |
+
app:layout_constraintTop_toBottomOf="@id/tvAppSubtitle"
|
| 85 |
app:layout_constraintStart_toStartOf="parent"
|
| 86 |
app:layout_constraintEnd_toEndOf="parent">
|
| 87 |
|
app/src/main/res/values/colors.xml
CHANGED
|
@@ -18,4 +18,9 @@
|
|
| 18 |
<!-- Standard Colors -->
|
| 19 |
<color name="black">#000000</color>
|
| 20 |
<color name="white">#FFFFFF</color>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
</resources>
|
|
|
|
| 18 |
<!-- Standard Colors -->
|
| 19 |
<color name="black">#000000</color>
|
| 20 |
<color name="white">#FFFFFF</color>
|
| 21 |
+
|
| 22 |
+
<!-- Status Pill Colors -->
|
| 23 |
+
<color name="status_red_text">#D32F2F</color>
|
| 24 |
+
<color name="status_red_bg">#FFEBEE</color>
|
| 25 |
+
<color name="status_gray_bg">#ECEFF1</color>
|
| 26 |
</resources>
|
app/src/main/res/xml/network_security_config.xml
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="utf-8"?>
|
| 2 |
+
<network-security-config>
|
| 3 |
+
<!-- Allow cleartext HTTP traffic only for local developer environments -->
|
| 4 |
+
<domain-config cleartextTrafficPermitted="true">
|
| 5 |
+
<domain includeSubdomains="true">localhost</domain>
|
| 6 |
+
<domain includeSubdomains="true">127.0.0.1</domain>
|
| 7 |
+
<domain includeSubdomains="true">10.0.2.2</domain> <!-- Android Emulator loopback to host -->
|
| 8 |
+
</domain-config>
|
| 9 |
+
|
| 10 |
+
<!-- Enforce HTTPS for everything else globally -->
|
| 11 |
+
<base-config cleartextTrafficPermitted="false" />
|
| 12 |
+
</network-security-config>
|
backend/app.py
CHANGED
|
@@ -1,21 +1,53 @@
|
|
| 1 |
-
from fastapi import FastAPI, File, UploadFile, HTTPException
|
| 2 |
from fastapi.responses import PlainTextResponse
|
| 3 |
import os
|
| 4 |
-
import shutil
|
| 5 |
from pathlib import Path
|
| 6 |
-
from werkzeug.utils import secure_filename
|
| 7 |
import threading
|
| 8 |
import time
|
| 9 |
import urllib.request
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
BASE_DIR = Path(__file__).resolve().parent
|
| 12 |
-
|
| 13 |
MODEL_FILE = BASE_DIR / "best_cereal.pt"
|
| 14 |
MODEL_IP102_FILE = BASE_DIR / "best_ip102.pt"
|
| 15 |
IP102_URL = "https://huggingface.co/underdogquality/yolo11s-pest-detection/resolve/main/best.pt"
|
| 16 |
-
ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg"}
|
| 17 |
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
app = FastAPI(title="Pest Detection API")
|
| 21 |
|
|
@@ -37,7 +69,7 @@ def download_ip102_model():
|
|
| 37 |
temp_file = MODEL_IP102_FILE.with_suffix(".tmp")
|
| 38 |
|
| 39 |
with urllib.request.urlopen(req, timeout=60) as response, open(temp_file, 'wb') as out_file:
|
| 40 |
-
chunk_size = 1024 * 1024
|
| 41 |
bytes_downloaded = 0
|
| 42 |
while True:
|
| 43 |
chunk = response.read(chunk_size)
|
|
@@ -77,14 +109,12 @@ def init_model():
|
|
| 77 |
try:
|
| 78 |
from ultralytics import YOLO
|
| 79 |
|
| 80 |
-
# 1. Look for IP102 model
|
| 81 |
if MODEL_IP102_FILE.exists():
|
| 82 |
model = YOLO(str(MODEL_IP102_FILE))
|
| 83 |
HAS_YOLO = True
|
| 84 |
print(f"Success: Loaded IP102 YOLO model from {MODEL_IP102_FILE} ({len(model.names)} classes)")
|
| 85 |
return
|
| 86 |
|
| 87 |
-
# 2. Try loading a fallback model to serve requests in the meantime
|
| 88 |
loaded_fallback = False
|
| 89 |
if MODEL_FILE.exists():
|
| 90 |
model = YOLO(str(MODEL_FILE))
|
|
@@ -99,15 +129,14 @@ def init_model():
|
|
| 99 |
print(f"Success: Loaded fallback YOLO model from {fallback_path} ({len(model.names)} classes)")
|
| 100 |
loaded_fallback = True
|
| 101 |
|
| 102 |
-
|
| 103 |
-
print("IP102 model is missing. Initiating background download for IP102 model to include all 102 pests...")
|
| 104 |
threading.Thread(target=download_ip102_model, daemon=True).start()
|
| 105 |
except Exception as e:
|
| 106 |
if not HAS_YOLO:
|
| 107 |
HAS_YOLO = False
|
| 108 |
print(f"Warning: Failed to load YOLO model: {e}. Using dummy detection.")
|
| 109 |
else:
|
| 110 |
-
print(f"Warning: Fallback model loaded, but error occurred
|
| 111 |
|
| 112 |
init_model()
|
| 113 |
|
|
@@ -132,7 +161,7 @@ def get_ip102_category_and_treatment(cls_id, raw_name):
|
|
| 132 |
elif 73 <= cls_id <= 91:
|
| 133 |
return "Citrus Pest", "Citrus Pest: Prune infested shoots, encourage beneficial predators (e.g., predatory mites or wasps), use horticultural oil sprays, and apply targeted systemic treatments if necessary."
|
| 134 |
elif 92 <= cls_id <= 101:
|
| 135 |
-
return "Mango Pest", "Mango Pest: Maintain orchard sanitation, prune dense branches to improve sunlight penetration, use sticky bands on tree trunks, and apply specific crop protection sprays during
|
| 136 |
return "Unknown Pest", "Consult with a local agricultural officer for specific treatment advice."
|
| 137 |
|
| 138 |
PEST_TREATMENT = {
|
|
@@ -158,6 +187,8 @@ PEST_TREATMENT = {
|
|
| 158 |
"default": "Consult with a local agricultural officer for specific treatment advice.",
|
| 159 |
}
|
| 160 |
|
|
|
|
|
|
|
| 161 |
def allowed_file(filename):
|
| 162 |
return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
|
| 163 |
|
|
@@ -169,13 +200,13 @@ def index():
|
|
| 169 |
return f"Pest Detection API is running! YOLO Model: {model_info}"
|
| 170 |
|
| 171 |
@app.get("/health")
|
| 172 |
-
def health():
|
| 173 |
-
model_type = "None"
|
| 174 |
classes_count = 0
|
|
|
|
| 175 |
if HAS_YOLO and model:
|
| 176 |
classes_count = len(model.names)
|
| 177 |
model_type = "IP102 (102 pests)" if classes_count == 102 else "Cereal Pests (10 pests)"
|
| 178 |
-
|
| 179 |
return {
|
| 180 |
"status": "ok",
|
| 181 |
"yolo": HAS_YOLO,
|
|
@@ -185,25 +216,37 @@ def health():
|
|
| 185 |
}
|
| 186 |
|
| 187 |
@app.post("/predict")
|
| 188 |
-
async def predict(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
if not image.filename:
|
|
|
|
| 190 |
raise HTTPException(status_code=400, detail="No selected file")
|
| 191 |
|
| 192 |
if not allowed_file(image.filename):
|
|
|
|
| 193 |
raise HTTPException(status_code=400, detail="File type not allowed")
|
| 194 |
|
| 195 |
-
filename = secure_filename(image.filename)
|
| 196 |
-
filepath = UPLOAD_FOLDER / filename
|
| 197 |
-
|
| 198 |
try:
|
| 199 |
-
|
| 200 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
except Exception as e:
|
| 202 |
-
|
|
|
|
| 203 |
|
| 204 |
if HAS_YOLO:
|
| 205 |
try:
|
| 206 |
-
results = model.predict(source=
|
| 207 |
|
| 208 |
detections = []
|
| 209 |
for r in results:
|
|
@@ -226,10 +269,10 @@ async def predict(image: UploadFile = File(...)):
|
|
| 226 |
"confidence": round(conf, 2),
|
| 227 |
"treatment": treatment,
|
| 228 |
})
|
| 229 |
-
|
| 230 |
-
print(f"Detections for {filename}: {detections}")
|
| 231 |
|
| 232 |
if not detections:
|
|
|
|
|
|
|
| 233 |
return {
|
| 234 |
"status": "success",
|
| 235 |
"pest_detected": "None",
|
|
@@ -238,6 +281,13 @@ async def predict(image: UploadFile = File(...)):
|
|
| 238 |
"message": "No pests detected",
|
| 239 |
}
|
| 240 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
return {
|
| 242 |
"status": "success",
|
| 243 |
"pest_detected": detections[0]["pest_detected"],
|
|
@@ -247,8 +297,11 @@ async def predict(image: UploadFile = File(...)):
|
|
| 247 |
"all_detections": detections,
|
| 248 |
}
|
| 249 |
except Exception as e:
|
|
|
|
| 250 |
raise HTTPException(status_code=500, detail=f"Inference error: {str(e)}")
|
| 251 |
else:
|
|
|
|
|
|
|
| 252 |
return {
|
| 253 |
"status": "success",
|
| 254 |
"pest_detected": "Aphids (Mock)",
|
|
@@ -259,6 +312,5 @@ async def predict(image: UploadFile = File(...)):
|
|
| 259 |
|
| 260 |
if __name__ == "__main__":
|
| 261 |
import uvicorn
|
| 262 |
-
|
| 263 |
port = int(os.environ.get("PORT", 5000))
|
| 264 |
uvicorn.run(app, host="0.0.0.0", port=port)
|
|
|
|
| 1 |
+
from fastapi import FastAPI, File, UploadFile, HTTPException, Request
|
| 2 |
from fastapi.responses import PlainTextResponse
|
| 3 |
import os
|
|
|
|
| 4 |
from pathlib import Path
|
|
|
|
| 5 |
import threading
|
| 6 |
import time
|
| 7 |
import urllib.request
|
| 8 |
+
import sqlite3
|
| 9 |
+
import json
|
| 10 |
+
import io
|
| 11 |
+
from PIL import Image
|
| 12 |
+
from typing import Optional
|
| 13 |
|
| 14 |
BASE_DIR = Path(__file__).resolve().parent
|
| 15 |
+
DB_FILE = BASE_DIR / "pest_detection.db"
|
| 16 |
MODEL_FILE = BASE_DIR / "best_cereal.pt"
|
| 17 |
MODEL_IP102_FILE = BASE_DIR / "best_ip102.pt"
|
| 18 |
IP102_URL = "https://huggingface.co/underdogquality/yolo11s-pest-detection/resolve/main/best.pt"
|
|
|
|
| 19 |
|
| 20 |
+
def init_db():
|
| 21 |
+
conn = sqlite3.connect(str(DB_FILE))
|
| 22 |
+
cursor = conn.cursor()
|
| 23 |
+
cursor.execute("""
|
| 24 |
+
CREATE TABLE IF NOT EXISTS audit_logs (
|
| 25 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 26 |
+
username TEXT,
|
| 27 |
+
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 28 |
+
endpoint TEXT NOT NULL,
|
| 29 |
+
status TEXT NOT NULL,
|
| 30 |
+
details TEXT,
|
| 31 |
+
ip_address TEXT
|
| 32 |
+
)
|
| 33 |
+
""")
|
| 34 |
+
conn.commit()
|
| 35 |
+
conn.close()
|
| 36 |
+
|
| 37 |
+
def log_audit(username: Optional[str], endpoint: str, status: str, details: str, ip_address: str):
|
| 38 |
+
try:
|
| 39 |
+
conn = sqlite3.connect(str(DB_FILE))
|
| 40 |
+
cursor = conn.cursor()
|
| 41 |
+
cursor.execute(
|
| 42 |
+
"INSERT INTO audit_logs (username, endpoint, status, details, ip_address) VALUES (?, ?, ?, ?, ?)",
|
| 43 |
+
(username, endpoint, status, details, ip_address)
|
| 44 |
+
)
|
| 45 |
+
conn.commit()
|
| 46 |
+
conn.close()
|
| 47 |
+
except Exception as e:
|
| 48 |
+
print(f"Failed to write audit log: {e}")
|
| 49 |
+
|
| 50 |
+
init_db()
|
| 51 |
|
| 52 |
app = FastAPI(title="Pest Detection API")
|
| 53 |
|
|
|
|
| 69 |
temp_file = MODEL_IP102_FILE.with_suffix(".tmp")
|
| 70 |
|
| 71 |
with urllib.request.urlopen(req, timeout=60) as response, open(temp_file, 'wb') as out_file:
|
| 72 |
+
chunk_size = 1024 * 1024
|
| 73 |
bytes_downloaded = 0
|
| 74 |
while True:
|
| 75 |
chunk = response.read(chunk_size)
|
|
|
|
| 109 |
try:
|
| 110 |
from ultralytics import YOLO
|
| 111 |
|
|
|
|
| 112 |
if MODEL_IP102_FILE.exists():
|
| 113 |
model = YOLO(str(MODEL_IP102_FILE))
|
| 114 |
HAS_YOLO = True
|
| 115 |
print(f"Success: Loaded IP102 YOLO model from {MODEL_IP102_FILE} ({len(model.names)} classes)")
|
| 116 |
return
|
| 117 |
|
|
|
|
| 118 |
loaded_fallback = False
|
| 119 |
if MODEL_FILE.exists():
|
| 120 |
model = YOLO(str(MODEL_FILE))
|
|
|
|
| 129 |
print(f"Success: Loaded fallback YOLO model from {fallback_path} ({len(model.names)} classes)")
|
| 130 |
loaded_fallback = True
|
| 131 |
|
| 132 |
+
print("IP102 model is missing. Initiating background download...")
|
|
|
|
| 133 |
threading.Thread(target=download_ip102_model, daemon=True).start()
|
| 134 |
except Exception as e:
|
| 135 |
if not HAS_YOLO:
|
| 136 |
HAS_YOLO = False
|
| 137 |
print(f"Warning: Failed to load YOLO model: {e}. Using dummy detection.")
|
| 138 |
else:
|
| 139 |
+
print(f"Warning: Fallback model loaded, but error occurred: {e}")
|
| 140 |
|
| 141 |
init_model()
|
| 142 |
|
|
|
|
| 161 |
elif 73 <= cls_id <= 91:
|
| 162 |
return "Citrus Pest", "Citrus Pest: Prune infested shoots, encourage beneficial predators (e.g., predatory mites or wasps), use horticultural oil sprays, and apply targeted systemic treatments if necessary."
|
| 163 |
elif 92 <= cls_id <= 101:
|
| 164 |
+
return "Mango Pest", "Mango Pest: Maintain orchard sanitation, prune dense branches to improve sunlight penetration, use sticky bands on tree trunks, and apply specific crop protection sprays during flowering."
|
| 165 |
return "Unknown Pest", "Consult with a local agricultural officer for specific treatment advice."
|
| 166 |
|
| 167 |
PEST_TREATMENT = {
|
|
|
|
| 187 |
"default": "Consult with a local agricultural officer for specific treatment advice.",
|
| 188 |
}
|
| 189 |
|
| 190 |
+
ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg"}
|
| 191 |
+
|
| 192 |
def allowed_file(filename):
|
| 193 |
return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
|
| 194 |
|
|
|
|
| 200 |
return f"Pest Detection API is running! YOLO Model: {model_info}"
|
| 201 |
|
| 202 |
@app.get("/health")
|
| 203 |
+
def health(request: Request):
|
|
|
|
| 204 |
classes_count = 0
|
| 205 |
+
model_type = "None"
|
| 206 |
if HAS_YOLO and model:
|
| 207 |
classes_count = len(model.names)
|
| 208 |
model_type = "IP102 (102 pests)" if classes_count == 102 else "Cereal Pests (10 pests)"
|
| 209 |
+
|
| 210 |
return {
|
| 211 |
"status": "ok",
|
| 212 |
"yolo": HAS_YOLO,
|
|
|
|
| 216 |
}
|
| 217 |
|
| 218 |
@app.post("/predict")
|
| 219 |
+
async def predict(
|
| 220 |
+
request: Request,
|
| 221 |
+
image: UploadFile = File(...)
|
| 222 |
+
):
|
| 223 |
+
ip = request.client.host if request.client else "unknown"
|
| 224 |
+
|
| 225 |
if not image.filename:
|
| 226 |
+
log_audit("anonymous", "/predict", "failed", "No selected file", ip)
|
| 227 |
raise HTTPException(status_code=400, detail="No selected file")
|
| 228 |
|
| 229 |
if not allowed_file(image.filename):
|
| 230 |
+
log_audit("anonymous", "/predict", "failed", f"File type not allowed: {image.filename}", ip)
|
| 231 |
raise HTTPException(status_code=400, detail="File type not allowed")
|
| 232 |
|
|
|
|
|
|
|
|
|
|
| 233 |
try:
|
| 234 |
+
contents = await image.read()
|
| 235 |
+
|
| 236 |
+
if len(contents) > 5 * 1024 * 1024:
|
| 237 |
+
log_audit("anonymous", "/predict", "failed", "File too large (exceeded 5MB)", ip)
|
| 238 |
+
raise HTTPException(status_code=413, detail="File size exceeds the 5MB limit.")
|
| 239 |
+
|
| 240 |
+
img = Image.open(io.BytesIO(contents)).convert("RGB")
|
| 241 |
+
except HTTPException as he:
|
| 242 |
+
raise he
|
| 243 |
except Exception as e:
|
| 244 |
+
log_audit("anonymous", "/predict", "error", f"Image parsing error: {str(e)}", ip)
|
| 245 |
+
raise HTTPException(status_code=500, detail=f"Failed to process image: {str(e)}")
|
| 246 |
|
| 247 |
if HAS_YOLO:
|
| 248 |
try:
|
| 249 |
+
results = model.predict(source=img, save=False, conf=0.4)
|
| 250 |
|
| 251 |
detections = []
|
| 252 |
for r in results:
|
|
|
|
| 269 |
"confidence": round(conf, 2),
|
| 270 |
"treatment": treatment,
|
| 271 |
})
|
|
|
|
|
|
|
| 272 |
|
| 273 |
if not detections:
|
| 274 |
+
log_details = json.dumps({"pest": "None", "confidence": 0.0, "filename": image.filename})
|
| 275 |
+
log_audit("anonymous", "/predict", "success", log_details, ip)
|
| 276 |
return {
|
| 277 |
"status": "success",
|
| 278 |
"pest_detected": "None",
|
|
|
|
| 281 |
"message": "No pests detected",
|
| 282 |
}
|
| 283 |
|
| 284 |
+
log_details = json.dumps({
|
| 285 |
+
"pest": detections[0]["pest_detected"],
|
| 286 |
+
"confidence": detections[0]["confidence"],
|
| 287 |
+
"filename": image.filename
|
| 288 |
+
})
|
| 289 |
+
log_audit("anonymous", "/predict", "success", log_details, ip)
|
| 290 |
+
|
| 291 |
return {
|
| 292 |
"status": "success",
|
| 293 |
"pest_detected": detections[0]["pest_detected"],
|
|
|
|
| 297 |
"all_detections": detections,
|
| 298 |
}
|
| 299 |
except Exception as e:
|
| 300 |
+
log_audit("anonymous", "/predict", "error", f"YOLO Inference error: {str(e)}", ip)
|
| 301 |
raise HTTPException(status_code=500, detail=f"Inference error: {str(e)}")
|
| 302 |
else:
|
| 303 |
+
log_details = json.dumps({"pest": "Aphids (Mock)", "confidence": 0.95, "filename": image.filename})
|
| 304 |
+
log_audit("anonymous", "/predict", "success", log_details, ip)
|
| 305 |
return {
|
| 306 |
"status": "success",
|
| 307 |
"pest_detected": "Aphids (Mock)",
|
|
|
|
| 312 |
|
| 313 |
if __name__ == "__main__":
|
| 314 |
import uvicorn
|
|
|
|
| 315 |
port = int(os.environ.get("PORT", 5000))
|
| 316 |
uvicorn.run(app, host="0.0.0.0", port=port)
|
gradle.properties
CHANGED
|
@@ -20,4 +20,7 @@ kotlin.code.style=official
|
|
| 20 |
# Enables namespacing of each library's R class so that its R class includes only the
|
| 21 |
# resources declared in the library itself and none from the library's dependencies,
|
| 22 |
# thereby reducing the size of the R class for that library
|
| 23 |
-
android.nonTransitiveRClass=true
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
# Enables namespacing of each library's R class so that its R class includes only the
|
| 21 |
# resources declared in the library itself and none from the library's dependencies,
|
| 22 |
# thereby reducing the size of the R class for that library
|
| 23 |
+
android.nonTransitiveRClass=true
|
| 24 |
+
|
| 25 |
+
# Java Home configuration for Gradle build
|
| 26 |
+
org.gradle.java.home=C:\\Program Files\\Android\\Android Studio\\jbr
|
gradlew.bat
CHANGED
|
@@ -39,6 +39,14 @@ for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
|
| 39 |
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
| 40 |
|
| 41 |
@rem Find java.exe
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
if defined JAVA_HOME goto findJavaFromJavaHome
|
| 43 |
|
| 44 |
set JAVA_EXE=java.exe
|
|
|
|
| 39 |
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
| 40 |
|
| 41 |
@rem Find java.exe
|
| 42 |
+
if not defined JAVA_HOME (
|
| 43 |
+
if exist "C:\Program Files\Android\Android Studio\jbr" (
|
| 44 |
+
set "JAVA_HOME=C:\Program Files\Android\Android Studio\jbr"
|
| 45 |
+
) else if exist "C:\Program Files\Android\Android Studio\jre" (
|
| 46 |
+
set "JAVA_HOME=C:\Program Files\Android\Android Studio\jre"
|
| 47 |
+
)
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
if defined JAVA_HOME goto findJavaFromJavaHome
|
| 51 |
|
| 52 |
set JAVA_EXE=java.exe
|