Spaces:
Sleeping
Sleeping
Backend restructuring and YOLOv8 removal
Browse files- .gitattributes +1 -0
- .gitignore +32 -51
- Dockerfile → BACKEND/Dockerfile +9 -18
- api.py → BACKEND/api.py +80 -37
- app.py → BACKEND/app.py +36 -40
- config.py → BACKEND/config.py +55 -0
- {inference → BACKEND/inference}/__init__.py +0 -0
- {inference → BACKEND/inference}/predict.py +22 -0
- {knowledge_base → BACKEND/knowledge_base}/__init__.py +0 -0
- {knowledge_base → BACKEND/knowledge_base}/diseases.json +0 -0
- {models → BACKEND/models}/__init__.py +0 -0
- BACKEND/models/__pycache__/__init__.cpython-310.pyc +0 -0
- BACKEND/models/__pycache__/__init__.cpython-312.pyc +0 -0
- BACKEND/models/__pycache__/model.cpython-310.pyc +0 -0
- BACKEND/models/__pycache__/model.cpython-312.pyc +0 -0
- BACKEND/models/checkpoints/best_swin_model.pth +3 -0
- {models → BACKEND/models}/class_mapping.json +0 -0
- {models → BACKEND/models}/evaluate.py +18 -43
- {models → BACKEND/models}/model.py +22 -10
- {models → BACKEND/models}/training_history.json +0 -0
- pipeline.py → BACKEND/pipeline.py +111 -40
- preprocessing.py → BACKEND/preprocessing.py +17 -0
- requirements.txt → BACKEND/requirements.txt +0 -2
- {training → BACKEND/training}/__init__.py +0 -0
- BACKEND/training/find_corrupt_images.py +41 -0
- BACKEND/training/find_corrupt_images_fast.py +43 -0
- {training → BACKEND/training}/train.py +82 -75
- {utils → BACKEND/utils}/__init__.py +0 -0
- BACKEND/utils/__pycache__/__init__.cpython-310.pyc +0 -0
- BACKEND/utils/__pycache__/__init__.cpython-312.pyc +0 -0
- BACKEND/utils/__pycache__/dataset.cpython-310.pyc +0 -0
- BACKEND/utils/__pycache__/dataset.cpython-312.pyc +0 -0
- BACKEND/utils/analyze_dataset.py +50 -0
- {utils → BACKEND/utils}/dataset.py +29 -0
- {utils → BACKEND/utils}/download_data.py +0 -0
- BACKEND/utils/fix_folders.py +58 -0
- README.md +56 -83
.gitattributes
CHANGED
|
@@ -1 +1,2 @@
|
|
| 1 |
BACKEND/models/checkpoints/*.pth filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 1 |
BACKEND/models/checkpoints/*.pth filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
BACKEND/models/*.pth filter=lfs diff=lfs merge=lfs -text
|
.gitignore
CHANGED
|
@@ -19,62 +19,47 @@ Thumbs.db
|
|
| 19 |
*.sublime-project
|
| 20 |
*.sublime-workspace
|
| 21 |
|
| 22 |
-
# ---
|
| 23 |
-
__pycache__/
|
| 24 |
-
*.py[cod]
|
| 25 |
-
*$py.class
|
| 26 |
-
*.so
|
| 27 |
-
.Python
|
| 28 |
-
build/
|
| 29 |
-
develop-eggs/
|
| 30 |
-
dist/
|
| 31 |
-
downloads/
|
| 32 |
-
eggs/
|
| 33 |
-
.eggs/
|
| 34 |
-
lib/
|
| 35 |
-
lib64/
|
| 36 |
-
parts/
|
| 37 |
-
sdist/
|
| 38 |
-
var/
|
| 39 |
-
wheels/
|
| 40 |
-
share/python-wheels/
|
| 41 |
-
*.egg-info/
|
| 42 |
-
.installed.cfg
|
| 43 |
-
*.egg
|
| 44 |
-
MANIFEST
|
| 45 |
|
| 46 |
# Virtual Environments
|
| 47 |
-
.venv/
|
| 48 |
-
venv/
|
| 49 |
-
ENV/
|
| 50 |
-
env/
|
| 51 |
-
env.bak/
|
| 52 |
-
venv.bak/
|
| 53 |
|
| 54 |
# Jupyter Notebook
|
| 55 |
-
.ipynb_checkpoints
|
| 56 |
|
| 57 |
# --- Models & Data ---
|
| 58 |
-
#
|
| 59 |
-
# *.pt
|
| 60 |
-
# *.pth
|
| 61 |
-
# *.ckpt
|
| 62 |
-
# *.h5
|
| 63 |
-
# *.onnx
|
| 64 |
-
# *.weights
|
| 65 |
-
# *.pb
|
| 66 |
-
|
| 67 |
-
# Model directories (Keep checkpoints for deployment if needed)
|
| 68 |
-
# BACKEND/models/checkpoints/
|
| 69 |
BACKEND/models/logs/
|
| 70 |
BACKEND/results/
|
| 71 |
BACKEND/data/
|
| 72 |
BACKEND/logs/
|
| 73 |
-
results/
|
| 74 |
-
data/
|
| 75 |
-
logs/
|
| 76 |
|
| 77 |
-
# Exception: Keep configuration
|
| 78 |
!BACKEND/models/*.json
|
| 79 |
!BACKEND/models/*.py
|
| 80 |
!BACKEND/models/__init__.py
|
|
@@ -100,11 +85,7 @@ FRONTEND/pnpm-debug.log*
|
|
| 100 |
.env.test.local
|
| 101 |
.env.production.local
|
| 102 |
*.env*
|
| 103 |
-
kaggle.json
|
| 104 |
-
|
| 105 |
-
# --- Vercel ---
|
| 106 |
-
.vercel
|
| 107 |
|
| 108 |
# --- Misc ---
|
| 109 |
-
download_test.py
|
| 110 |
-
|
|
|
|
| 19 |
*.sublime-project
|
| 20 |
*.sublime-workspace
|
| 21 |
|
| 22 |
+
# --- Backend (BACKEND/) ---
|
| 23 |
+
BACKEND/__pycache__/
|
| 24 |
+
BACKEND/*.py[cod]
|
| 25 |
+
BACKEND/*$py.class
|
| 26 |
+
BACKEND/*.so
|
| 27 |
+
BACKEND/.Python
|
| 28 |
+
BACKEND/build/
|
| 29 |
+
BACKEND/develop-eggs/
|
| 30 |
+
BACKEND/dist/
|
| 31 |
+
BACKEND/downloads/
|
| 32 |
+
BACKEND/eggs/
|
| 33 |
+
BACKEND/.eggs/
|
| 34 |
+
BACKEND/lib/
|
| 35 |
+
BACKEND/lib64/
|
| 36 |
+
BACKEND/parts/
|
| 37 |
+
BACKEND/sdist/
|
| 38 |
+
BACKEND/var/
|
| 39 |
+
BACKEND/wheels/
|
| 40 |
+
BACKEND/share/python-wheels/
|
| 41 |
+
BACKEND/*.egg-info/
|
| 42 |
+
BACKEND/.installed.cfg
|
| 43 |
+
BACKEND/*.egg
|
| 44 |
+
BACKEND/MANIFEST
|
| 45 |
|
| 46 |
# Virtual Environments
|
| 47 |
+
BACKEND/.venv/
|
| 48 |
+
BACKEND/venv/
|
| 49 |
+
BACKEND/ENV/
|
| 50 |
+
BACKEND/env/
|
|
|
|
|
|
|
| 51 |
|
| 52 |
# Jupyter Notebook
|
| 53 |
+
BACKEND/.ipynb_checkpoints/
|
| 54 |
|
| 55 |
# --- Models & Data ---
|
| 56 |
+
# Model logs and temporary results
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
BACKEND/models/logs/
|
| 58 |
BACKEND/results/
|
| 59 |
BACKEND/data/
|
| 60 |
BACKEND/logs/
|
|
|
|
|
|
|
|
|
|
| 61 |
|
| 62 |
+
# Exception: Keep configuration and mapping files
|
| 63 |
!BACKEND/models/*.json
|
| 64 |
!BACKEND/models/*.py
|
| 65 |
!BACKEND/models/__init__.py
|
|
|
|
| 85 |
.env.test.local
|
| 86 |
.env.production.local
|
| 87 |
*.env*
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
|
| 89 |
# --- Misc ---
|
| 90 |
+
BACKEND/download_test.py
|
| 91 |
+
kaggle.json
|
Dockerfile → BACKEND/Dockerfile
RENAMED
|
@@ -1,34 +1,25 @@
|
|
| 1 |
-
# Python 3.10 tabanlı hafif bir imaj kullanıyoruz
|
| 2 |
FROM python:3.10-slim
|
| 3 |
|
| 4 |
# Çalışma dizinini ayarla
|
| 5 |
WORKDIR /app
|
| 6 |
|
| 7 |
-
#
|
| 8 |
-
RUN apt-get update && apt-get install -y \
|
| 9 |
-
libgl1 \
|
| 10 |
libglib2.0-0 \
|
| 11 |
-
libsm6 \
|
| 12 |
-
libxext6 \
|
| 13 |
&& rm -rf /var/lib/apt/lists/*
|
| 14 |
|
| 15 |
-
|
| 16 |
-
# Bağımlılıkları kopyala ve yükle
|
| 17 |
COPY requirements.txt .
|
| 18 |
-
RUN pip install --no-cache-dir -
|
|
|
|
| 19 |
RUN pip install --no-cache-dir uvicorn gunicorn
|
| 20 |
|
| 21 |
-
# Proje
|
| 22 |
COPY . .
|
| 23 |
|
| 24 |
-
#
|
| 25 |
-
RUN mkdir -p data models/checkpoints results
|
| 26 |
-
|
| 27 |
-
# API portunu aç
|
| 28 |
EXPOSE 7860
|
| 29 |
|
| 30 |
-
# API'yi başlat
|
| 31 |
-
# Gradio kullanacaksanız: python app.py
|
| 32 |
-
# FastAPI kullanacaksanız: uvicorn api:app --host 0.0.0.0 --port 7860
|
| 33 |
CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "7860"]
|
| 34 |
-
# CMD ["python", "app.py"]
|
|
|
|
|
|
|
| 1 |
FROM python:3.10-slim
|
| 2 |
|
| 3 |
# Çalışma dizinini ayarla
|
| 4 |
WORKDIR /app
|
| 5 |
|
| 6 |
+
# Gerekli sistem kütüphanelerini kur (OpenCV / GLIB gereksinimleri için vb.)
|
| 7 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 8 |
+
libgl1-mesa-glx \
|
| 9 |
libglib2.0-0 \
|
|
|
|
|
|
|
| 10 |
&& rm -rf /var/lib/apt/lists/*
|
| 11 |
|
| 12 |
+
# Bağımlılıkları kopyala ve kur
|
|
|
|
| 13 |
COPY requirements.txt .
|
| 14 |
+
RUN pip install --no-cache-dir --upgrade pip && \
|
| 15 |
+
pip install --no-cache-dir -r requirements.txt
|
| 16 |
RUN pip install --no-cache-dir uvicorn gunicorn
|
| 17 |
|
| 18 |
+
# Proje kaynak kodlarını kopyala
|
| 19 |
COPY . .
|
| 20 |
|
| 21 |
+
# FastAPI portunu dışa aç (Hugging Face Spaces için 7860)
|
|
|
|
|
|
|
|
|
|
| 22 |
EXPOSE 7860
|
| 23 |
|
| 24 |
+
# Uvicorn ile API'yi başlat
|
|
|
|
|
|
|
| 25 |
CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "7860"]
|
|
|
api.py → BACKEND/api.py
RENAMED
|
@@ -1,3 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import sys
|
| 2 |
import time
|
| 3 |
import json
|
|
@@ -8,7 +18,7 @@ from contextlib import asynccontextmanager
|
|
| 8 |
from fastapi import FastAPI, File, UploadFile, HTTPException, Query
|
| 9 |
from fastapi.responses import JSONResponse
|
| 10 |
from fastapi.middleware.cors import CORSMiddleware
|
| 11 |
-
from pydantic import BaseModel
|
| 12 |
|
| 13 |
project_root = Path(__file__).resolve().parent
|
| 14 |
if str(project_root) not in sys.path:
|
|
@@ -19,16 +29,19 @@ from pipeline import WheatDiseasePipeline, PipelineResult
|
|
| 19 |
|
| 20 |
|
| 21 |
# ============================================================================
|
| 22 |
-
#
|
| 23 |
# ============================================================================
|
| 24 |
|
|
|
|
| 25 |
pipeline: Optional[WheatDiseasePipeline] = None
|
| 26 |
startup_error: Optional[str] = None
|
| 27 |
|
|
|
|
| 28 |
@asynccontextmanager
|
| 29 |
async def lifespan(app: FastAPI):
|
|
|
|
| 30 |
global pipeline, startup_error
|
| 31 |
-
print("Pipeline
|
| 32 |
try:
|
| 33 |
pipeline = WheatDiseasePipeline(
|
| 34 |
cls_checkpoint = str(config.MODEL_CHECKPOINT_PATH),
|
|
@@ -36,20 +49,28 @@ async def lifespan(app: FastAPI):
|
|
| 36 |
device = str(config.DEVICE),
|
| 37 |
cls_conf = config.CONFIDENCE_THRESHOLD,
|
| 38 |
)
|
| 39 |
-
print("Pipeline
|
| 40 |
except Exception as e:
|
| 41 |
startup_error = str(e)
|
| 42 |
-
print(f"Pipeline
|
| 43 |
yield
|
| 44 |
-
print("API
|
|
|
|
| 45 |
|
| 46 |
app = FastAPI(
|
| 47 |
-
title = "Wheat Disease
|
| 48 |
-
description =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
version = "1.0.0",
|
| 50 |
lifespan = lifespan,
|
| 51 |
)
|
| 52 |
|
|
|
|
| 53 |
app.add_middleware(
|
| 54 |
CORSMiddleware,
|
| 55 |
allow_origins = ["*"],
|
|
@@ -60,7 +81,7 @@ app.add_middleware(
|
|
| 60 |
|
| 61 |
|
| 62 |
# ============================================================================
|
| 63 |
-
#
|
| 64 |
# ============================================================================
|
| 65 |
|
| 66 |
class ClassificationResult(BaseModel):
|
|
@@ -92,44 +113,55 @@ class HealthResponse(BaseModel):
|
|
| 92 |
num_classes : int
|
| 93 |
error : Optional[str] = None
|
| 94 |
|
|
|
|
| 95 |
# ============================================================================
|
| 96 |
-
#
|
| 97 |
# ============================================================================
|
| 98 |
|
| 99 |
def _check_pipeline():
|
|
|
|
| 100 |
if pipeline is None:
|
| 101 |
raise HTTPException(
|
| 102 |
status_code=503,
|
| 103 |
detail={
|
| 104 |
-
"error" : "Pipeline
|
| 105 |
"message": startup_error or "Bilinmeyen hata",
|
|
|
|
| 106 |
},
|
| 107 |
)
|
| 108 |
|
|
|
|
| 109 |
def _validate_image(file: UploadFile):
|
| 110 |
-
|
|
|
|
|
|
|
| 111 |
if file.content_type not in allowed:
|
| 112 |
-
raise HTTPException(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
MAX_SIZE = 20 * 1024 * 1024
|
| 114 |
if file.size and file.size > MAX_SIZE:
|
| 115 |
-
raise HTTPException(
|
|
|
|
|
|
|
|
|
|
| 116 |
|
| 117 |
|
| 118 |
# ============================================================================
|
| 119 |
# ENDPOINTS
|
| 120 |
# ============================================================================
|
| 121 |
|
| 122 |
-
@app.get(
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
"endpoints": ["/analyze", "/health", "/classes"]
|
| 129 |
-
}
|
| 130 |
-
|
| 131 |
-
@app.get("/health", response_model=HealthResponse)
|
| 132 |
async def health():
|
|
|
|
| 133 |
ready = pipeline is not None
|
| 134 |
return HealthResponse(
|
| 135 |
status = "ok" if ready else "degraded",
|
|
@@ -140,8 +172,14 @@ async def health():
|
|
| 140 |
error = startup_error,
|
| 141 |
)
|
| 142 |
|
| 143 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
async def get_classes():
|
|
|
|
| 145 |
mapping_path = config.MODELS_DIR / "class_mapping.json"
|
| 146 |
if mapping_path.exists():
|
| 147 |
with open(mapping_path, "r", encoding="utf-8") as f:
|
|
@@ -155,36 +193,41 @@ async def get_classes():
|
|
| 155 |
"classes" : classes,
|
| 156 |
}
|
| 157 |
|
| 158 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
async def analyze(
|
| 160 |
-
file
|
| 161 |
-
skip_quality: bool = Query(False),
|
| 162 |
):
|
|
|
|
| 163 |
_check_pipeline()
|
| 164 |
_validate_image(file)
|
| 165 |
|
| 166 |
image_bytes = await file.read()
|
| 167 |
if not image_bytes:
|
| 168 |
-
raise HTTPException(status_code=400, detail="
|
| 169 |
|
| 170 |
try:
|
| 171 |
result: PipelineResult = pipeline.run(image_bytes, skip_quality=skip_quality)
|
| 172 |
except Exception as e:
|
| 173 |
-
raise HTTPException(status_code=500, detail=f"
|
| 174 |
|
| 175 |
return JSONResponse(content=pipeline.result_to_dict(result))
|
| 176 |
|
| 177 |
-
@app.post("/classify", response_model=AnalyzeResponse)
|
| 178 |
-
async def classify(
|
| 179 |
-
file : UploadFile = File(...),
|
| 180 |
-
skip_quality: bool = Query(False),
|
| 181 |
-
):
|
| 182 |
-
# Yalnızca sınıflandırma (Swin-T) kullanıldığı için analyze ile aynı
|
| 183 |
-
return await analyze(file, skip_quality)
|
| 184 |
|
|
|
|
|
|
|
|
|
|
| 185 |
|
| 186 |
if __name__ == "__main__":
|
| 187 |
import uvicorn
|
|
|
|
|
|
|
| 188 |
uvicorn.run(
|
| 189 |
"api:app",
|
| 190 |
host = config.API_HOST,
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
API.PY - FastAPI Wheat Disease Analysis API
|
| 3 |
+
|
| 4 |
+
Endpoints:
|
| 5 |
+
POST /analyze -> Classification and quality analysis
|
| 6 |
+
GET /health -> System status
|
| 7 |
+
GET /classes -> Supported classes
|
| 8 |
+
GET /docs -> Swagger UI (automatic)
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
import sys
|
| 12 |
import time
|
| 13 |
import json
|
|
|
|
| 18 |
from fastapi import FastAPI, File, UploadFile, HTTPException, Query
|
| 19 |
from fastapi.responses import JSONResponse
|
| 20 |
from fastapi.middleware.cors import CORSMiddleware
|
| 21 |
+
from pydantic import BaseModel, Field
|
| 22 |
|
| 23 |
project_root = Path(__file__).resolve().parent
|
| 24 |
if str(project_root) not in sys.path:
|
|
|
|
| 29 |
|
| 30 |
|
| 31 |
# ============================================================================
|
| 32 |
+
# APP INITIALIZATION
|
| 33 |
# ============================================================================
|
| 34 |
|
| 35 |
+
# Pipeline object (loaded at startup)
|
| 36 |
pipeline: Optional[WheatDiseasePipeline] = None
|
| 37 |
startup_error: Optional[str] = None
|
| 38 |
|
| 39 |
+
|
| 40 |
@asynccontextmanager
|
| 41 |
async def lifespan(app: FastAPI):
|
| 42 |
+
"""Load pipeline on startup."""
|
| 43 |
global pipeline, startup_error
|
| 44 |
+
print("Pipeline yukleniyor...")
|
| 45 |
try:
|
| 46 |
pipeline = WheatDiseasePipeline(
|
| 47 |
cls_checkpoint = str(config.MODEL_CHECKPOINT_PATH),
|
|
|
|
| 49 |
device = str(config.DEVICE),
|
| 50 |
cls_conf = config.CONFIDENCE_THRESHOLD,
|
| 51 |
)
|
| 52 |
+
print("Pipeline hazir, API isteklere acik.")
|
| 53 |
except Exception as e:
|
| 54 |
startup_error = str(e)
|
| 55 |
+
print(f"Pipeline yuklenemedi: {e}")
|
| 56 |
yield
|
| 57 |
+
print("API kapatiliyor.")
|
| 58 |
+
|
| 59 |
|
| 60 |
app = FastAPI(
|
| 61 |
+
title = "Wheat Disease Detection API",
|
| 62 |
+
description = (
|
| 63 |
+
"Wheat leaf and head disease classification API.\n\n"
|
| 64 |
+
"**Supported 15 Classes:** Aphid, Blast, Black Rust, Brown Rust, "
|
| 65 |
+
"Common Root Rot, Fusarium Head Blight, Healthy, Leaf Blight, "
|
| 66 |
+
"Mildew, Mite, Septoria, Smut, Stem fly, Tan spot, Yellow Rust\n\n"
|
| 67 |
+
"**Model:** Swin Transformer (Tiny)"
|
| 68 |
+
),
|
| 69 |
version = "1.0.0",
|
| 70 |
lifespan = lifespan,
|
| 71 |
)
|
| 72 |
|
| 73 |
+
# CORS
|
| 74 |
app.add_middleware(
|
| 75 |
CORSMiddleware,
|
| 76 |
allow_origins = ["*"],
|
|
|
|
| 81 |
|
| 82 |
|
| 83 |
# ============================================================================
|
| 84 |
+
# SCHEMAS (Pydantic)
|
| 85 |
# ============================================================================
|
| 86 |
|
| 87 |
class ClassificationResult(BaseModel):
|
|
|
|
| 113 |
num_classes : int
|
| 114 |
error : Optional[str] = None
|
| 115 |
|
| 116 |
+
|
| 117 |
# ============================================================================
|
| 118 |
+
# HELPERS
|
| 119 |
# ============================================================================
|
| 120 |
|
| 121 |
def _check_pipeline():
|
| 122 |
+
"""Raise 503 if pipeline is not ready."""
|
| 123 |
if pipeline is None:
|
| 124 |
raise HTTPException(
|
| 125 |
status_code=503,
|
| 126 |
detail={
|
| 127 |
+
"error" : "Pipeline yuklenemedi",
|
| 128 |
"message": startup_error or "Bilinmeyen hata",
|
| 129 |
+
"hint" : "Model dosyalarinin var oldugundan emin olun.",
|
| 130 |
},
|
| 131 |
)
|
| 132 |
|
| 133 |
+
|
| 134 |
def _validate_image(file: UploadFile):
|
| 135 |
+
"""Validate uploaded file is an image."""
|
| 136 |
+
allowed = {"image/jpeg", "image/jpg", "image/png",
|
| 137 |
+
"image/bmp", "image/tiff", "image/webp"}
|
| 138 |
if file.content_type not in allowed:
|
| 139 |
+
raise HTTPException(
|
| 140 |
+
status_code=415,
|
| 141 |
+
detail=f"Desteklenmeyen format: {file.content_type}. "
|
| 142 |
+
f"Kabul edilenler: {', '.join(allowed)}",
|
| 143 |
+
)
|
| 144 |
+
# Size limit: 20MB
|
| 145 |
MAX_SIZE = 20 * 1024 * 1024
|
| 146 |
if file.size and file.size > MAX_SIZE:
|
| 147 |
+
raise HTTPException(
|
| 148 |
+
status_code=413,
|
| 149 |
+
detail=f"Dosya cok buyuk ({file.size/1024/1024:.1f} MB). Maksimum: 20 MB",
|
| 150 |
+
)
|
| 151 |
|
| 152 |
|
| 153 |
# ============================================================================
|
| 154 |
# ENDPOINTS
|
| 155 |
# ============================================================================
|
| 156 |
|
| 157 |
+
@app.get(
|
| 158 |
+
"/health",
|
| 159 |
+
response_model=HealthResponse,
|
| 160 |
+
summary="System Status",
|
| 161 |
+
tags=["System"],
|
| 162 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
async def health():
|
| 164 |
+
"""Returns API and model status."""
|
| 165 |
ready = pipeline is not None
|
| 166 |
return HealthResponse(
|
| 167 |
status = "ok" if ready else "degraded",
|
|
|
|
| 172 |
error = startup_error,
|
| 173 |
)
|
| 174 |
|
| 175 |
+
|
| 176 |
+
@app.get(
|
| 177 |
+
"/classes",
|
| 178 |
+
summary="Supported Disease Classes",
|
| 179 |
+
tags=["Info"],
|
| 180 |
+
)
|
| 181 |
async def get_classes():
|
| 182 |
+
"""Lists 15 disease classes."""
|
| 183 |
mapping_path = config.MODELS_DIR / "class_mapping.json"
|
| 184 |
if mapping_path.exists():
|
| 185 |
with open(mapping_path, "r", encoding="utf-8") as f:
|
|
|
|
| 193 |
"classes" : classes,
|
| 194 |
}
|
| 195 |
|
| 196 |
+
|
| 197 |
+
@app.post(
|
| 198 |
+
"/analyze",
|
| 199 |
+
response_model=AnalyzeResponse,
|
| 200 |
+
summary="Image Analysis",
|
| 201 |
+
tags=["Analysis"],
|
| 202 |
+
)
|
| 203 |
async def analyze(
|
| 204 |
+
file : UploadFile = File(..., description="Wheat image (jpg/png)"),
|
| 205 |
+
skip_quality : bool = Query(False, description="Skip quality filter"),
|
| 206 |
):
|
| 207 |
+
"""Runs analysis on uploaded image."""
|
| 208 |
_check_pipeline()
|
| 209 |
_validate_image(file)
|
| 210 |
|
| 211 |
image_bytes = await file.read()
|
| 212 |
if not image_bytes:
|
| 213 |
+
raise HTTPException(status_code=400, detail="Empty file")
|
| 214 |
|
| 215 |
try:
|
| 216 |
result: PipelineResult = pipeline.run(image_bytes, skip_quality=skip_quality)
|
| 217 |
except Exception as e:
|
| 218 |
+
raise HTTPException(status_code=500, detail=f"Analysis error: {str(e)}")
|
| 219 |
|
| 220 |
return JSONResponse(content=pipeline.result_to_dict(result))
|
| 221 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
|
| 223 |
+
# ============================================================================
|
| 224 |
+
# RUN
|
| 225 |
+
# ============================================================================
|
| 226 |
|
| 227 |
if __name__ == "__main__":
|
| 228 |
import uvicorn
|
| 229 |
+
|
| 230 |
+
print("Wheat Disease API baslatiliyor...")
|
| 231 |
uvicorn.run(
|
| 232 |
"api:app",
|
| 233 |
host = config.API_HOST,
|
app.py → BACKEND/app.py
RENAMED
|
@@ -1,57 +1,52 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import sys
|
| 3 |
import gradio as gr
|
| 4 |
-
|
| 5 |
-
from PIL import Image
|
| 6 |
-
|
| 7 |
-
# Add current directory to path
|
| 8 |
-
project_root = Path(__file__).resolve().parent
|
| 9 |
-
if str(project_root) not in sys.path:
|
| 10 |
-
sys.path.append(str(project_root))
|
| 11 |
-
|
| 12 |
from pipeline import WheatDiseasePipeline
|
| 13 |
import config
|
|
|
|
| 14 |
|
| 15 |
-
#
|
| 16 |
pipeline = WheatDiseasePipeline(
|
| 17 |
cls_checkpoint = str(config.MODEL_CHECKPOINT_PATH),
|
| 18 |
cls_mapping = str(config.MODELS_DIR / "class_mapping.json"),
|
| 19 |
-
device =
|
| 20 |
-
cls_conf =
|
| 21 |
)
|
| 22 |
|
| 23 |
def predict(image):
|
| 24 |
if image is None:
|
| 25 |
-
return "
|
| 26 |
|
| 27 |
-
#
|
| 28 |
-
|
| 29 |
-
|
|
|
|
| 30 |
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
if quality["warnings"]:
|
| 41 |
-
quality_text += f"Uyarılar: {', '.join(quality['warnings'])}"
|
| 42 |
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
-
#
|
| 46 |
-
with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
| 47 |
-
gr.Markdown("#
|
| 48 |
-
gr.Markdown("
|
| 49 |
|
| 50 |
with gr.Row():
|
| 51 |
with gr.Column():
|
| 52 |
-
input_img = gr.Image(
|
| 53 |
-
btn = gr.Button("Teşhis Et", variant="primary")
|
| 54 |
-
|
| 55 |
with gr.Column():
|
| 56 |
output_label = gr.Textbox(label="En Olası Teşhis")
|
| 57 |
output_probs = gr.Label(label="Sınıf Olasılıkları", num_top_classes=3)
|
|
@@ -59,10 +54,11 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
|
| 59 |
|
| 60 |
btn.click(fn=predict, inputs=input_img, outputs=[output_label, output_probs, output_quality])
|
| 61 |
|
| 62 |
-
gr.
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
|
|
|
| 66 |
|
| 67 |
if __name__ == "__main__":
|
| 68 |
demo.launch(server_name="0.0.0.0", server_port=7860)
|
|
|
|
|
|
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
+
import numpy as np
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
from pipeline import WheatDiseasePipeline
|
| 4 |
import config
|
| 5 |
+
from pathlib import Path
|
| 6 |
|
| 7 |
+
# Pipeline başlatma
|
| 8 |
pipeline = WheatDiseasePipeline(
|
| 9 |
cls_checkpoint = str(config.MODEL_CHECKPOINT_PATH),
|
| 10 |
cls_mapping = str(config.MODELS_DIR / "class_mapping.json"),
|
| 11 |
+
device = str(config.DEVICE),
|
| 12 |
+
cls_conf = config.CONFIDENCE_THRESHOLD,
|
| 13 |
)
|
| 14 |
|
| 15 |
def predict(image):
|
| 16 |
if image is None:
|
| 17 |
+
return "Görüntü yüklenmedi", {}, "N/A"
|
| 18 |
|
| 19 |
+
# Görüntü Gradio'dan RGB numpy array olarak gelir,
|
| 20 |
+
# pipeline ise bytes veya PIL bekler (veya numpy BGR).
|
| 21 |
+
# Biz bytes'a çevirip gönderelim ya da doğrudan numpy olarak işleyelim.
|
| 22 |
+
result = pipeline.run(image, skip_quality=False)
|
| 23 |
|
| 24 |
+
if result.predicted_class == "Reddedildi":
|
| 25 |
+
return (
|
| 26 |
+
f"❌ Analiz Reddedildi: {result.rejection_reason}",
|
| 27 |
+
{},
|
| 28 |
+
f"Bulanıklık: {result.blur_score:.1f} (Düşük kalite)"
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
# Olasılıkları Gradio Label formatına çevir
|
| 32 |
+
probs = {c: float(s) for c, s in result.top3_predictions}
|
|
|
|
|
|
|
| 33 |
|
| 34 |
+
quality_info = f"Görüntü Kalite Skoru: {result.blur_score:.1f}"
|
| 35 |
+
if result.quality_warnings:
|
| 36 |
+
quality_info += f"\nUyarılar: {', '.join(result.quality_warnings)}"
|
| 37 |
+
|
| 38 |
+
return result.predicted_class, probs, quality_info
|
| 39 |
|
| 40 |
+
# Gradio Arayüzü
|
| 41 |
+
with gr.Blocks(theme=gr.themes.Soft(), title="Bugday Hastaligi Teshis Sistemi") as demo:
|
| 42 |
+
gr.Markdown("# Bugday Hastaligi Teshis Sistemi")
|
| 43 |
+
gr.Markdown("Swin Transformer tabanli derin ogrenme modeli ile 15 farkli bugday hastaligini teshis edin.")
|
| 44 |
|
| 45 |
with gr.Row():
|
| 46 |
with gr.Column():
|
| 47 |
+
input_img = gr.Image(label="Buğday Yaprağı Fotoğrafı Yükleyin", type="numpy")
|
| 48 |
+
btn = gr.Button("🔍 Teşhis Et", variant="primary")
|
| 49 |
+
|
| 50 |
with gr.Column():
|
| 51 |
output_label = gr.Textbox(label="En Olası Teşhis")
|
| 52 |
output_probs = gr.Label(label="Sınıf Olasılıkları", num_top_classes=3)
|
|
|
|
| 54 |
|
| 55 |
btn.click(fn=predict, inputs=input_img, outputs=[output_label, output_probs, output_quality])
|
| 56 |
|
| 57 |
+
gr.Markdown("""
|
| 58 |
+
### ℹ️ Desteklenen Sınıflar
|
| 59 |
+
Aphid, Blast, Black Rust, Brown Rust, Common Root Rot, Fusarium Head Blight, Healthy,
|
| 60 |
+
Leaf Blight, Mildew, Mite, Septoria, Smut, Stem fly, Tan spot, Yellow Rust
|
| 61 |
+
""")
|
| 62 |
|
| 63 |
if __name__ == "__main__":
|
| 64 |
demo.launch(server_name="0.0.0.0", server_port=7860)
|
config.py → BACKEND/config.py
RENAMED
|
@@ -3,8 +3,12 @@ import torch
|
|
| 3 |
from pathlib import Path
|
| 4 |
import logging
|
| 5 |
|
|
|
|
|
|
|
| 6 |
# Veri Seti: 15 Sınıf | Train: 13104 | Valid: 300 | Test: 750
|
|
|
|
| 7 |
|
|
|
|
| 8 |
BASE_DIR = Path(__file__).resolve().parent
|
| 9 |
DATA_DIR = BASE_DIR / "data"
|
| 10 |
MODELS_DIR = BASE_DIR / "models"
|
|
@@ -13,7 +17,9 @@ CHECKPOINTS_DIR = MODELS_DIR / "checkpoints"
|
|
| 13 |
KNOWLEDGE_BASE_DIR = BASE_DIR / "knowledge_base"
|
| 14 |
RESULTS_DIR = BASE_DIR / "results"
|
| 15 |
|
|
|
|
| 16 |
# VERİ SETİ SINIF BİLGİLERİ
|
|
|
|
| 17 |
|
| 18 |
# Veri setindeki tüm sınıflar (train klasöründeki sırayla - alfabetik)
|
| 19 |
DATASET_CLASSES = [
|
|
@@ -45,7 +51,11 @@ TOTAL_TRAIN = 13104
|
|
| 45 |
TOTAL_VALID = 300
|
| 46 |
TOTAL_TEST = 750
|
| 47 |
|
|
|
|
|
|
|
|
|
|
| 48 |
|
|
|
|
| 49 |
# Colab T4/A100 için 32 güvenli; OOM yaşarsanız 16'ya düşürün
|
| 50 |
BATCH_SIZE = 8
|
| 51 |
# Colab'da 2 en kararlı değerdir
|
|
@@ -62,18 +72,24 @@ LR_DIVISOR = 5
|
|
| 62 |
# Scheduler minimum LR
|
| 63 |
MIN_LEARNING_RATE = 1e-07
|
| 64 |
|
|
|
|
| 65 |
MODEL_NAME = "swin_t" # Swin Transformer Tiny
|
| 66 |
IMG_SIZE = 224
|
| 67 |
PRETRAINED = True # ImageNet pre-trained weights
|
| 68 |
|
|
|
|
| 69 |
LABEL_SMOOTHING = 0.1 # Overconfidence engellemek için
|
| 70 |
WEIGHT_DECAY = 0.05 # L2 regularization
|
| 71 |
GRADIENT_CLIP_MAX_NORM = 0.5 # Transformer'larda gradient explosion önleme
|
| 72 |
|
|
|
|
| 73 |
# Colab GPU'da ~2x hız artışı, bellek tasarrufu sağlar
|
| 74 |
USE_MIXED_PRECISION = False
|
| 75 |
SCALER_INIT_SCALE = 65536.0
|
| 76 |
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
# Aşama 1 (Epoch 1-4): Backbone dondurulur, sadece head eğitilir
|
| 79 |
# Aşama 2 (Epoch 5+): Backbone açılır, differential LR uygulanır
|
|
@@ -81,21 +97,33 @@ FREEZE_BACKBONE_INITIALLY = True
|
|
| 81 |
UNFREEZE_EPOCH = 5
|
| 82 |
DIFFERENTIAL_LR = True
|
| 83 |
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
SAVE_BEST_MODEL = True
|
| 86 |
SAVE_CHECKPOINT_INTERVAL = 5 # Her 5 epoch'ta checkpoint
|
| 87 |
MODEL_CHECKPOINT_PATH = CHECKPOINTS_DIR / "best_swin_model.pth"
|
| 88 |
FINAL_MODEL_PATH = MODELS_DIR / "final_swin_model.pth"
|
| 89 |
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
USE_EARLY_STOPPING = True
|
| 92 |
EARLY_STOPPING_PATIENCE = 15 # 12 epoch iyileşme olmazsa dur
|
| 93 |
EARLY_STOPPING_DELTA = 0.001 # Minimum iyileşme eşiği
|
| 94 |
|
|
|
|
|
|
|
|
|
|
| 95 |
|
| 96 |
SEED = 42
|
| 97 |
DETERMINISTIC = True
|
| 98 |
|
|
|
|
|
|
|
|
|
|
| 99 |
|
| 100 |
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 101 |
CUDA_AVAILABLE = torch.cuda.is_available()
|
|
@@ -111,6 +139,9 @@ else:
|
|
| 111 |
GPU_NAME = "CPU"
|
| 112 |
GPU_MEMORY = 0
|
| 113 |
|
|
|
|
|
|
|
|
|
|
| 114 |
|
| 115 |
API_HOST = "0.0.0.0"
|
| 116 |
API_PORT = 8000
|
|
@@ -118,22 +149,34 @@ API_DEBUG = False
|
|
| 118 |
INFERENCE_TIMEOUT = 30
|
| 119 |
CONFIDENCE_THRESHOLD = 0.5 # Bu altında "Belirsiz" döndür
|
| 120 |
|
|
|
|
|
|
|
|
|
|
| 121 |
|
| 122 |
LOG_LEVEL = logging.INFO
|
| 123 |
LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
| 124 |
CONSOLE_LOG = True
|
| 125 |
FILE_LOG = True
|
| 126 |
|
|
|
|
|
|
|
|
|
|
| 127 |
|
| 128 |
CALCULATE_CLASS_WEIGHTS = True
|
| 129 |
VALIDATION_INTERVAL = 1 # Her epoch validate et
|
| 130 |
PRINT_INTERVAL = 20 # Her 20 batch'te log
|
| 131 |
|
|
|
|
|
|
|
|
|
|
| 132 |
|
| 133 |
SCHEDULER_TYPE = "cosine"
|
| 134 |
COSINE_T_MAX = EPOCHS
|
| 135 |
COSINE_ETA_MIN = MIN_LEARNING_RATE
|
| 136 |
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
TRACK_METRICS = [
|
| 139 |
"train_loss",
|
|
@@ -152,6 +195,9 @@ RESULTS_JSON = RESULTS_DIR / "final_results.json"
|
|
| 152 |
CLASS_TO_IDX = {}
|
| 153 |
IDX_TO_CLASS = {}
|
| 154 |
|
|
|
|
|
|
|
|
|
|
| 155 |
|
| 156 |
def create_directories():
|
| 157 |
dirs = [DATA_DIR, MODELS_DIR, LOGS_DIR,
|
|
@@ -163,6 +209,9 @@ def create_directories():
|
|
| 163 |
|
| 164 |
create_directories()
|
| 165 |
|
|
|
|
|
|
|
|
|
|
| 166 |
|
| 167 |
def validate_config():
|
| 168 |
warnings = []
|
|
@@ -187,6 +236,9 @@ def validate_config():
|
|
| 187 |
|
| 188 |
config_warnings = validate_config()
|
| 189 |
|
|
|
|
|
|
|
|
|
|
| 190 |
|
| 191 |
def print_config():
|
| 192 |
print("\n" + "=" * 80)
|
|
@@ -246,6 +298,9 @@ def print_config():
|
|
| 246 |
print("\n" + "=" * 80 + "\n")
|
| 247 |
|
| 248 |
|
|
|
|
|
|
|
|
|
|
| 249 |
|
| 250 |
def get_device_info():
|
| 251 |
return {
|
|
|
|
| 3 |
from pathlib import Path
|
| 4 |
import logging
|
| 5 |
|
| 6 |
+
# ============================================================================
|
| 7 |
+
# 📋 CONFIG.PY - SWIN TRANSFORMER WHEAT DISEASE CLASSIFIER
|
| 8 |
# Veri Seti: 15 Sınıf | Train: 13104 | Valid: 300 | Test: 750
|
| 9 |
+
# ============================================================================
|
| 10 |
|
| 11 |
+
# --- Dizin Ayarlaması ---
|
| 12 |
BASE_DIR = Path(__file__).resolve().parent
|
| 13 |
DATA_DIR = BASE_DIR / "data"
|
| 14 |
MODELS_DIR = BASE_DIR / "models"
|
|
|
|
| 17 |
KNOWLEDGE_BASE_DIR = BASE_DIR / "knowledge_base"
|
| 18 |
RESULTS_DIR = BASE_DIR / "results"
|
| 19 |
|
| 20 |
+
# ============================================================================
|
| 21 |
# VERİ SETİ SINIF BİLGİLERİ
|
| 22 |
+
# ============================================================================
|
| 23 |
|
| 24 |
# Veri setindeki tüm sınıflar (train klasöründeki sırayla - alfabetik)
|
| 25 |
DATASET_CLASSES = [
|
|
|
|
| 51 |
TOTAL_VALID = 300
|
| 52 |
TOTAL_TEST = 750
|
| 53 |
|
| 54 |
+
# ============================================================================
|
| 55 |
+
# ⚙️ EĞİTİM KONFİGÜRASYONU
|
| 56 |
+
# ============================================================================
|
| 57 |
|
| 58 |
+
# --- Temel Eğitim Parametreleri ---
|
| 59 |
# Colab T4/A100 için 32 güvenli; OOM yaşarsanız 16'ya düşürün
|
| 60 |
BATCH_SIZE = 8
|
| 61 |
# Colab'da 2 en kararlı değerdir
|
|
|
|
| 72 |
# Scheduler minimum LR
|
| 73 |
MIN_LEARNING_RATE = 1e-07
|
| 74 |
|
| 75 |
+
# --- Model Konfigürasyonu ---
|
| 76 |
MODEL_NAME = "swin_t" # Swin Transformer Tiny
|
| 77 |
IMG_SIZE = 224
|
| 78 |
PRETRAINED = True # ImageNet pre-trained weights
|
| 79 |
|
| 80 |
+
# --- Gelişmiş Eğitim Ayarları ---
|
| 81 |
LABEL_SMOOTHING = 0.1 # Overconfidence engellemek için
|
| 82 |
WEIGHT_DECAY = 0.05 # L2 regularization
|
| 83 |
GRADIENT_CLIP_MAX_NORM = 0.5 # Transformer'larda gradient explosion önleme
|
| 84 |
|
| 85 |
+
# --- Mixed Precision (AMP) ---
|
| 86 |
# Colab GPU'da ~2x hız artışı, bellek tasarrufu sağlar
|
| 87 |
USE_MIXED_PRECISION = False
|
| 88 |
SCALER_INIT_SCALE = 65536.0
|
| 89 |
|
| 90 |
+
# ============================================================================
|
| 91 |
+
# 🔧 FINE-TUNING STRATEJİSİ
|
| 92 |
+
# ============================================================================
|
| 93 |
|
| 94 |
# Aşama 1 (Epoch 1-4): Backbone dondurulur, sadece head eğitilir
|
| 95 |
# Aşama 2 (Epoch 5+): Backbone açılır, differential LR uygulanır
|
|
|
|
| 97 |
UNFREEZE_EPOCH = 5
|
| 98 |
DIFFERENTIAL_LR = True
|
| 99 |
|
| 100 |
+
# ============================================================================
|
| 101 |
+
# 📊 KAYDETME & CHECKPOINT
|
| 102 |
+
# ============================================================================
|
| 103 |
|
| 104 |
SAVE_BEST_MODEL = True
|
| 105 |
SAVE_CHECKPOINT_INTERVAL = 5 # Her 5 epoch'ta checkpoint
|
| 106 |
MODEL_CHECKPOINT_PATH = CHECKPOINTS_DIR / "best_swin_model.pth"
|
| 107 |
FINAL_MODEL_PATH = MODELS_DIR / "final_swin_model.pth"
|
| 108 |
|
| 109 |
+
# ============================================================================
|
| 110 |
+
# ⏹️ EARLY STOPPING
|
| 111 |
+
# ============================================================================
|
| 112 |
|
| 113 |
USE_EARLY_STOPPING = True
|
| 114 |
EARLY_STOPPING_PATIENCE = 15 # 12 epoch iyileşme olmazsa dur
|
| 115 |
EARLY_STOPPING_DELTA = 0.001 # Minimum iyileşme eşiği
|
| 116 |
|
| 117 |
+
# ============================================================================
|
| 118 |
+
# 🎲 REPRODUCIBILITY
|
| 119 |
+
# ============================================================================
|
| 120 |
|
| 121 |
SEED = 42
|
| 122 |
DETERMINISTIC = True
|
| 123 |
|
| 124 |
+
# ============================================================================
|
| 125 |
+
# 💻 CİHAZ AYARLARI
|
| 126 |
+
# ============================================================================
|
| 127 |
|
| 128 |
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 129 |
CUDA_AVAILABLE = torch.cuda.is_available()
|
|
|
|
| 139 |
GPU_NAME = "CPU"
|
| 140 |
GPU_MEMORY = 0
|
| 141 |
|
| 142 |
+
# ============================================================================
|
| 143 |
+
# 📡 API & INFERENCE
|
| 144 |
+
# ============================================================================
|
| 145 |
|
| 146 |
API_HOST = "0.0.0.0"
|
| 147 |
API_PORT = 8000
|
|
|
|
| 149 |
INFERENCE_TIMEOUT = 30
|
| 150 |
CONFIDENCE_THRESHOLD = 0.5 # Bu altında "Belirsiz" döndür
|
| 151 |
|
| 152 |
+
# ============================================================================
|
| 153 |
+
# 📝 LOGGING
|
| 154 |
+
# ============================================================================
|
| 155 |
|
| 156 |
LOG_LEVEL = logging.INFO
|
| 157 |
LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
| 158 |
CONSOLE_LOG = True
|
| 159 |
FILE_LOG = True
|
| 160 |
|
| 161 |
+
# ============================================================================
|
| 162 |
+
# 🔍 VALİDASYON
|
| 163 |
+
# ============================================================================
|
| 164 |
|
| 165 |
CALCULATE_CLASS_WEIGHTS = True
|
| 166 |
VALIDATION_INTERVAL = 1 # Her epoch validate et
|
| 167 |
PRINT_INTERVAL = 20 # Her 20 batch'te log
|
| 168 |
|
| 169 |
+
# ============================================================================
|
| 170 |
+
# 📈 SCHEDULER
|
| 171 |
+
# ============================================================================
|
| 172 |
|
| 173 |
SCHEDULER_TYPE = "cosine"
|
| 174 |
COSINE_T_MAX = EPOCHS
|
| 175 |
COSINE_ETA_MIN = MIN_LEARNING_RATE
|
| 176 |
|
| 177 |
+
# ============================================================================
|
| 178 |
+
# 📊 METRİK TAKİBİ
|
| 179 |
+
# ============================================================================
|
| 180 |
|
| 181 |
TRACK_METRICS = [
|
| 182 |
"train_loss",
|
|
|
|
| 195 |
CLASS_TO_IDX = {}
|
| 196 |
IDX_TO_CLASS = {}
|
| 197 |
|
| 198 |
+
# ============================================================================
|
| 199 |
+
# 📁 KLASÖR OLUŞTURMA
|
| 200 |
+
# ============================================================================
|
| 201 |
|
| 202 |
def create_directories():
|
| 203 |
dirs = [DATA_DIR, MODELS_DIR, LOGS_DIR,
|
|
|
|
| 209 |
|
| 210 |
create_directories()
|
| 211 |
|
| 212 |
+
# ============================================================================
|
| 213 |
+
# ✅ KONFİGÜRASYON DOĞRULAMA
|
| 214 |
+
# ============================================================================
|
| 215 |
|
| 216 |
def validate_config():
|
| 217 |
warnings = []
|
|
|
|
| 236 |
|
| 237 |
config_warnings = validate_config()
|
| 238 |
|
| 239 |
+
# ============================================================================
|
| 240 |
+
# 🖨️ KONFİGÜRASYON YAZDIR
|
| 241 |
+
# ============================================================================
|
| 242 |
|
| 243 |
def print_config():
|
| 244 |
print("\n" + "=" * 80)
|
|
|
|
| 298 |
print("\n" + "=" * 80 + "\n")
|
| 299 |
|
| 300 |
|
| 301 |
+
# ============================================================================
|
| 302 |
+
# 🔧 YARDIMCI FONKSİYONLAR
|
| 303 |
+
# ============================================================================
|
| 304 |
|
| 305 |
def get_device_info():
|
| 306 |
return {
|
{inference → BACKEND/inference}/__init__.py
RENAMED
|
File without changes
|
{inference → BACKEND/inference}/predict.py
RENAMED
|
@@ -16,6 +16,7 @@ from PIL import Image
|
|
| 16 |
from pathlib import Path
|
| 17 |
from typing import Optional
|
| 18 |
|
|
|
|
| 19 |
project_root = Path(__file__).resolve().parent
|
| 20 |
if str(project_root) not in sys.path:
|
| 21 |
sys.path.append(str(project_root))
|
|
@@ -24,6 +25,9 @@ from models.model import WheatDiseaseClassifier
|
|
| 24 |
from utils.dataset import get_transforms
|
| 25 |
|
| 26 |
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
def load_model(
|
| 29 |
model_path: str,
|
|
@@ -77,6 +81,9 @@ def load_model(
|
|
| 77 |
return model
|
| 78 |
|
| 79 |
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
def predict_single(
|
| 82 |
image_path: str,
|
|
@@ -138,6 +145,9 @@ def predict_single(
|
|
| 138 |
}
|
| 139 |
|
| 140 |
|
|
|
|
|
|
|
|
|
|
| 141 |
|
| 142 |
def predict_folder(
|
| 143 |
folder_path: str,
|
|
@@ -184,6 +194,9 @@ def predict_folder(
|
|
| 184 |
return results
|
| 185 |
|
| 186 |
|
|
|
|
|
|
|
|
|
|
| 187 |
|
| 188 |
def print_result(result: dict):
|
| 189 |
if result is None:
|
|
@@ -200,6 +213,9 @@ def print_result(result: dict):
|
|
| 200 |
print(f"{'─'*55}\n")
|
| 201 |
|
| 202 |
|
|
|
|
|
|
|
|
|
|
| 203 |
|
| 204 |
def main():
|
| 205 |
parser = argparse.ArgumentParser(
|
|
@@ -226,12 +242,14 @@ def main():
|
|
| 226 |
|
| 227 |
args = parser.parse_args()
|
| 228 |
|
|
|
|
| 229 |
if args.cpu:
|
| 230 |
device = torch.device("cpu")
|
| 231 |
else:
|
| 232 |
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 233 |
print(f"💻 Cihaz: {device}")
|
| 234 |
|
|
|
|
| 235 |
if not Path(args.mapping).exists():
|
| 236 |
print(f"❌ Class mapping dosyası bulunamadı: {args.mapping}")
|
| 237 |
print(" Önce train.py çalıştırın.")
|
|
@@ -243,10 +261,13 @@ def main():
|
|
| 243 |
num_classes = len(idx_to_class)
|
| 244 |
print(f"🗂️ Sınıf sayısı: {num_classes}")
|
| 245 |
|
|
|
|
| 246 |
model = load_model(args.model, num_classes, device)
|
| 247 |
|
|
|
|
| 248 |
_, val_test_transform = get_transforms()
|
| 249 |
|
|
|
|
| 250 |
all_results = []
|
| 251 |
|
| 252 |
if args.image:
|
|
@@ -279,6 +300,7 @@ def main():
|
|
| 279 |
print(f"\n📊 Özet: {len(all_results)} görüntü | "
|
| 280 |
f"Kesin: {certain} | Belirsiz: {len(all_results)-certain}")
|
| 281 |
|
|
|
|
| 282 |
if args.save and all_results:
|
| 283 |
save_path = Path(args.save)
|
| 284 |
save_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
| 16 |
from pathlib import Path
|
| 17 |
from typing import Optional
|
| 18 |
|
| 19 |
+
# ── Proje path ayarı ──────────────────────────────────────────────────────────
|
| 20 |
project_root = Path(__file__).resolve().parent
|
| 21 |
if str(project_root) not in sys.path:
|
| 22 |
sys.path.append(str(project_root))
|
|
|
|
| 25 |
from utils.dataset import get_transforms
|
| 26 |
|
| 27 |
|
| 28 |
+
# ============================================================================
|
| 29 |
+
# 🔧 MODEL YÜKLEME
|
| 30 |
+
# ============================================================================
|
| 31 |
|
| 32 |
def load_model(
|
| 33 |
model_path: str,
|
|
|
|
| 81 |
return model
|
| 82 |
|
| 83 |
|
| 84 |
+
# ============================================================================
|
| 85 |
+
# 🔍 TEK GÖRÜNTÜ TAHMİN
|
| 86 |
+
# ============================================================================
|
| 87 |
|
| 88 |
def predict_single(
|
| 89 |
image_path: str,
|
|
|
|
| 145 |
}
|
| 146 |
|
| 147 |
|
| 148 |
+
# ============================================================================
|
| 149 |
+
# 📁 KLASÖR TAHMİN
|
| 150 |
+
# ============================================================================
|
| 151 |
|
| 152 |
def predict_folder(
|
| 153 |
folder_path: str,
|
|
|
|
| 194 |
return results
|
| 195 |
|
| 196 |
|
| 197 |
+
# ============================================================================
|
| 198 |
+
# 🖨️ SONUÇ YAZDIR
|
| 199 |
+
# ============================================================================
|
| 200 |
|
| 201 |
def print_result(result: dict):
|
| 202 |
if result is None:
|
|
|
|
| 213 |
print(f"{'─'*55}\n")
|
| 214 |
|
| 215 |
|
| 216 |
+
# ============================================================================
|
| 217 |
+
# 🎬 MAIN
|
| 218 |
+
# ============================================================================
|
| 219 |
|
| 220 |
def main():
|
| 221 |
parser = argparse.ArgumentParser(
|
|
|
|
| 242 |
|
| 243 |
args = parser.parse_args()
|
| 244 |
|
| 245 |
+
# ── Cihaz ─────────────────────────────────────────────────────────────────
|
| 246 |
if args.cpu:
|
| 247 |
device = torch.device("cpu")
|
| 248 |
else:
|
| 249 |
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 250 |
print(f"💻 Cihaz: {device}")
|
| 251 |
|
| 252 |
+
# ── Class Mapping ──────────────────────────────────────────────────────────
|
| 253 |
if not Path(args.mapping).exists():
|
| 254 |
print(f"❌ Class mapping dosyası bulunamadı: {args.mapping}")
|
| 255 |
print(" Önce train.py çalıştırın.")
|
|
|
|
| 261 |
num_classes = len(idx_to_class)
|
| 262 |
print(f"🗂️ Sınıf sayısı: {num_classes}")
|
| 263 |
|
| 264 |
+
# ── Model ─────────────────────────────────────────────────────────────────
|
| 265 |
model = load_model(args.model, num_classes, device)
|
| 266 |
|
| 267 |
+
# ── Transform ─────────────────────────────────────────────────────────────
|
| 268 |
_, val_test_transform = get_transforms()
|
| 269 |
|
| 270 |
+
# ── Tahmin ────────────────────────────────────────────────────────────────
|
| 271 |
all_results = []
|
| 272 |
|
| 273 |
if args.image:
|
|
|
|
| 300 |
print(f"\n📊 Özet: {len(all_results)} görüntü | "
|
| 301 |
f"Kesin: {certain} | Belirsiz: {len(all_results)-certain}")
|
| 302 |
|
| 303 |
+
# ── JSON Kaydet ───────────────────────────────────────────────────────────
|
| 304 |
if args.save and all_results:
|
| 305 |
save_path = Path(args.save)
|
| 306 |
save_path.parent.mkdir(parents=True, exist_ok=True)
|
{knowledge_base → BACKEND/knowledge_base}/__init__.py
RENAMED
|
File without changes
|
{knowledge_base → BACKEND/knowledge_base}/diseases.json
RENAMED
|
File without changes
|
{models → BACKEND/models}/__init__.py
RENAMED
|
File without changes
|
BACKEND/models/__pycache__/__init__.cpython-310.pyc
ADDED
|
Binary file (189 Bytes). View file
|
|
|
BACKEND/models/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (160 Bytes). View file
|
|
|
BACKEND/models/__pycache__/model.cpython-310.pyc
ADDED
|
Binary file (4.6 kB). View file
|
|
|
BACKEND/models/__pycache__/model.cpython-312.pyc
ADDED
|
Binary file (7.09 kB). View file
|
|
|
BACKEND/models/checkpoints/best_swin_model.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d086c2b9ba82cfc3dfb6c47c08cf1108b26fd00b263480dbf7712e8d1fdf37bf
|
| 3 |
+
size 335512043
|
{models → BACKEND/models}/class_mapping.json
RENAMED
|
File without changes
|
{models → BACKEND/models}/evaluate.py
RENAMED
|
@@ -5,6 +5,7 @@ import torch
|
|
| 5 |
import numpy as np
|
| 6 |
from sklearn.metrics import classification_report, accuracy_score, f1_score
|
| 7 |
|
|
|
|
| 8 |
current_dir = Path(__file__).resolve().parent
|
| 9 |
# Eğer evaluate.py 'models' klasöründeyse, bir üst klasöre (ana dizine) çık
|
| 10 |
project_root = current_dir.parent if current_dir.name == "models" else current_dir
|
|
@@ -16,7 +17,6 @@ import config
|
|
| 16 |
from utils.dataset import get_dataloaders
|
| 17 |
from models.model import WheatDiseaseClassifier
|
| 18 |
|
| 19 |
-
|
| 20 |
def evaluate_model(model_path):
|
| 21 |
print("=" * 60)
|
| 22 |
print("🔍 SWIN TRANSFORMER TEST DEĞERLENDİRMESİ BAŞLIYOR")
|
|
@@ -27,33 +27,13 @@ def evaluate_model(model_path):
|
|
| 27 |
print(f"🖥️ Cihaz: {device}")
|
| 28 |
# 2. Test Verisini Yükle (Sadece test_loader'a ihtiyacımız var)
|
| 29 |
print("📦 Test verisi yükleniyor...")
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
from torchvision import transforms, datasets
|
| 33 |
-
from utils.dataset import get_transforms, robust_pil_loader
|
| 34 |
-
|
| 35 |
-
test_dir = config.DATA_DIR / "test"
|
| 36 |
-
if not test_dir.exists():
|
| 37 |
-
print(f"❌ HATA: Test klasörü bulunamadı! Yol: {test_dir}")
|
| 38 |
-
return
|
| 39 |
-
|
| 40 |
-
_, val_test_transform = get_transforms()
|
| 41 |
-
test_dataset = datasets.ImageFolder(
|
| 42 |
-
root=str(test_dir),
|
| 43 |
-
transform=val_test_transform,
|
| 44 |
-
loader=robust_pil_loader,
|
| 45 |
-
)
|
| 46 |
-
|
| 47 |
-
test_loader = torch.utils.data.DataLoader(
|
| 48 |
-
test_dataset,
|
| 49 |
batch_size=config.BATCH_SIZE,
|
| 50 |
-
shuffle=False,
|
| 51 |
num_workers=config.NUM_WORKERS,
|
| 52 |
-
pin_memory=config.PIN_MEMORY
|
| 53 |
)
|
| 54 |
-
|
| 55 |
-
class_to_idx = test_dataset.class_to_idx
|
| 56 |
-
|
| 57 |
# Sınıf isimlerini index sırasına göre alalım (Raporlama için)
|
| 58 |
idx_to_class = {v: k for k, v in class_to_idx.items()}
|
| 59 |
class_names = [idx_to_class[i] for i in range(len(idx_to_class))]
|
|
@@ -63,7 +43,7 @@ def evaluate_model(model_path):
|
|
| 63 |
model = WheatDiseaseClassifier(
|
| 64 |
num_classes=config.NUM_CLASSES,
|
| 65 |
model_name=config.MODEL_NAME,
|
| 66 |
-
pretrained=False
|
| 67 |
).to(device)
|
| 68 |
|
| 69 |
# 4. Kayıtlı .pth Dosyasını Yükle
|
|
@@ -73,11 +53,11 @@ def evaluate_model(model_path):
|
|
| 73 |
return
|
| 74 |
|
| 75 |
checkpoint = torch.load(model_path, map_location=device)
|
| 76 |
-
|
| 77 |
# Eğitim sırasında "checkpoint" sözlüğü (dict) kaydettiysek:
|
| 78 |
-
if isinstance(checkpoint, dict) and
|
| 79 |
-
model.load_state_dict(checkpoint[
|
| 80 |
-
epoch_info = checkpoint.get(
|
| 81 |
print(f"✅ Checkpoint başarıyla yüklendi. (Kaydedildiği Epoch: {epoch_info})")
|
| 82 |
else:
|
| 83 |
# Eğer sadece ağırlıkları (state_dict) doğrudan kaydettiysek:
|
|
@@ -92,23 +72,23 @@ def evaluate_model(model_path):
|
|
| 92 |
test_labels = []
|
| 93 |
|
| 94 |
print("⏳ Test seti üzerinde tahminler yapılıyor, lütfen bekleyin...")
|
| 95 |
-
|
| 96 |
# torch.no_grad() ile RAM kullanımını düşürüp hızı artırıyoruz
|
| 97 |
with torch.no_grad():
|
| 98 |
for images, labels in test_loader:
|
| 99 |
images = images.to(device)
|
| 100 |
labels = labels.to(device)
|
| 101 |
-
|
| 102 |
outputs = model(images)
|
| 103 |
_, predicted = torch.max(outputs, 1)
|
| 104 |
-
|
| 105 |
test_preds.extend(predicted.cpu().numpy())
|
| 106 |
test_labels.extend(labels.cpu().numpy())
|
| 107 |
|
| 108 |
# 7. Sonuçları ve Raporu Hesapla
|
| 109 |
accuracy = accuracy_score(test_labels, test_preds)
|
| 110 |
f1 = f1_score(test_labels, test_preds, average="weighted")
|
| 111 |
-
|
| 112 |
print("\n" + "=" * 60)
|
| 113 |
print("🏆 FİNAL TEST SONUÇLARI")
|
| 114 |
print("=" * 60)
|
|
@@ -116,16 +96,11 @@ def evaluate_model(model_path):
|
|
| 116 |
print(f"📈 Test F1-Score : {f1 * 100:.2f}%")
|
| 117 |
print("-" * 60)
|
| 118 |
print("📋 Detaylı Sınıflandırma Raporu (Sklearn):")
|
| 119 |
-
print(
|
| 120 |
-
classification_report(
|
| 121 |
-
test_labels, test_preds, target_names=class_names, zero_division=0
|
| 122 |
-
)
|
| 123 |
-
)
|
| 124 |
print("=" * 60)
|
| 125 |
-
|
| 126 |
-
|
| 127 |
if __name__ == "__main__":
|
| 128 |
# Yolu CHECKPOINTS_DIR olarak değiştirip doğru dosya adını yazıyoruz
|
| 129 |
MODEL_PATH = config.CHECKPOINTS_DIR / "best_swin_model.pth"
|
| 130 |
-
|
| 131 |
-
evaluate_model(MODEL_PATH)
|
|
|
|
| 5 |
import numpy as np
|
| 6 |
from sklearn.metrics import classification_report, accuracy_score, f1_score
|
| 7 |
|
| 8 |
+
# --- KRİTİK DÜZELTME: config.py'yi bulmak için proje ana dizinini sisteme tanıtıyoruz ---
|
| 9 |
current_dir = Path(__file__).resolve().parent
|
| 10 |
# Eğer evaluate.py 'models' klasöründeyse, bir üst klasöre (ana dizine) çık
|
| 11 |
project_root = current_dir.parent if current_dir.name == "models" else current_dir
|
|
|
|
| 17 |
from utils.dataset import get_dataloaders
|
| 18 |
from models.model import WheatDiseaseClassifier
|
| 19 |
|
|
|
|
| 20 |
def evaluate_model(model_path):
|
| 21 |
print("=" * 60)
|
| 22 |
print("🔍 SWIN TRANSFORMER TEST DEĞERLENDİRMESİ BAŞLIYOR")
|
|
|
|
| 27 |
print(f"🖥️ Cihaz: {device}")
|
| 28 |
# 2. Test Verisini Yükle (Sadece test_loader'a ihtiyacımız var)
|
| 29 |
print("📦 Test verisi yükleniyor...")
|
| 30 |
+
_, _, test_loader, class_to_idx, _ = get_dataloaders(
|
| 31 |
+
data_dir=str(config.DATA_DIR),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
batch_size=config.BATCH_SIZE,
|
|
|
|
| 33 |
num_workers=config.NUM_WORKERS,
|
| 34 |
+
pin_memory=config.PIN_MEMORY
|
| 35 |
)
|
| 36 |
+
|
|
|
|
|
|
|
| 37 |
# Sınıf isimlerini index sırasına göre alalım (Raporlama için)
|
| 38 |
idx_to_class = {v: k for k, v in class_to_idx.items()}
|
| 39 |
class_names = [idx_to_class[i] for i in range(len(idx_to_class))]
|
|
|
|
| 43 |
model = WheatDiseaseClassifier(
|
| 44 |
num_classes=config.NUM_CLASSES,
|
| 45 |
model_name=config.MODEL_NAME,
|
| 46 |
+
pretrained=False # Zaten kendi eğittiğimiz ağırlıkları yükleyeceğiz
|
| 47 |
).to(device)
|
| 48 |
|
| 49 |
# 4. Kayıtlı .pth Dosyasını Yükle
|
|
|
|
| 53 |
return
|
| 54 |
|
| 55 |
checkpoint = torch.load(model_path, map_location=device)
|
| 56 |
+
|
| 57 |
# Eğitim sırasında "checkpoint" sözlüğü (dict) kaydettiysek:
|
| 58 |
+
if isinstance(checkpoint, dict) and 'model_state_dict' in checkpoint:
|
| 59 |
+
model.load_state_dict(checkpoint['model_state_dict'])
|
| 60 |
+
epoch_info = checkpoint.get('epoch', 'Bilinmiyor')
|
| 61 |
print(f"✅ Checkpoint başarıyla yüklendi. (Kaydedildiği Epoch: {epoch_info})")
|
| 62 |
else:
|
| 63 |
# Eğer sadece ağırlıkları (state_dict) doğrudan kaydettiysek:
|
|
|
|
| 72 |
test_labels = []
|
| 73 |
|
| 74 |
print("⏳ Test seti üzerinde tahminler yapılıyor, lütfen bekleyin...")
|
| 75 |
+
|
| 76 |
# torch.no_grad() ile RAM kullanımını düşürüp hızı artırıyoruz
|
| 77 |
with torch.no_grad():
|
| 78 |
for images, labels in test_loader:
|
| 79 |
images = images.to(device)
|
| 80 |
labels = labels.to(device)
|
| 81 |
+
|
| 82 |
outputs = model(images)
|
| 83 |
_, predicted = torch.max(outputs, 1)
|
| 84 |
+
|
| 85 |
test_preds.extend(predicted.cpu().numpy())
|
| 86 |
test_labels.extend(labels.cpu().numpy())
|
| 87 |
|
| 88 |
# 7. Sonuçları ve Raporu Hesapla
|
| 89 |
accuracy = accuracy_score(test_labels, test_preds)
|
| 90 |
f1 = f1_score(test_labels, test_preds, average="weighted")
|
| 91 |
+
|
| 92 |
print("\n" + "=" * 60)
|
| 93 |
print("🏆 FİNAL TEST SONUÇLARI")
|
| 94 |
print("=" * 60)
|
|
|
|
| 96 |
print(f"📈 Test F1-Score : {f1 * 100:.2f}%")
|
| 97 |
print("-" * 60)
|
| 98 |
print("📋 Detaylı Sınıflandırma Raporu (Sklearn):")
|
| 99 |
+
print(classification_report(test_labels, test_preds, target_names=class_names, zero_division=0))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
print("=" * 60)
|
| 101 |
+
|
|
|
|
| 102 |
if __name__ == "__main__":
|
| 103 |
# Yolu CHECKPOINTS_DIR olarak değiştirip doğru dosya adını yazıyoruz
|
| 104 |
MODEL_PATH = config.CHECKPOINTS_DIR / "best_swin_model.pth"
|
| 105 |
+
|
| 106 |
+
evaluate_model(MODEL_PATH)
|
{models → BACKEND/models}/model.py
RENAMED
|
@@ -2,8 +2,10 @@ import torch
|
|
| 2 |
import torch.nn as nn
|
| 3 |
from torchvision.models import swin_t, Swin_T_Weights
|
| 4 |
|
| 5 |
-
# 🤖 MODEL: SWIN TRANSFORMER WHEAT DISEASE CLASSIFIER
|
| 6 |
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
class WheatDiseaseClassifier(nn.Module):
|
| 9 |
"""
|
|
@@ -18,19 +20,19 @@ class WheatDiseaseClassifier(nn.Module):
|
|
| 18 |
Aşama 2 (Epoch 5+) : Backbone açılır, differential LR uygulanır.
|
| 19 |
"""
|
| 20 |
|
| 21 |
-
def __init__(
|
| 22 |
-
self, num_classes: int, model_name: str = "swin_t", pretrained: bool = True
|
| 23 |
-
):
|
| 24 |
super(WheatDiseaseClassifier, self).__init__()
|
| 25 |
|
| 26 |
self.num_classes = num_classes
|
| 27 |
-
self.model_name
|
| 28 |
|
|
|
|
| 29 |
if pretrained:
|
| 30 |
self.base_model = swin_t(weights=Swin_T_Weights.DEFAULT)
|
| 31 |
else:
|
| 32 |
self.base_model = swin_t(weights=None)
|
| 33 |
|
|
|
|
| 34 |
# Swin-T çıkış boyutu: 768
|
| 35 |
num_features = self.base_model.head.in_features
|
| 36 |
|
|
@@ -42,9 +44,13 @@ class WheatDiseaseClassifier(nn.Module):
|
|
| 42 |
nn.Linear(512, num_classes),
|
| 43 |
)
|
| 44 |
|
|
|
|
|
|
|
| 45 |
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 46 |
return self.base_model(x)
|
| 47 |
|
|
|
|
|
|
|
| 48 |
def freeze_backbone(self):
|
| 49 |
"""Backbone'u dondurur — yalnızca head eğitilir (Aşama 1)."""
|
| 50 |
for param in self.base_model.features.parameters():
|
|
@@ -83,11 +89,13 @@ class WheatDiseaseClassifier(nn.Module):
|
|
| 83 |
},
|
| 84 |
]
|
| 85 |
|
|
|
|
|
|
|
| 86 |
def model_summary(self):
|
| 87 |
"""Parametre sayılarını yazdırır."""
|
| 88 |
-
total
|
| 89 |
trainable = sum(p.numel() for p in self.parameters() if p.requires_grad)
|
| 90 |
-
frozen
|
| 91 |
print(f"\n{'─'*50}")
|
| 92 |
print(f" Model : {self.model_name}")
|
| 93 |
print(f" Num Classes : {self.num_classes}")
|
|
@@ -97,6 +105,10 @@ class WheatDiseaseClassifier(nn.Module):
|
|
| 97 |
print(f"{'─'*50}\n")
|
| 98 |
|
| 99 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
if __name__ == "__main__":
|
| 101 |
model = WheatDiseaseClassifier(num_classes=15, pretrained=False)
|
| 102 |
model.model_summary()
|
|
@@ -111,7 +123,7 @@ if __name__ == "__main__":
|
|
| 111 |
|
| 112 |
# Forward pass testi
|
| 113 |
dummy = torch.randn(2, 3, 224, 224)
|
| 114 |
-
out
|
| 115 |
-
print(f"Output shape: {out.shape}")
|
| 116 |
assert out.shape == (2, 15), "❌ Output shape yanlış!"
|
| 117 |
-
print("✅ Model forward pass başarılı.")
|
|
|
|
| 2 |
import torch.nn as nn
|
| 3 |
from torchvision.models import swin_t, Swin_T_Weights
|
| 4 |
|
|
|
|
| 5 |
|
| 6 |
+
# ============================================================================
|
| 7 |
+
# 🤖 MODEL: SWIN TRANSFORMER WHEAT DISEASE CLASSIFIER
|
| 8 |
+
# ============================================================================
|
| 9 |
|
| 10 |
class WheatDiseaseClassifier(nn.Module):
|
| 11 |
"""
|
|
|
|
| 20 |
Aşama 2 (Epoch 5+) : Backbone açılır, differential LR uygulanır.
|
| 21 |
"""
|
| 22 |
|
| 23 |
+
def __init__(self, num_classes: int, model_name: str = "swin_t", pretrained: bool = True):
|
|
|
|
|
|
|
| 24 |
super(WheatDiseaseClassifier, self).__init__()
|
| 25 |
|
| 26 |
self.num_classes = num_classes
|
| 27 |
+
self.model_name = model_name
|
| 28 |
|
| 29 |
+
# ── Backbone ──────────────────────────────────────────────────────────
|
| 30 |
if pretrained:
|
| 31 |
self.base_model = swin_t(weights=Swin_T_Weights.DEFAULT)
|
| 32 |
else:
|
| 33 |
self.base_model = swin_t(weights=None)
|
| 34 |
|
| 35 |
+
# ── Sınıflandırma Kafası ──────────────────────────────────────────────
|
| 36 |
# Swin-T çıkış boyutu: 768
|
| 37 |
num_features = self.base_model.head.in_features
|
| 38 |
|
|
|
|
| 44 |
nn.Linear(512, num_classes),
|
| 45 |
)
|
| 46 |
|
| 47 |
+
# ── Forward ───────────────────────────────────────────────────────────────
|
| 48 |
+
|
| 49 |
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 50 |
return self.base_model(x)
|
| 51 |
|
| 52 |
+
# ── Fine-tuning Yardımcıları ──────────────────────────────────────────────
|
| 53 |
+
|
| 54 |
def freeze_backbone(self):
|
| 55 |
"""Backbone'u dondurur — yalnızca head eğitilir (Aşama 1)."""
|
| 56 |
for param in self.base_model.features.parameters():
|
|
|
|
| 89 |
},
|
| 90 |
]
|
| 91 |
|
| 92 |
+
# ── Model Bilgisi ─────────────────────────────────────────────────────────
|
| 93 |
+
|
| 94 |
def model_summary(self):
|
| 95 |
"""Parametre sayılarını yazdırır."""
|
| 96 |
+
total = sum(p.numel() for p in self.parameters())
|
| 97 |
trainable = sum(p.numel() for p in self.parameters() if p.requires_grad)
|
| 98 |
+
frozen = total - trainable
|
| 99 |
print(f"\n{'─'*50}")
|
| 100 |
print(f" Model : {self.model_name}")
|
| 101 |
print(f" Num Classes : {self.num_classes}")
|
|
|
|
| 105 |
print(f"{'─'*50}\n")
|
| 106 |
|
| 107 |
|
| 108 |
+
# ============================================================================
|
| 109 |
+
# 🧪 TEST
|
| 110 |
+
# ============================================================================
|
| 111 |
+
|
| 112 |
if __name__ == "__main__":
|
| 113 |
model = WheatDiseaseClassifier(num_classes=15, pretrained=False)
|
| 114 |
model.model_summary()
|
|
|
|
| 123 |
|
| 124 |
# Forward pass testi
|
| 125 |
dummy = torch.randn(2, 3, 224, 224)
|
| 126 |
+
out = model(dummy)
|
| 127 |
+
print(f"Output shape: {out.shape}") # [2, 15]
|
| 128 |
assert out.shape == (2, 15), "❌ Output shape yanlış!"
|
| 129 |
+
print("✅ Model forward pass başarılı.")
|
{models → BACKEND/models}/training_history.json
RENAMED
|
File without changes
|
pipeline.py → BACKEND/pipeline.py
RENAMED
|
@@ -1,10 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import time
|
| 2 |
import torch
|
| 3 |
import numpy as np
|
| 4 |
import torch.nn.functional as F
|
| 5 |
from PIL import Image
|
| 6 |
from pathlib import Path
|
| 7 |
-
from dataclasses import dataclass
|
| 8 |
from typing import Optional
|
| 9 |
|
| 10 |
import sys
|
|
@@ -18,22 +27,51 @@ from utils.dataset import get_transforms
|
|
| 18 |
|
| 19 |
import config
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
@dataclass
|
| 22 |
class PipelineResult:
|
|
|
|
|
|
|
|
|
|
| 23 |
predicted_class: str
|
| 24 |
confidence: float
|
| 25 |
is_certain: bool
|
| 26 |
-
top3_predictions: list
|
| 27 |
|
|
|
|
| 28 |
quality_valid: bool
|
| 29 |
quality_warnings: list
|
| 30 |
blur_score: float
|
| 31 |
rejection_reason: Optional[str]
|
| 32 |
|
|
|
|
| 33 |
processing_time_ms: float
|
| 34 |
-
image_size: tuple
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
class WheatDiseasePipeline:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
def __init__(
|
| 38 |
self,
|
| 39 |
cls_checkpoint: str = None,
|
|
@@ -41,9 +79,10 @@ class WheatDiseasePipeline:
|
|
| 41 |
device: str = None,
|
| 42 |
cls_conf: float = 0.50,
|
| 43 |
):
|
| 44 |
-
self.device
|
| 45 |
-
self.cls_conf
|
| 46 |
|
|
|
|
| 47 |
cls_checkpoint = cls_checkpoint or str(
|
| 48 |
project_root / "models" / "checkpoints" / "best_swin_model.pth"
|
| 49 |
)
|
|
@@ -51,40 +90,50 @@ class WheatDiseasePipeline:
|
|
| 51 |
project_root / "models" / "class_mapping.json"
|
| 52 |
)
|
| 53 |
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
| 55 |
self.idx_to_class, self.num_classes = self._load_mapping(cls_mapping)
|
| 56 |
self.classifier = self._load_classifier(cls_checkpoint)
|
| 57 |
_, self.cls_transform = get_transforms()
|
| 58 |
|
| 59 |
-
print(f"Pipeline
|
|
|
|
|
|
|
| 60 |
|
| 61 |
-
def run(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
t0 = time.perf_counter()
|
|
|
|
|
|
|
| 63 |
bgr = self._to_bgr(source)
|
|
|
|
|
|
|
| 64 |
pre: PreprocessResult = self.preprocessor.process(bgr, skip_quality_check=skip_quality)
|
| 65 |
|
|
|
|
| 66 |
if not pre.quality.is_valid:
|
| 67 |
elapsed = (time.perf_counter() - t0) * 1000
|
| 68 |
return self._rejected_result(pre, elapsed)
|
| 69 |
|
|
|
|
| 70 |
cls_result = self._run_classification(pre.image_pil)
|
|
|
|
|
|
|
| 71 |
elapsed = (time.perf_counter() - t0) * 1000
|
|
|
|
| 72 |
|
| 73 |
-
|
| 74 |
-
predicted_class = cls_result["predicted_class"],
|
| 75 |
-
confidence = cls_result["confidence"],
|
| 76 |
-
is_certain = cls_result["is_certain"],
|
| 77 |
-
top3_predictions = cls_result["top3"],
|
| 78 |
-
quality_valid = pre.quality.is_valid,
|
| 79 |
-
quality_warnings = pre.quality.warnings,
|
| 80 |
-
blur_score = pre.quality.blur_score,
|
| 81 |
-
rejection_reason = None,
|
| 82 |
-
processing_time_ms = round(elapsed, 1),
|
| 83 |
-
image_size = pre.original_size,
|
| 84 |
-
)
|
| 85 |
|
| 86 |
def _run_classification(self, pil_image: Image.Image) -> dict:
|
|
|
|
| 87 |
tensor = self.cls_transform(pil_image).unsqueeze(0).to(self.device)
|
|
|
|
| 88 |
with torch.no_grad():
|
| 89 |
logits = self.classifier(tensor)
|
| 90 |
probs = F.softmax(logits, dim=1)[0]
|
|
@@ -95,19 +144,41 @@ class WheatDiseasePipeline:
|
|
| 95 |
round(p.item(), 4))
|
| 96 |
for i, p in zip(top3_idx, top3_probs)
|
| 97 |
]
|
|
|
|
| 98 |
best_class = top3[0][0]
|
| 99 |
best_conf = top3[0][1]
|
| 100 |
|
| 101 |
return {
|
| 102 |
-
"predicted_class" : best_class if best_conf >= self.cls_conf else "
|
| 103 |
"confidence" : best_conf,
|
| 104 |
"is_certain" : best_conf >= self.cls_conf,
|
| 105 |
"top3" : top3,
|
| 106 |
}
|
| 107 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
def _rejected_result(self, pre: PreprocessResult, elapsed_ms: float) -> PipelineResult:
|
| 109 |
return PipelineResult(
|
| 110 |
-
predicted_class = "
|
| 111 |
confidence = 0.0,
|
| 112 |
is_certain = False,
|
| 113 |
top3_predictions = [],
|
|
@@ -119,28 +190,33 @@ class WheatDiseasePipeline:
|
|
| 119 |
image_size = pre.original_size,
|
| 120 |
)
|
| 121 |
|
|
|
|
|
|
|
| 122 |
def _to_bgr(self, source) -> np.ndarray:
|
| 123 |
import cv2
|
| 124 |
if isinstance(source, bytes):
|
| 125 |
arr = np.frombuffer(source, np.uint8)
|
| 126 |
bgr = cv2.imdecode(arr, cv2.IMREAD_COLOR)
|
| 127 |
if bgr is None:
|
| 128 |
-
raise ValueError("
|
| 129 |
return bgr
|
| 130 |
if isinstance(source, Image.Image):
|
| 131 |
return cv2.cvtColor(np.array(source.convert("RGB")), cv2.COLOR_RGB2BGR)
|
| 132 |
if isinstance(source, np.ndarray):
|
| 133 |
return source
|
|
|
|
| 134 |
bgr = cv2.imread(str(source))
|
| 135 |
if bgr is None:
|
| 136 |
-
raise FileNotFoundError(f"
|
| 137 |
return bgr
|
| 138 |
|
| 139 |
def _load_mapping(self, mapping_path: str):
|
| 140 |
import json
|
| 141 |
path = Path(mapping_path)
|
| 142 |
if not path.exists():
|
| 143 |
-
raise FileNotFoundError(
|
|
|
|
|
|
|
| 144 |
with open(path, "r", encoding="utf-8") as f:
|
| 145 |
idx_to_class = json.load(f)
|
| 146 |
return idx_to_class, len(idx_to_class)
|
|
@@ -151,24 +227,19 @@ class WheatDiseasePipeline:
|
|
| 151 |
)
|
| 152 |
path = Path(checkpoint_path)
|
| 153 |
if not path.exists():
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
model.to(self.device)
|
| 164 |
-
model.eval()
|
| 165 |
-
print(f"✅ Model başarıyla yüklendi: {path.name}")
|
| 166 |
-
except Exception as e:
|
| 167 |
-
print(f"❌ Model yükleme hatası: {e}")
|
| 168 |
-
|
| 169 |
return model
|
| 170 |
|
| 171 |
def result_to_dict(self, r: PipelineResult) -> dict:
|
|
|
|
| 172 |
return {
|
| 173 |
"classification": {
|
| 174 |
"predicted_class" : r.predicted_class,
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
PIPELINE.PY - Combined Analysis Pipeline
|
| 3 |
+
Flow:
|
| 4 |
+
Image -> Preprocessing -> Classification
|
| 5 |
+
-> API-ready dict
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import io
|
| 9 |
+
import base64
|
| 10 |
import time
|
| 11 |
import torch
|
| 12 |
import numpy as np
|
| 13 |
import torch.nn.functional as F
|
| 14 |
from PIL import Image
|
| 15 |
from pathlib import Path
|
| 16 |
+
from dataclasses import dataclass, field
|
| 17 |
from typing import Optional
|
| 18 |
|
| 19 |
import sys
|
|
|
|
| 27 |
|
| 28 |
import config
|
| 29 |
|
| 30 |
+
|
| 31 |
+
# ============================================================================
|
| 32 |
+
# OUTPUT DATASTRUCTURE
|
| 33 |
+
# ============================================================================
|
| 34 |
+
|
| 35 |
@dataclass
|
| 36 |
class PipelineResult:
|
| 37 |
+
"""Classification and quality output."""
|
| 38 |
+
|
| 39 |
+
# -- Classification --------------------------------------------------------
|
| 40 |
predicted_class: str
|
| 41 |
confidence: float
|
| 42 |
is_certain: bool
|
| 43 |
+
top3_predictions: list # [(class, score), ...]
|
| 44 |
|
| 45 |
+
# -- Quality ---------------------------------------------------------------
|
| 46 |
quality_valid: bool
|
| 47 |
quality_warnings: list
|
| 48 |
blur_score: float
|
| 49 |
rejection_reason: Optional[str]
|
| 50 |
|
| 51 |
+
# -- Meta ------------------------------------------------------------------
|
| 52 |
processing_time_ms: float
|
| 53 |
+
image_size: tuple # (W, H) original
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
# ============================================================================
|
| 57 |
+
# PIPELINE
|
| 58 |
+
# ============================================================================
|
| 59 |
|
| 60 |
class WheatDiseasePipeline:
|
| 61 |
+
"""
|
| 62 |
+
Wheat disease analysis pipeline (Classification Only).
|
| 63 |
+
|
| 64 |
+
Modules:
|
| 65 |
+
1. ImagePreprocessor -> CLAHE + quality filter
|
| 66 |
+
2. WheatDiseaseClassifier (Swin-T) -> 15 classes
|
| 67 |
+
|
| 68 |
+
Args:
|
| 69 |
+
cls_checkpoint : Swin-T checkpoint path (.pth)
|
| 70 |
+
cls_mapping : class_mapping.json path
|
| 71 |
+
device : "cuda" | "cpu"
|
| 72 |
+
cls_conf : Classification confidence threshold
|
| 73 |
+
"""
|
| 74 |
+
|
| 75 |
def __init__(
|
| 76 |
self,
|
| 77 |
cls_checkpoint: str = None,
|
|
|
|
| 79 |
device: str = None,
|
| 80 |
cls_conf: float = 0.50,
|
| 81 |
):
|
| 82 |
+
self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
|
| 83 |
+
self.cls_conf = cls_conf
|
| 84 |
|
| 85 |
+
# Default paths
|
| 86 |
cls_checkpoint = cls_checkpoint or str(
|
| 87 |
project_root / "models" / "checkpoints" / "best_swin_model.pth"
|
| 88 |
)
|
|
|
|
| 90 |
project_root / "models" / "class_mapping.json"
|
| 91 |
)
|
| 92 |
|
| 93 |
+
# -- Preprocessing -----------------------------------------------------
|
| 94 |
+
self.preprocessor = ImagePreprocessor(target_size=(224, 224))
|
| 95 |
+
|
| 96 |
+
# -- Classifier --------------------------------------------------------
|
| 97 |
self.idx_to_class, self.num_classes = self._load_mapping(cls_mapping)
|
| 98 |
self.classifier = self._load_classifier(cls_checkpoint)
|
| 99 |
_, self.cls_transform = get_transforms()
|
| 100 |
|
| 101 |
+
print(f"Pipeline ready | device={self.device}")
|
| 102 |
+
|
| 103 |
+
# -- Main Method -----------------------------------------------------------
|
| 104 |
|
| 105 |
+
def run(
|
| 106 |
+
self,
|
| 107 |
+
source, # bytes | PIL.Image | np.ndarray | str path
|
| 108 |
+
skip_quality: bool = False,
|
| 109 |
+
) -> PipelineResult:
|
| 110 |
+
"""Runs analysis pipeline."""
|
| 111 |
t0 = time.perf_counter()
|
| 112 |
+
|
| 113 |
+
# 1. -- Source -> numpy ------------------------------------------------
|
| 114 |
bgr = self._to_bgr(source)
|
| 115 |
+
|
| 116 |
+
# 2. -- Preprocessing --------------------------------------------------
|
| 117 |
pre: PreprocessResult = self.preprocessor.process(bgr, skip_quality_check=skip_quality)
|
| 118 |
|
| 119 |
+
# If quality fails, return early
|
| 120 |
if not pre.quality.is_valid:
|
| 121 |
elapsed = (time.perf_counter() - t0) * 1000
|
| 122 |
return self._rejected_result(pre, elapsed)
|
| 123 |
|
| 124 |
+
# 3. -- Classification (Swin-T) ----------------------------------------
|
| 125 |
cls_result = self._run_classification(pre.image_pil)
|
| 126 |
+
|
| 127 |
+
# 4. -- Combine Results ------------------------------------------------
|
| 128 |
elapsed = (time.perf_counter() - t0) * 1000
|
| 129 |
+
return self._build_result(pre, cls_result, elapsed)
|
| 130 |
|
| 131 |
+
# -- Classification --------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
|
| 133 |
def _run_classification(self, pil_image: Image.Image) -> dict:
|
| 134 |
+
"""Classify image using Swin-T."""
|
| 135 |
tensor = self.cls_transform(pil_image).unsqueeze(0).to(self.device)
|
| 136 |
+
|
| 137 |
with torch.no_grad():
|
| 138 |
logits = self.classifier(tensor)
|
| 139 |
probs = F.softmax(logits, dim=1)[0]
|
|
|
|
| 144 |
round(p.item(), 4))
|
| 145 |
for i, p in zip(top3_idx, top3_probs)
|
| 146 |
]
|
| 147 |
+
|
| 148 |
best_class = top3[0][0]
|
| 149 |
best_conf = top3[0][1]
|
| 150 |
|
| 151 |
return {
|
| 152 |
+
"predicted_class" : best_class if best_conf >= self.cls_conf else "Uncertain",
|
| 153 |
"confidence" : best_conf,
|
| 154 |
"is_certain" : best_conf >= self.cls_conf,
|
| 155 |
"top3" : top3,
|
| 156 |
}
|
| 157 |
|
| 158 |
+
# -- Build Result ----------------------------------------------------------
|
| 159 |
+
|
| 160 |
+
def _build_result(
|
| 161 |
+
self,
|
| 162 |
+
pre: PreprocessResult,
|
| 163 |
+
cls: dict,
|
| 164 |
+
elapsed_ms: float,
|
| 165 |
+
) -> PipelineResult:
|
| 166 |
+
return PipelineResult(
|
| 167 |
+
predicted_class = cls["predicted_class"],
|
| 168 |
+
confidence = cls["confidence"],
|
| 169 |
+
is_certain = cls["is_certain"],
|
| 170 |
+
top3_predictions = cls["top3"],
|
| 171 |
+
quality_valid = pre.quality.is_valid,
|
| 172 |
+
quality_warnings = pre.quality.warnings,
|
| 173 |
+
blur_score = pre.quality.blur_score,
|
| 174 |
+
rejection_reason = None,
|
| 175 |
+
processing_time_ms = round(elapsed_ms, 1),
|
| 176 |
+
image_size = pre.original_size,
|
| 177 |
+
)
|
| 178 |
+
|
| 179 |
def _rejected_result(self, pre: PreprocessResult, elapsed_ms: float) -> PipelineResult:
|
| 180 |
return PipelineResult(
|
| 181 |
+
predicted_class = "Rejected",
|
| 182 |
confidence = 0.0,
|
| 183 |
is_certain = False,
|
| 184 |
top3_predictions = [],
|
|
|
|
| 190 |
image_size = pre.original_size,
|
| 191 |
)
|
| 192 |
|
| 193 |
+
# -- Helpers ---------------------------------------------------------------
|
| 194 |
+
|
| 195 |
def _to_bgr(self, source) -> np.ndarray:
|
| 196 |
import cv2
|
| 197 |
if isinstance(source, bytes):
|
| 198 |
arr = np.frombuffer(source, np.uint8)
|
| 199 |
bgr = cv2.imdecode(arr, cv2.IMREAD_COLOR)
|
| 200 |
if bgr is None:
|
| 201 |
+
raise ValueError("Invalid image bytes")
|
| 202 |
return bgr
|
| 203 |
if isinstance(source, Image.Image):
|
| 204 |
return cv2.cvtColor(np.array(source.convert("RGB")), cv2.COLOR_RGB2BGR)
|
| 205 |
if isinstance(source, np.ndarray):
|
| 206 |
return source
|
| 207 |
+
# File path
|
| 208 |
bgr = cv2.imread(str(source))
|
| 209 |
if bgr is None:
|
| 210 |
+
raise FileNotFoundError(f"Image not found: {source}")
|
| 211 |
return bgr
|
| 212 |
|
| 213 |
def _load_mapping(self, mapping_path: str):
|
| 214 |
import json
|
| 215 |
path = Path(mapping_path)
|
| 216 |
if not path.exists():
|
| 217 |
+
raise FileNotFoundError(
|
| 218 |
+
f"class_mapping.json not found: {mapping_path}"
|
| 219 |
+
)
|
| 220 |
with open(path, "r", encoding="utf-8") as f:
|
| 221 |
idx_to_class = json.load(f)
|
| 222 |
return idx_to_class, len(idx_to_class)
|
|
|
|
| 227 |
)
|
| 228 |
path = Path(checkpoint_path)
|
| 229 |
if not path.exists():
|
| 230 |
+
raise FileNotFoundError(
|
| 231 |
+
f"Checkpoint not found: {checkpoint_path}"
|
| 232 |
+
)
|
| 233 |
+
ckpt = torch.load(checkpoint_path, map_location=self.device)
|
| 234 |
+
state = ckpt["model_state_dict"] if "model_state_dict" in ckpt else ckpt
|
| 235 |
+
model.load_state_dict(state)
|
| 236 |
+
model.to(self.device)
|
| 237 |
+
model.eval()
|
| 238 |
+
print(f"Classifier loaded: {path.name}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
return model
|
| 240 |
|
| 241 |
def result_to_dict(self, r: PipelineResult) -> dict:
|
| 242 |
+
"""JSON-serializable dict for FastAPI response."""
|
| 243 |
return {
|
| 244 |
"classification": {
|
| 245 |
"predicted_class" : r.predicted_class,
|
preprocessing.py → BACKEND/preprocessing.py
RENAMED
|
@@ -15,6 +15,9 @@ from dataclasses import dataclass, field
|
|
| 15 |
from typing import Optional, Tuple
|
| 16 |
|
| 17 |
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
@dataclass
|
| 20 |
class QualityReport:
|
|
@@ -39,6 +42,9 @@ class PreprocessResult:
|
|
| 39 |
processed_size: Tuple[int, int]
|
| 40 |
|
| 41 |
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
class QualityThresholds:
|
| 44 |
BLUR_MIN = 80.0 # Bu altı → bulanık
|
|
@@ -51,6 +57,9 @@ class QualityThresholds:
|
|
| 51 |
GREEN_MIN_RATIO = 0.05 # Görüntünün en az %5'i yeşil olmalı
|
| 52 |
|
| 53 |
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
class ImagePreprocessor:
|
| 56 |
"""
|
|
@@ -85,6 +94,7 @@ class ImagePreprocessor:
|
|
| 85 |
tileGridSize= clahe_grid,
|
| 86 |
)
|
| 87 |
|
|
|
|
| 88 |
|
| 89 |
def process(
|
| 90 |
self,
|
|
@@ -131,6 +141,7 @@ class ImagePreprocessor:
|
|
| 131 |
processed_size = self.target_size,
|
| 132 |
)
|
| 133 |
|
|
|
|
| 134 |
|
| 135 |
def _load_to_bgr(self, source) -> np.ndarray:
|
| 136 |
"""Her formatı BGR numpy array'e çevirir."""
|
|
@@ -153,6 +164,7 @@ class ImagePreprocessor:
|
|
| 153 |
raise ValueError(f"Görüntü okunamadı: {path}")
|
| 154 |
return bgr
|
| 155 |
|
|
|
|
| 156 |
|
| 157 |
def _apply_clahe(self, bgr: np.ndarray) -> np.ndarray:
|
| 158 |
"""
|
|
@@ -165,6 +177,7 @@ class ImagePreprocessor:
|
|
| 165 |
lab_clahe = cv2.merge([l_clahe, a, b])
|
| 166 |
return cv2.cvtColor(lab_clahe, cv2.COLOR_LAB2BGR)
|
| 167 |
|
|
|
|
| 168 |
|
| 169 |
def _check_quality(self, bgr: np.ndarray) -> QualityReport:
|
| 170 |
t = self.thresholds
|
|
@@ -227,6 +240,7 @@ class ImagePreprocessor:
|
|
| 227 |
warnings = warnings,
|
| 228 |
)
|
| 229 |
|
|
|
|
| 230 |
|
| 231 |
def quality_to_dict(self, q: QualityReport) -> dict:
|
| 232 |
return {
|
|
@@ -240,6 +254,9 @@ class ImagePreprocessor:
|
|
| 240 |
}
|
| 241 |
|
| 242 |
|
|
|
|
|
|
|
|
|
|
| 243 |
|
| 244 |
if __name__ == "__main__":
|
| 245 |
import sys
|
|
|
|
| 15 |
from typing import Optional, Tuple
|
| 16 |
|
| 17 |
|
| 18 |
+
# ============================================================================
|
| 19 |
+
# 📦 VERİ YAPILARI
|
| 20 |
+
# ============================================================================
|
| 21 |
|
| 22 |
@dataclass
|
| 23 |
class QualityReport:
|
|
|
|
| 42 |
processed_size: Tuple[int, int]
|
| 43 |
|
| 44 |
|
| 45 |
+
# ============================================================================
|
| 46 |
+
# ⚙️ KALİBRASYON EŞİKLERİ
|
| 47 |
+
# ============================================================================
|
| 48 |
|
| 49 |
class QualityThresholds:
|
| 50 |
BLUR_MIN = 80.0 # Bu altı → bulanık
|
|
|
|
| 57 |
GREEN_MIN_RATIO = 0.05 # Görüntünün en az %5'i yeşil olmalı
|
| 58 |
|
| 59 |
|
| 60 |
+
# ============================================================================
|
| 61 |
+
# 🔧 ÖN İŞLEYİCİ
|
| 62 |
+
# ============================================================================
|
| 63 |
|
| 64 |
class ImagePreprocessor:
|
| 65 |
"""
|
|
|
|
| 94 |
tileGridSize= clahe_grid,
|
| 95 |
)
|
| 96 |
|
| 97 |
+
# ── Ana Metod ─────────────────────────────────────────────────────────────
|
| 98 |
|
| 99 |
def process(
|
| 100 |
self,
|
|
|
|
| 141 |
processed_size = self.target_size,
|
| 142 |
)
|
| 143 |
|
| 144 |
+
# ── Görüntü Yükleme ───────────────────────────────────────────────────────
|
| 145 |
|
| 146 |
def _load_to_bgr(self, source) -> np.ndarray:
|
| 147 |
"""Her formatı BGR numpy array'e çevirir."""
|
|
|
|
| 164 |
raise ValueError(f"Görüntü okunamadı: {path}")
|
| 165 |
return bgr
|
| 166 |
|
| 167 |
+
# ── CLAHE ─────────────────────────────────────────────────────────────────
|
| 168 |
|
| 169 |
def _apply_clahe(self, bgr: np.ndarray) -> np.ndarray:
|
| 170 |
"""
|
|
|
|
| 177 |
lab_clahe = cv2.merge([l_clahe, a, b])
|
| 178 |
return cv2.cvtColor(lab_clahe, cv2.COLOR_LAB2BGR)
|
| 179 |
|
| 180 |
+
# ── Kalite Kontrolü ───────────────────────────────────────────────────────
|
| 181 |
|
| 182 |
def _check_quality(self, bgr: np.ndarray) -> QualityReport:
|
| 183 |
t = self.thresholds
|
|
|
|
| 240 |
warnings = warnings,
|
| 241 |
)
|
| 242 |
|
| 243 |
+
# ── Yardımcılar ───────────────────────────────────────────────────────────
|
| 244 |
|
| 245 |
def quality_to_dict(self, q: QualityReport) -> dict:
|
| 246 |
return {
|
|
|
|
| 254 |
}
|
| 255 |
|
| 256 |
|
| 257 |
+
# ============================================================================
|
| 258 |
+
# 🧪 TEST
|
| 259 |
+
# ============================================================================
|
| 260 |
|
| 261 |
if __name__ == "__main__":
|
| 262 |
import sys
|
requirements.txt → BACKEND/requirements.txt
RENAMED
|
@@ -13,5 +13,3 @@ pydantic>=2.0.0
|
|
| 13 |
tqdm>=4.65.0
|
| 14 |
kaggle>=1.6.0
|
| 15 |
python-dotenv>=1.0.0
|
| 16 |
-
gradio>=3.35.0
|
| 17 |
-
opencv-python-headless
|
|
|
|
| 13 |
tqdm>=4.65.0
|
| 14 |
kaggle>=1.6.0
|
| 15 |
python-dotenv>=1.0.0
|
|
|
|
|
|
{training → BACKEND/training}/__init__.py
RENAMED
|
File without changes
|
BACKEND/training/find_corrupt_images.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from glob import glob
|
| 3 |
+
from PIL import Image
|
| 4 |
+
|
| 5 |
+
def find_corrupt_images(data_dir):
|
| 6 |
+
print(f"Veri dizini aranıyor: {os.path.abspath(data_dir)}")
|
| 7 |
+
image_files = glob(os.path.join(data_dir, '**', '*.jpg'), recursive=True) + \
|
| 8 |
+
glob(os.path.join(data_dir, '**', '*.jpeg'), recursive=True) + \
|
| 9 |
+
glob(os.path.join(data_dir, '**', '*.png'), recursive=True)
|
| 10 |
+
|
| 11 |
+
print(f"Toplam {len(image_files)} resim bulundu. Taranıyor...")
|
| 12 |
+
|
| 13 |
+
corrupt_images = []
|
| 14 |
+
|
| 15 |
+
for i, file_path in enumerate(image_files):
|
| 16 |
+
if i % 1000 == 0 and i > 0:
|
| 17 |
+
print(f"{i} resim tarandı...")
|
| 18 |
+
|
| 19 |
+
try:
|
| 20 |
+
with Image.open(file_path) as img:
|
| 21 |
+
# verify() sadece dosyanın bir resim olduğunu doğrular
|
| 22 |
+
img.verify()
|
| 23 |
+
|
| 24 |
+
# verify() kapatıp açmayı gerektirdiği için yeniden açıyoruz
|
| 25 |
+
# load() veriyi belleğe alır, bozuk byte kısımlarını burada yakalarız
|
| 26 |
+
with Image.open(file_path) as img:
|
| 27 |
+
img.load()
|
| 28 |
+
|
| 29 |
+
except Exception as e:
|
| 30 |
+
print(f"Bozuk dosya bulundu: {file_path} - Hata: {str(e)}")
|
| 31 |
+
corrupt_images.append((file_path, str(e)))
|
| 32 |
+
|
| 33 |
+
print("-" * 50)
|
| 34 |
+
print(f"Tarama bitti. Toplam {len(corrupt_images)} adet bozuk resim bulundu.")
|
| 35 |
+
for file, err in corrupt_images:
|
| 36 |
+
print(f"> {file}")
|
| 37 |
+
|
| 38 |
+
return corrupt_images
|
| 39 |
+
|
| 40 |
+
if __name__ == "__main__":
|
| 41 |
+
find_corrupt_images("../data")
|
BACKEND/training/find_corrupt_images_fast.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from glob import glob
|
| 3 |
+
from PIL import Image
|
| 4 |
+
from concurrent.futures import ProcessPoolExecutor
|
| 5 |
+
|
| 6 |
+
def check_image(file_path):
|
| 7 |
+
try:
|
| 8 |
+
with Image.open(file_path) as img:
|
| 9 |
+
img.verify()
|
| 10 |
+
with Image.open(file_path) as img:
|
| 11 |
+
img.load()
|
| 12 |
+
return None
|
| 13 |
+
except Exception as e:
|
| 14 |
+
return (file_path, str(e))
|
| 15 |
+
|
| 16 |
+
def find_corrupt_images(data_dir):
|
| 17 |
+
print(f"Hızlı tarama başlatılıyor: {os.path.abspath(data_dir)}")
|
| 18 |
+
image_files = glob(os.path.join(data_dir, '**', '*.jpg'), recursive=True) + \
|
| 19 |
+
glob(os.path.join(data_dir, '**', '*.jpeg'), recursive=True) + \
|
| 20 |
+
glob(os.path.join(data_dir, '**', '*.png'), recursive=True)
|
| 21 |
+
|
| 22 |
+
print(f"Toplam {len(image_files)} resim taramaya alınıyor...")
|
| 23 |
+
|
| 24 |
+
corrupt_images = []
|
| 25 |
+
with ProcessPoolExecutor() as executor:
|
| 26 |
+
results = executor.map(check_image, image_files)
|
| 27 |
+
|
| 28 |
+
for i, res in enumerate(results):
|
| 29 |
+
if i % 2000 == 0 and i > 0:
|
| 30 |
+
print(f"{i} resim tarandı...")
|
| 31 |
+
if res is not None:
|
| 32 |
+
print(f"Bozuk dosya bulundu: {res[0]} - Hata: {res[1]}")
|
| 33 |
+
corrupt_images.append(res)
|
| 34 |
+
|
| 35 |
+
print("-" * 50)
|
| 36 |
+
print(f"Tarama bitti. Toplam {len(corrupt_images)} adet bozuk resim bulundu.")
|
| 37 |
+
for file, err in corrupt_images:
|
| 38 |
+
print(f"> {file}")
|
| 39 |
+
|
| 40 |
+
return corrupt_images
|
| 41 |
+
|
| 42 |
+
if __name__ == "__main__":
|
| 43 |
+
find_corrupt_images("../data")
|
{training → BACKEND/training}/train.py
RENAMED
|
@@ -7,7 +7,7 @@ TRAIN.PY — Swin Transformer Wheat Disease Classifier
|
|
| 7 |
• Aşamalı Fine-tuning (freeze → unfreeze)
|
| 8 |
• Detaylı metrik takibi (Accuracy, F1, Precision, Recall)
|
| 9 |
• Checkpoint kaydetme (best + periyodik)
|
| 10 |
-
• Kaldığı yerden devam etme (resume training)
|
| 11 |
"""
|
| 12 |
|
| 13 |
import os
|
|
@@ -36,10 +36,9 @@ from sklearn.metrics import (
|
|
| 36 |
classification_report,
|
| 37 |
)
|
| 38 |
|
|
|
|
| 39 |
current_dir = Path(__file__).resolve().parent
|
| 40 |
-
project_root = (
|
| 41 |
-
current_dir.parent if (current_dir.parent / "config.py").exists() else current_dir
|
| 42 |
-
)
|
| 43 |
if str(project_root) not in sys.path:
|
| 44 |
sys.path.append(str(project_root))
|
| 45 |
|
|
@@ -48,6 +47,10 @@ from utils.dataset import get_dataloaders
|
|
| 48 |
from models.model import WheatDiseaseClassifier
|
| 49 |
|
| 50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
def setup_logger() -> logging.Logger:
|
| 52 |
logger = logging.getLogger("WheatTrainer")
|
| 53 |
logger.setLevel(config.LOG_LEVEL)
|
|
@@ -60,9 +63,7 @@ def setup_logger() -> logging.Logger:
|
|
| 60 |
|
| 61 |
if config.FILE_LOG:
|
| 62 |
config.LOGS_DIR.mkdir(parents=True, exist_ok=True)
|
| 63 |
-
log_file = (
|
| 64 |
-
config.LOGS_DIR / f"train_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
|
| 65 |
-
)
|
| 66 |
fh = logging.FileHandler(log_file, encoding="utf-8")
|
| 67 |
fh.setFormatter(formatter)
|
| 68 |
logger.addHandler(fh)
|
|
@@ -71,6 +72,10 @@ def setup_logger() -> logging.Logger:
|
|
| 71 |
return logger
|
| 72 |
|
| 73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
def set_seed(seed: int = config.SEED):
|
| 75 |
torch.manual_seed(seed)
|
| 76 |
np.random.seed(seed)
|
|
@@ -81,24 +86,20 @@ def set_seed(seed: int = config.SEED):
|
|
| 81 |
torch.backends.cudnn.benchmark = False
|
| 82 |
|
| 83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
def calculate_metrics(preds: np.ndarray, labels: np.ndarray) -> dict:
|
| 85 |
"""Accuracy, Precision, Recall, F1 ve confusion matrix hesaplar."""
|
| 86 |
return {
|
| 87 |
"accuracy": float(accuracy_score(labels, preds)),
|
| 88 |
-
"precision": float(
|
| 89 |
-
|
| 90 |
-
),
|
| 91 |
-
"recall": float(
|
| 92 |
-
recall_score(labels, preds, average="weighted", zero_division=0)
|
| 93 |
-
),
|
| 94 |
"f1_score": float(f1_score(labels, preds, average="weighted", zero_division=0)),
|
| 95 |
"per_class_f1": f1_score(labels, preds, average=None, zero_division=0).tolist(),
|
| 96 |
-
"per_class_precision": precision_score(
|
| 97 |
-
|
| 98 |
-
).tolist(),
|
| 99 |
-
"per_class_recall": recall_score(
|
| 100 |
-
labels, preds, average=None, zero_division=0
|
| 101 |
-
).tolist(),
|
| 102 |
"confusion_matrix": confusion_matrix(labels, preds).tolist(),
|
| 103 |
}
|
| 104 |
|
|
@@ -106,7 +107,7 @@ def calculate_metrics(preds: np.ndarray, labels: np.ndarray) -> dict:
|
|
| 106 |
def log_metrics(metrics: dict, class_names: list, stage: str, logger: logging.Logger):
|
| 107 |
"""Metrikleri okunabilir şekilde loglar."""
|
| 108 |
logger.info(f"\n{'='*70}")
|
| 109 |
-
logger.info(f" {stage.upper()} METRİKLERİ")
|
| 110 |
logger.info(f"{'='*70}")
|
| 111 |
logger.info(f" Accuracy : {metrics['accuracy']*100:6.2f}%")
|
| 112 |
logger.info(f" Precision : {metrics['precision']*100:6.2f}%")
|
|
@@ -125,6 +126,10 @@ def log_metrics(metrics: dict, class_names: list, stage: str, logger: logging.Lo
|
|
| 125 |
logger.info(f"{'='*70}\n")
|
| 126 |
|
| 127 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
class EarlyStopping:
|
| 129 |
"""
|
| 130 |
Validation accuracy belirli epoch boyunca iyileşmezse eğitimi durdurur.
|
|
@@ -162,6 +167,10 @@ class EarlyStopping:
|
|
| 162 |
return self.stop
|
| 163 |
|
| 164 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
def train_model():
|
| 166 |
logger = setup_logger()
|
| 167 |
set_seed()
|
|
@@ -174,22 +183,22 @@ def train_model():
|
|
| 174 |
config.print_config()
|
| 175 |
|
| 176 |
device = config.DEVICE
|
| 177 |
-
logger.info(f" Cihaz: {device}")
|
| 178 |
for k, v in config.get_device_info().items():
|
| 179 |
logger.info(f" • {k}: {v}")
|
| 180 |
|
|
|
|
| 181 |
use_amp = config.USE_MIXED_PRECISION and torch.cuda.is_available()
|
| 182 |
scaler = GradScaler(init_scale=config.SCALER_INIT_SCALE) if use_amp else None
|
| 183 |
logger.info(f"⚡ Mixed Precision (AMP): {'Aktif' if use_amp else 'Devre Dışı'}")
|
| 184 |
|
|
|
|
| 185 |
logger.info("📦 Veri setleri yükleniyor...")
|
| 186 |
-
train_loader, val_loader, test_loader, class_to_idx, class_weights = (
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
pin_memory=config.PIN_MEMORY,
|
| 192 |
-
)
|
| 193 |
)
|
| 194 |
|
| 195 |
config.CLASS_TO_IDX = class_to_idx
|
|
@@ -203,6 +212,7 @@ def train_model():
|
|
| 203 |
json.dump(config.IDX_TO_CLASS, f, indent=4, ensure_ascii=False)
|
| 204 |
logger.info(f"✅ Class mapping kaydedildi: {mapping_path}")
|
| 205 |
|
|
|
|
| 206 |
logger.info("🤖 Model oluşturuluyor...")
|
| 207 |
model = WheatDiseaseClassifier(
|
| 208 |
num_classes=config.NUM_CLASSES,
|
|
@@ -215,25 +225,22 @@ def train_model():
|
|
| 215 |
if config.FREEZE_BACKBONE_INITIALLY:
|
| 216 |
model.freeze_backbone()
|
| 217 |
|
|
|
|
| 218 |
class_weights = class_weights.to(device)
|
| 219 |
criterion = nn.CrossEntropyLoss(
|
| 220 |
weight=class_weights,
|
| 221 |
label_smoothing=config.LABEL_SMOOTHING,
|
| 222 |
)
|
| 223 |
-
logger.info(
|
| 224 |
-
f"📉 Loss: CrossEntropyLoss | Label Smoothing: {config.LABEL_SMOOTHING}"
|
| 225 |
-
)
|
| 226 |
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
if config.USE_EARLY_STOPPING
|
| 234 |
-
else None
|
| 235 |
-
)
|
| 236 |
|
|
|
|
| 237 |
history = {m: [] for m in config.TRACK_METRICS}
|
| 238 |
history["val_f1"] = []
|
| 239 |
|
|
@@ -241,6 +248,7 @@ def train_model():
|
|
| 241 |
best_val_f1 = 0.0
|
| 242 |
val_metrics_log = []
|
| 243 |
|
|
|
|
| 244 |
resume_path = config.CHECKPOINTS_DIR / "checkpoint_epoch_030.pth"
|
| 245 |
start_epoch = 1 # Eğitimi 1'den başlat (sadece ağırlıklar alınır)
|
| 246 |
|
|
@@ -248,15 +256,14 @@ def train_model():
|
|
| 248 |
logger.info(f"♻️ Önceki model ağırlıkları yükleniyor: {resume_path}")
|
| 249 |
checkpoint = torch.load(resume_path, map_location=device)
|
| 250 |
# SADECE MODEL AĞIRLIKLARINI YÜKLE (optimizer/scheduler resetlenecek)
|
| 251 |
-
model.load_state_dict(checkpoint[
|
| 252 |
# 384px'e geçtiğimiz için backbone'un açık olduğundan emin olalım
|
| 253 |
model.unfreeze_backbone()
|
| 254 |
-
logger.info(
|
| 255 |
-
"✅ Model ağırlıkları aktarıldı. Optimizer ve scheduler sıfırdan başlatılıyor."
|
| 256 |
-
)
|
| 257 |
else:
|
| 258 |
logger.warning("⚠️ Checkpoint bulunamadı, eğitim sıfırdan başlıyor!")
|
| 259 |
|
|
|
|
| 260 |
# Differential LR için parametre grupları
|
| 261 |
backbone_lr = config.LEARNING_RATE / config.LR_DIVISOR
|
| 262 |
head_lr = config.LEARNING_RATE
|
|
@@ -278,15 +285,16 @@ def train_model():
|
|
| 278 |
|
| 279 |
total_start = time.time()
|
| 280 |
|
|
|
|
| 281 |
# EPOCH DÖNGÜSÜ
|
|
|
|
| 282 |
for epoch in range(start_epoch, config.EPOCHS + 1):
|
| 283 |
epoch_start = time.time()
|
| 284 |
|
|
|
|
| 285 |
if epoch == config.UNFREEZE_EPOCH and config.FREEZE_BACKBONE_INITIALLY:
|
| 286 |
logger.info(f"\n{'='*70}")
|
| 287 |
-
logger.info(
|
| 288 |
-
f"[Epoch {epoch}] 🔓 Backbone açılıyor — Differential LR uygulanıyor"
|
| 289 |
-
)
|
| 290 |
logger.info(f"{'='*70}")
|
| 291 |
|
| 292 |
model.unfreeze_backbone()
|
|
@@ -306,6 +314,7 @@ def train_model():
|
|
| 306 |
logger.info(f" Backbone LR : {backbone_lr}")
|
| 307 |
logger.info(f" Head LR : {head_lr}")
|
| 308 |
|
|
|
|
| 309 |
model.train()
|
| 310 |
running_loss = 0.0
|
| 311 |
batch_count = 0
|
|
@@ -325,9 +334,7 @@ def train_model():
|
|
| 325 |
loss = criterion(outputs, labels)
|
| 326 |
scaler.scale(loss).backward()
|
| 327 |
scaler.unscale_(optimizer)
|
| 328 |
-
torch.nn.utils.clip_grad_norm_(
|
| 329 |
-
model.parameters(), config.GRADIENT_CLIP_MAX_NORM
|
| 330 |
-
)
|
| 331 |
scaler.step(optimizer)
|
| 332 |
scaler.update()
|
| 333 |
else:
|
|
@@ -337,9 +344,7 @@ def train_model():
|
|
| 337 |
logger.error(f"❌ NaN loss — epoch {epoch}, batch {batch_idx}")
|
| 338 |
continue
|
| 339 |
loss.backward()
|
| 340 |
-
torch.nn.utils.clip_grad_norm_(
|
| 341 |
-
model.parameters(), config.GRADIENT_CLIP_MAX_NORM
|
| 342 |
-
)
|
| 343 |
optimizer.step()
|
| 344 |
|
| 345 |
running_loss += loss.item() * images.size(0)
|
|
@@ -357,6 +362,7 @@ def train_model():
|
|
| 357 |
|
| 358 |
epoch_train_loss = running_loss / len(train_loader.dataset)
|
| 359 |
|
|
|
|
| 360 |
if epoch % config.VALIDATION_INTERVAL == 0 or epoch == config.EPOCHS:
|
| 361 |
model.eval()
|
| 362 |
val_loss = 0.0
|
|
@@ -396,6 +402,7 @@ def train_model():
|
|
| 396 |
current_lr = optimizer.param_groups[0]["lr"]
|
| 397 |
epoch_time = time.time() - epoch_start
|
| 398 |
|
|
|
|
| 399 |
history["train_loss"].append(epoch_train_loss)
|
| 400 |
history["val_loss"].append(epoch_val_loss)
|
| 401 |
history["val_accuracy"].append(epoch_val_acc)
|
|
@@ -403,8 +410,9 @@ def train_model():
|
|
| 403 |
history["learning_rate"].append(current_lr)
|
| 404 |
history["epoch_time"].append(epoch_time)
|
| 405 |
|
|
|
|
| 406 |
logger.info(
|
| 407 |
-
f"\n Epoch {epoch:02d}/{config.EPOCHS} — "
|
| 408 |
f"Train Loss: {epoch_train_loss:.4f} | "
|
| 409 |
f"Val Loss: {epoch_val_loss:.4f} | "
|
| 410 |
f"Val Acc: {epoch_val_acc*100:.2f}% | "
|
|
@@ -413,6 +421,7 @@ def train_model():
|
|
| 413 |
f"Süre: {epoch_time:.1f}s"
|
| 414 |
)
|
| 415 |
|
|
|
|
| 416 |
if epoch_val_acc > best_val_acc:
|
| 417 |
best_val_acc = epoch_val_acc
|
| 418 |
best_val_f1 = epoch_val_f1
|
|
@@ -444,23 +453,22 @@ def train_model():
|
|
| 444 |
f"(Acc: {best_val_acc*100:.2f}% | F1: {best_val_f1*100:.2f}%)"
|
| 445 |
)
|
| 446 |
|
|
|
|
| 447 |
if epoch % config.SAVE_CHECKPOINT_INTERVAL == 0:
|
| 448 |
periodic_path = config.CHECKPOINTS_DIR / f"checkpoint_epoch_{epoch:03d}.pth"
|
| 449 |
-
torch.save(
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
},
|
| 458 |
-
periodic_path,
|
| 459 |
-
)
|
| 460 |
logger.info(f" 📌 Periyodik checkpoint: {periodic_path.name}")
|
| 461 |
|
| 462 |
logger.info("─" * 70)
|
| 463 |
|
|
|
|
| 464 |
if early_stopping and early_stopping(epoch_val_acc):
|
| 465 |
logger.info(f"⏹️ Early stopping tetiklendi — Epoch {epoch}")
|
| 466 |
break
|
|
@@ -497,12 +505,9 @@ def train_model():
|
|
| 497 |
log_metrics(test_metrics, class_names, "TEST", logger)
|
| 498 |
|
| 499 |
# sklearn classification report
|
| 500 |
-
logger.info(
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
test_labels, test_preds, target_names=class_names, zero_division=0
|
| 504 |
-
)
|
| 505 |
-
)
|
| 506 |
|
| 507 |
# ══════════════════════════════════════════════════════════════════════════
|
| 508 |
# SONUÇ & KAYIT
|
|
@@ -542,9 +547,7 @@ def train_model():
|
|
| 542 |
}
|
| 543 |
|
| 544 |
config.RESULTS_DIR.mkdir(parents=True, exist_ok=True)
|
| 545 |
-
result_path = (
|
| 546 |
-
config.RESULTS_DIR / f"results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
| 547 |
-
)
|
| 548 |
with open(result_path, "w", encoding="utf-8") as f:
|
| 549 |
json.dump(results, f, indent=4)
|
| 550 |
logger.info(f" 📄 Sonuç raporu: {result_path}")
|
|
@@ -553,6 +556,10 @@ def train_model():
|
|
| 553 |
return model, history, results
|
| 554 |
|
| 555 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 556 |
if __name__ == "__main__":
|
| 557 |
logger = setup_logger()
|
| 558 |
logger.info("🚀 train.py başlatıldı")
|
|
@@ -563,4 +570,4 @@ if __name__ == "__main__":
|
|
| 563 |
logger.warning("⚠️ Eğitim kullanıcı tarafından durduruldu (Ctrl+C)")
|
| 564 |
except Exception as e:
|
| 565 |
logger.error(f"❌ Eğitim hatası: {e}", exc_info=True)
|
| 566 |
-
raise
|
|
|
|
| 7 |
• Aşamalı Fine-tuning (freeze → unfreeze)
|
| 8 |
• Detaylı metrik takibi (Accuracy, F1, Precision, Recall)
|
| 9 |
• Checkpoint kaydetme (best + periyodik)
|
| 10 |
+
• ✨ Kaldığı yerden devam etme (resume training)
|
| 11 |
"""
|
| 12 |
|
| 13 |
import os
|
|
|
|
| 36 |
classification_report,
|
| 37 |
)
|
| 38 |
|
| 39 |
+
# ── Proje path ayarı ──────────────────────────────────────────────────────────
|
| 40 |
current_dir = Path(__file__).resolve().parent
|
| 41 |
+
project_root = current_dir.parent if (current_dir.parent / "config.py").exists() else current_dir
|
|
|
|
|
|
|
| 42 |
if str(project_root) not in sys.path:
|
| 43 |
sys.path.append(str(project_root))
|
| 44 |
|
|
|
|
| 47 |
from models.model import WheatDiseaseClassifier
|
| 48 |
|
| 49 |
|
| 50 |
+
# ============================================================================
|
| 51 |
+
# 📝 LOGGER
|
| 52 |
+
# ============================================================================
|
| 53 |
+
|
| 54 |
def setup_logger() -> logging.Logger:
|
| 55 |
logger = logging.getLogger("WheatTrainer")
|
| 56 |
logger.setLevel(config.LOG_LEVEL)
|
|
|
|
| 63 |
|
| 64 |
if config.FILE_LOG:
|
| 65 |
config.LOGS_DIR.mkdir(parents=True, exist_ok=True)
|
| 66 |
+
log_file = config.LOGS_DIR / f"train_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
|
|
|
|
|
|
|
| 67 |
fh = logging.FileHandler(log_file, encoding="utf-8")
|
| 68 |
fh.setFormatter(formatter)
|
| 69 |
logger.addHandler(fh)
|
|
|
|
| 72 |
return logger
|
| 73 |
|
| 74 |
|
| 75 |
+
# ============================================================================
|
| 76 |
+
# 🎲 REPRODUCIBILITY
|
| 77 |
+
# ============================================================================
|
| 78 |
+
|
| 79 |
def set_seed(seed: int = config.SEED):
|
| 80 |
torch.manual_seed(seed)
|
| 81 |
np.random.seed(seed)
|
|
|
|
| 86 |
torch.backends.cudnn.benchmark = False
|
| 87 |
|
| 88 |
|
| 89 |
+
# ============================================================================
|
| 90 |
+
# 📊 METRİKLER
|
| 91 |
+
# ============================================================================
|
| 92 |
+
|
| 93 |
def calculate_metrics(preds: np.ndarray, labels: np.ndarray) -> dict:
|
| 94 |
"""Accuracy, Precision, Recall, F1 ve confusion matrix hesaplar."""
|
| 95 |
return {
|
| 96 |
"accuracy": float(accuracy_score(labels, preds)),
|
| 97 |
+
"precision": float(precision_score(labels, preds, average="weighted", zero_division=0)),
|
| 98 |
+
"recall": float(recall_score(labels, preds, average="weighted", zero_division=0)),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
"f1_score": float(f1_score(labels, preds, average="weighted", zero_division=0)),
|
| 100 |
"per_class_f1": f1_score(labels, preds, average=None, zero_division=0).tolist(),
|
| 101 |
+
"per_class_precision": precision_score(labels, preds, average=None, zero_division=0).tolist(),
|
| 102 |
+
"per_class_recall": recall_score(labels, preds, average=None, zero_division=0).tolist(),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
"confusion_matrix": confusion_matrix(labels, preds).tolist(),
|
| 104 |
}
|
| 105 |
|
|
|
|
| 107 |
def log_metrics(metrics: dict, class_names: list, stage: str, logger: logging.Logger):
|
| 108 |
"""Metrikleri okunabilir şekilde loglar."""
|
| 109 |
logger.info(f"\n{'='*70}")
|
| 110 |
+
logger.info(f"📊 {stage.upper()} METRİKLERİ")
|
| 111 |
logger.info(f"{'='*70}")
|
| 112 |
logger.info(f" Accuracy : {metrics['accuracy']*100:6.2f}%")
|
| 113 |
logger.info(f" Precision : {metrics['precision']*100:6.2f}%")
|
|
|
|
| 126 |
logger.info(f"{'='*70}\n")
|
| 127 |
|
| 128 |
|
| 129 |
+
# ============================================================================
|
| 130 |
+
# ⏹️ EARLY STOPPING
|
| 131 |
+
# ============================================================================
|
| 132 |
+
|
| 133 |
class EarlyStopping:
|
| 134 |
"""
|
| 135 |
Validation accuracy belirli epoch boyunca iyileşmezse eğitimi durdurur.
|
|
|
|
| 167 |
return self.stop
|
| 168 |
|
| 169 |
|
| 170 |
+
# ============================================================================
|
| 171 |
+
# 🏋️ EĞİTİM DÖNGÜSÜ
|
| 172 |
+
# ============================================================================
|
| 173 |
+
|
| 174 |
def train_model():
|
| 175 |
logger = setup_logger()
|
| 176 |
set_seed()
|
|
|
|
| 183 |
config.print_config()
|
| 184 |
|
| 185 |
device = config.DEVICE
|
| 186 |
+
logger.info(f"🚀 Cihaz: {device}")
|
| 187 |
for k, v in config.get_device_info().items():
|
| 188 |
logger.info(f" • {k}: {v}")
|
| 189 |
|
| 190 |
+
# ── AMP Scaler ────────────────────────────────────────────────────────────
|
| 191 |
use_amp = config.USE_MIXED_PRECISION and torch.cuda.is_available()
|
| 192 |
scaler = GradScaler(init_scale=config.SCALER_INIT_SCALE) if use_amp else None
|
| 193 |
logger.info(f"⚡ Mixed Precision (AMP): {'Aktif' if use_amp else 'Devre Dışı'}")
|
| 194 |
|
| 195 |
+
# ── Veri ──────────────────────────────────────────────────────────────────
|
| 196 |
logger.info("📦 Veri setleri yükleniyor...")
|
| 197 |
+
train_loader, val_loader, test_loader, class_to_idx, class_weights = get_dataloaders(
|
| 198 |
+
data_dir=str(config.DATA_DIR),
|
| 199 |
+
batch_size=config.BATCH_SIZE,
|
| 200 |
+
num_workers=config.NUM_WORKERS,
|
| 201 |
+
pin_memory=config.PIN_MEMORY,
|
|
|
|
|
|
|
| 202 |
)
|
| 203 |
|
| 204 |
config.CLASS_TO_IDX = class_to_idx
|
|
|
|
| 212 |
json.dump(config.IDX_TO_CLASS, f, indent=4, ensure_ascii=False)
|
| 213 |
logger.info(f"✅ Class mapping kaydedildi: {mapping_path}")
|
| 214 |
|
| 215 |
+
# ── Model ─────────────────────────────────────────────────────────────────
|
| 216 |
logger.info("🤖 Model oluşturuluyor...")
|
| 217 |
model = WheatDiseaseClassifier(
|
| 218 |
num_classes=config.NUM_CLASSES,
|
|
|
|
| 225 |
if config.FREEZE_BACKBONE_INITIALLY:
|
| 226 |
model.freeze_backbone()
|
| 227 |
|
| 228 |
+
# ── Loss ──────────────────────────────────────────────────────────────────
|
| 229 |
class_weights = class_weights.to(device)
|
| 230 |
criterion = nn.CrossEntropyLoss(
|
| 231 |
weight=class_weights,
|
| 232 |
label_smoothing=config.LABEL_SMOOTHING,
|
| 233 |
)
|
| 234 |
+
logger.info(f"📉 Loss: CrossEntropyLoss | Label Smoothing: {config.LABEL_SMOOTHING}")
|
|
|
|
|
|
|
| 235 |
|
| 236 |
+
# ── Early Stopping ────────────────────────────────────────────────────────
|
| 237 |
+
early_stopping = EarlyStopping(
|
| 238 |
+
patience=config.EARLY_STOPPING_PATIENCE,
|
| 239 |
+
delta=config.EARLY_STOPPING_DELTA,
|
| 240 |
+
logger=logger,
|
| 241 |
+
) if config.USE_EARLY_STOPPING else None
|
|
|
|
|
|
|
|
|
|
| 242 |
|
| 243 |
+
# ── Tarihçe ───────────────────────────────────────────────────────────────
|
| 244 |
history = {m: [] for m in config.TRACK_METRICS}
|
| 245 |
history["val_f1"] = []
|
| 246 |
|
|
|
|
| 248 |
best_val_f1 = 0.0
|
| 249 |
val_metrics_log = []
|
| 250 |
|
| 251 |
+
# ── Checkpoint Resume (Sadece model ağırlıkları, optimizer/scheduler sıfır) ──
|
| 252 |
resume_path = config.CHECKPOINTS_DIR / "checkpoint_epoch_030.pth"
|
| 253 |
start_epoch = 1 # Eğitimi 1'den başlat (sadece ağırlıklar alınır)
|
| 254 |
|
|
|
|
| 256 |
logger.info(f"♻️ Önceki model ağırlıkları yükleniyor: {resume_path}")
|
| 257 |
checkpoint = torch.load(resume_path, map_location=device)
|
| 258 |
# SADECE MODEL AĞIRLIKLARINI YÜKLE (optimizer/scheduler resetlenecek)
|
| 259 |
+
model.load_state_dict(checkpoint['model_state_dict'])
|
| 260 |
# 384px'e geçtiğimiz için backbone'un açık olduğundan emin olalım
|
| 261 |
model.unfreeze_backbone()
|
| 262 |
+
logger.info("✅ Model ağırlıkları aktarıldı. Optimizer ve scheduler sıfırdan başlatılıyor.")
|
|
|
|
|
|
|
| 263 |
else:
|
| 264 |
logger.warning("⚠️ Checkpoint bulunamadı, eğitim sıfırdan başlıyor!")
|
| 265 |
|
| 266 |
+
# ── Optimizer & Scheduler (Taze başlangıç) ─────────────────────────────────
|
| 267 |
# Differential LR için parametre grupları
|
| 268 |
backbone_lr = config.LEARNING_RATE / config.LR_DIVISOR
|
| 269 |
head_lr = config.LEARNING_RATE
|
|
|
|
| 285 |
|
| 286 |
total_start = time.time()
|
| 287 |
|
| 288 |
+
# ══════════════════════════════════════════════════════════════════════════
|
| 289 |
# EPOCH DÖNGÜSÜ
|
| 290 |
+
# ══════════════════════════════════════════════════════════════════════════
|
| 291 |
for epoch in range(start_epoch, config.EPOCHS + 1):
|
| 292 |
epoch_start = time.time()
|
| 293 |
|
| 294 |
+
# ── Backbone Unfreeze (aşamalı fine-tuning) ───────────────────────────
|
| 295 |
if epoch == config.UNFREEZE_EPOCH and config.FREEZE_BACKBONE_INITIALLY:
|
| 296 |
logger.info(f"\n{'='*70}")
|
| 297 |
+
logger.info(f"[Epoch {epoch}] 🔓 Backbone açılıyor — Differential LR uygulanıyor")
|
|
|
|
|
|
|
| 298 |
logger.info(f"{'='*70}")
|
| 299 |
|
| 300 |
model.unfreeze_backbone()
|
|
|
|
| 314 |
logger.info(f" Backbone LR : {backbone_lr}")
|
| 315 |
logger.info(f" Head LR : {head_lr}")
|
| 316 |
|
| 317 |
+
# ── TRAIN ─────────────────────────────────────────────────────────────
|
| 318 |
model.train()
|
| 319 |
running_loss = 0.0
|
| 320 |
batch_count = 0
|
|
|
|
| 334 |
loss = criterion(outputs, labels)
|
| 335 |
scaler.scale(loss).backward()
|
| 336 |
scaler.unscale_(optimizer)
|
| 337 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), config.GRADIENT_CLIP_MAX_NORM)
|
|
|
|
|
|
|
| 338 |
scaler.step(optimizer)
|
| 339 |
scaler.update()
|
| 340 |
else:
|
|
|
|
| 344 |
logger.error(f"❌ NaN loss — epoch {epoch}, batch {batch_idx}")
|
| 345 |
continue
|
| 346 |
loss.backward()
|
| 347 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), config.GRADIENT_CLIP_MAX_NORM)
|
|
|
|
|
|
|
| 348 |
optimizer.step()
|
| 349 |
|
| 350 |
running_loss += loss.item() * images.size(0)
|
|
|
|
| 362 |
|
| 363 |
epoch_train_loss = running_loss / len(train_loader.dataset)
|
| 364 |
|
| 365 |
+
# ── VALIDATION ────────────────────────────────────────────────────────
|
| 366 |
if epoch % config.VALIDATION_INTERVAL == 0 or epoch == config.EPOCHS:
|
| 367 |
model.eval()
|
| 368 |
val_loss = 0.0
|
|
|
|
| 402 |
current_lr = optimizer.param_groups[0]["lr"]
|
| 403 |
epoch_time = time.time() - epoch_start
|
| 404 |
|
| 405 |
+
# ── History ───────────────────────────────────────────────────────────
|
| 406 |
history["train_loss"].append(epoch_train_loss)
|
| 407 |
history["val_loss"].append(epoch_val_loss)
|
| 408 |
history["val_accuracy"].append(epoch_val_acc)
|
|
|
|
| 410 |
history["learning_rate"].append(current_lr)
|
| 411 |
history["epoch_time"].append(epoch_time)
|
| 412 |
|
| 413 |
+
# ── Epoch Özeti ───────────────────────────────────────────────────────
|
| 414 |
logger.info(
|
| 415 |
+
f"\n✅ Epoch {epoch:02d}/{config.EPOCHS} — "
|
| 416 |
f"Train Loss: {epoch_train_loss:.4f} | "
|
| 417 |
f"Val Loss: {epoch_val_loss:.4f} | "
|
| 418 |
f"Val Acc: {epoch_val_acc*100:.2f}% | "
|
|
|
|
| 421 |
f"Süre: {epoch_time:.1f}s"
|
| 422 |
)
|
| 423 |
|
| 424 |
+
# ── Best Model Kaydet ─────────────────────────────────────────────────
|
| 425 |
if epoch_val_acc > best_val_acc:
|
| 426 |
best_val_acc = epoch_val_acc
|
| 427 |
best_val_f1 = epoch_val_f1
|
|
|
|
| 453 |
f"(Acc: {best_val_acc*100:.2f}% | F1: {best_val_f1*100:.2f}%)"
|
| 454 |
)
|
| 455 |
|
| 456 |
+
# ── Periyodik Checkpoint ──────────────────────────────────────────────
|
| 457 |
if epoch % config.SAVE_CHECKPOINT_INTERVAL == 0:
|
| 458 |
periodic_path = config.CHECKPOINTS_DIR / f"checkpoint_epoch_{epoch:03d}.pth"
|
| 459 |
+
torch.save({
|
| 460 |
+
"epoch": epoch,
|
| 461 |
+
"model_state_dict": model.state_dict(),
|
| 462 |
+
"optimizer_state_dict": optimizer.state_dict(),
|
| 463 |
+
"scheduler_state_dict": scheduler.state_dict(),
|
| 464 |
+
"val_acc": epoch_val_acc,
|
| 465 |
+
"history": history,
|
| 466 |
+
}, periodic_path)
|
|
|
|
|
|
|
|
|
|
| 467 |
logger.info(f" 📌 Periyodik checkpoint: {periodic_path.name}")
|
| 468 |
|
| 469 |
logger.info("─" * 70)
|
| 470 |
|
| 471 |
+
# ── Early Stopping Kontrolü ───────────────────────────────────────────
|
| 472 |
if early_stopping and early_stopping(epoch_val_acc):
|
| 473 |
logger.info(f"⏹️ Early stopping tetiklendi — Epoch {epoch}")
|
| 474 |
break
|
|
|
|
| 505 |
log_metrics(test_metrics, class_names, "TEST", logger)
|
| 506 |
|
| 507 |
# sklearn classification report
|
| 508 |
+
logger.info("📋 Sklearn Classification Report:\n" +
|
| 509 |
+
classification_report(test_labels, test_preds,
|
| 510 |
+
target_names=class_names, zero_division=0))
|
|
|
|
|
|
|
|
|
|
| 511 |
|
| 512 |
# ══════════════════════════════════════════════════════════════════════════
|
| 513 |
# SONUÇ & KAYIT
|
|
|
|
| 547 |
}
|
| 548 |
|
| 549 |
config.RESULTS_DIR.mkdir(parents=True, exist_ok=True)
|
| 550 |
+
result_path = config.RESULTS_DIR / f"results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
|
|
|
|
|
|
| 551 |
with open(result_path, "w", encoding="utf-8") as f:
|
| 552 |
json.dump(results, f, indent=4)
|
| 553 |
logger.info(f" 📄 Sonuç raporu: {result_path}")
|
|
|
|
| 556 |
return model, history, results
|
| 557 |
|
| 558 |
|
| 559 |
+
# ============================================================================
|
| 560 |
+
# 🎬 MAIN
|
| 561 |
+
# ============================================================================
|
| 562 |
+
|
| 563 |
if __name__ == "__main__":
|
| 564 |
logger = setup_logger()
|
| 565 |
logger.info("🚀 train.py başlatıldı")
|
|
|
|
| 570 |
logger.warning("⚠️ Eğitim kullanıcı tarafından durduruldu (Ctrl+C)")
|
| 571 |
except Exception as e:
|
| 572 |
logger.error(f"❌ Eğitim hatası: {e}", exc_info=True)
|
| 573 |
+
raise
|
{utils → BACKEND/utils}/__init__.py
RENAMED
|
File without changes
|
BACKEND/utils/__pycache__/__init__.cpython-310.pyc
ADDED
|
Binary file (188 Bytes). View file
|
|
|
BACKEND/utils/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (159 Bytes). View file
|
|
|
BACKEND/utils/__pycache__/dataset.cpython-310.pyc
ADDED
|
Binary file (8.63 kB). View file
|
|
|
BACKEND/utils/__pycache__/dataset.cpython-312.pyc
ADDED
|
Binary file (12.4 kB). View file
|
|
|
BACKEND/utils/analyze_dataset.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import ssl
|
| 3 |
+
from collections import Counter
|
| 4 |
+
|
| 5 |
+
# To avoid ssl issues if any downloading happened in the future
|
| 6 |
+
ssl._create_default_https_context = ssl._create_unverified_context
|
| 7 |
+
|
| 8 |
+
def analyze_dataset(data_dir):
|
| 9 |
+
print(f"Veri Seti Analizi Başlıyor: {data_dir}\n" + "="*40)
|
| 10 |
+
|
| 11 |
+
splits = ['train', 'valid', 'test']
|
| 12 |
+
total_images = 0
|
| 13 |
+
|
| 14 |
+
for split in splits:
|
| 15 |
+
split_dir = os.path.join(data_dir, split)
|
| 16 |
+
if not os.path.exists(split_dir):
|
| 17 |
+
print(f"[UYARI] {split} klasörü bulunamadı!")
|
| 18 |
+
continue
|
| 19 |
+
|
| 20 |
+
print(f"\n--- {split.upper()} Klasörü Sınıf Dağılımı ---")
|
| 21 |
+
class_counts = {}
|
| 22 |
+
split_total = 0
|
| 23 |
+
|
| 24 |
+
for class_name in os.listdir(split_dir):
|
| 25 |
+
class_path = os.path.join(split_dir, class_name)
|
| 26 |
+
if os.path.isdir(class_path):
|
| 27 |
+
# Count only image files roughly
|
| 28 |
+
images = [f for f in os.listdir(class_path) if f.lower().endswith(('.png', '.jpg', '.jpeg'))]
|
| 29 |
+
count = len(images)
|
| 30 |
+
class_counts[class_name] = count
|
| 31 |
+
split_total += count
|
| 32 |
+
|
| 33 |
+
# Print sorted by count
|
| 34 |
+
for class_name, count in sorted(class_counts.items(), key=lambda item: item[1], reverse=True):
|
| 35 |
+
print(f" {class_name.ljust(25)}: {count} görsel")
|
| 36 |
+
|
| 37 |
+
print(f"-> Toplam {split.upper()} Görseli: {split_total}")
|
| 38 |
+
total_images += split_total
|
| 39 |
+
|
| 40 |
+
print("\n" + "="*40)
|
| 41 |
+
print(f"Toplanan Veri Seti Toplam Büyüklüğü (Tüm Görseller): {total_images}")
|
| 42 |
+
|
| 43 |
+
if __name__ == "__main__":
|
| 44 |
+
# Script dosyasının bulunduğu klasöre göre 'data' klasörünün tam yolunu buluyoruz
|
| 45 |
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
| 46 |
+
project_root = os.path.dirname(current_dir)
|
| 47 |
+
data_path = os.path.join(project_root, "data")
|
| 48 |
+
|
| 49 |
+
analyze_dataset(data_path)
|
| 50 |
+
|
{utils → BACKEND/utils}/dataset.py
RENAMED
|
@@ -7,12 +7,16 @@ from torchvision import transforms, datasets
|
|
| 7 |
from torch.utils.data import DataLoader, Dataset
|
| 8 |
|
| 9 |
|
|
|
|
| 10 |
# 📐 GÖRÜNTÜ BOYUTU
|
|
|
|
| 11 |
|
| 12 |
IMG_SIZE = 224
|
| 13 |
|
| 14 |
|
|
|
|
| 15 |
# 🔄 VERİ DÖNÜŞÜMLER (TRANSFORMS)
|
|
|
|
| 16 |
|
| 17 |
def get_transforms():
|
| 18 |
"""
|
|
@@ -50,6 +54,9 @@ def get_transforms():
|
|
| 50 |
return train_transform, val_test_transform
|
| 51 |
|
| 52 |
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
def robust_pil_loader(path: str):
|
| 55 |
"""
|
|
@@ -70,6 +77,9 @@ def robust_pil_loader(path: str):
|
|
| 70 |
raise e
|
| 71 |
|
| 72 |
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
class RemappedImageFolder(datasets.ImageFolder):
|
| 75 |
"""
|
|
@@ -140,6 +150,9 @@ class RemappedImageFolder(datasets.ImageFolder):
|
|
| 140 |
print(f"⚠️ Toplam {skipped} örnek atlandı (eşleşme bulunamadı)")
|
| 141 |
|
| 142 |
|
|
|
|
|
|
|
|
|
|
| 143 |
|
| 144 |
def compute_class_weights(targets, num_classes):
|
| 145 |
"""
|
|
@@ -169,6 +182,9 @@ def compute_class_weights(targets, num_classes):
|
|
| 169 |
return torch.FloatTensor(class_weights)
|
| 170 |
|
| 171 |
|
|
|
|
|
|
|
|
|
|
| 172 |
|
| 173 |
def get_dataloaders(
|
| 174 |
data_dir: str,
|
|
@@ -206,7 +222,9 @@ def get_dataloaders(
|
|
| 206 |
|
| 207 |
train_transform, val_test_transform = get_transforms()
|
| 208 |
|
|
|
|
| 209 |
# 1. TRAIN DATASET
|
|
|
|
| 210 |
train_dataset = datasets.ImageFolder(
|
| 211 |
root=train_dir,
|
| 212 |
transform=train_transform,
|
|
@@ -219,7 +237,9 @@ def get_dataloaders(
|
|
| 219 |
count = train_dataset.targets.count(idx)
|
| 220 |
print(f" [{idx:2d}] {cls:<25} → {count} görsel")
|
| 221 |
|
|
|
|
| 222 |
# 2. VALID DATASET (Remap ile)
|
|
|
|
| 223 |
# Varsayılan remap: blast_test_valid → Blast
|
| 224 |
if valid_remap is None:
|
| 225 |
valid_remap = {"blast_test_valid": "Blast"}
|
|
@@ -233,7 +253,9 @@ def get_dataloaders(
|
|
| 233 |
print(f"\n✅ Valid sınıfları ({len(valid_dataset.class_to_idx)} adet) — "
|
| 234 |
f"Remap uygulandı: {valid_remap}")
|
| 235 |
|
|
|
|
| 236 |
# 3. TEST DATASET
|
|
|
|
| 237 |
test_dataset = datasets.ImageFolder(
|
| 238 |
root=test_dir,
|
| 239 |
transform=val_test_transform,
|
|
@@ -241,7 +263,9 @@ def get_dataloaders(
|
|
| 241 |
)
|
| 242 |
print(f"\n✅ Test sınıfları ({len(test_dataset.class_to_idx)} adet)")
|
| 243 |
|
|
|
|
| 244 |
# 4. CLASS WEIGHTS (Dengesizlik için)
|
|
|
|
| 245 |
class_weights = compute_class_weights(
|
| 246 |
targets=train_dataset.targets,
|
| 247 |
num_classes=len(class_to_idx)
|
|
@@ -251,7 +275,9 @@ def get_dataloaders(
|
|
| 251 |
for i, w in enumerate(class_weights):
|
| 252 |
print(f" [{i:2d}] {idx_to_class[i]:<25} → weight: {w:.4f}")
|
| 253 |
|
|
|
|
| 254 |
# 5. DATALOADERS
|
|
|
|
| 255 |
train_loader = DataLoader(
|
| 256 |
train_dataset,
|
| 257 |
batch_size=batch_size,
|
|
@@ -285,6 +311,9 @@ def get_dataloaders(
|
|
| 285 |
return train_loader, valid_loader, test_loader, class_to_idx, class_weights
|
| 286 |
|
| 287 |
|
|
|
|
|
|
|
|
|
|
| 288 |
|
| 289 |
if __name__ == "__main__":
|
| 290 |
import sys
|
|
|
|
| 7 |
from torch.utils.data import DataLoader, Dataset
|
| 8 |
|
| 9 |
|
| 10 |
+
# ============================================================================
|
| 11 |
# 📐 GÖRÜNTÜ BOYUTU
|
| 12 |
+
# ============================================================================
|
| 13 |
|
| 14 |
IMG_SIZE = 224
|
| 15 |
|
| 16 |
|
| 17 |
+
# ============================================================================
|
| 18 |
# 🔄 VERİ DÖNÜŞÜMLER (TRANSFORMS)
|
| 19 |
+
# ============================================================================
|
| 20 |
|
| 21 |
def get_transforms():
|
| 22 |
"""
|
|
|
|
| 54 |
return train_transform, val_test_transform
|
| 55 |
|
| 56 |
|
| 57 |
+
# ============================================================================
|
| 58 |
+
# 🖼️ GÜVENLİ GÖRÜNTÜ OKUYUCU
|
| 59 |
+
# ============================================================================
|
| 60 |
|
| 61 |
def robust_pil_loader(path: str):
|
| 62 |
"""
|
|
|
|
| 77 |
raise e
|
| 78 |
|
| 79 |
|
| 80 |
+
# ============================================================================
|
| 81 |
+
# 🔧 VALID KLASÖRÜ SINIF ADINI DÜZELTİCİ
|
| 82 |
+
# ============================================================================
|
| 83 |
|
| 84 |
class RemappedImageFolder(datasets.ImageFolder):
|
| 85 |
"""
|
|
|
|
| 150 |
print(f"⚠️ Toplam {skipped} örnek atlandı (eşleşme bulunamadı)")
|
| 151 |
|
| 152 |
|
| 153 |
+
# ============================================================================
|
| 154 |
+
# ⚖️ CLASS WEIGHTS HESAPLAMA
|
| 155 |
+
# ============================================================================
|
| 156 |
|
| 157 |
def compute_class_weights(targets, num_classes):
|
| 158 |
"""
|
|
|
|
| 182 |
return torch.FloatTensor(class_weights)
|
| 183 |
|
| 184 |
|
| 185 |
+
# ============================================================================
|
| 186 |
+
# 🚀 ANA FONKSİYON: DATALOADER'LAR
|
| 187 |
+
# ============================================================================
|
| 188 |
|
| 189 |
def get_dataloaders(
|
| 190 |
data_dir: str,
|
|
|
|
| 222 |
|
| 223 |
train_transform, val_test_transform = get_transforms()
|
| 224 |
|
| 225 |
+
# ------------------------------------------------------------------
|
| 226 |
# 1. TRAIN DATASET
|
| 227 |
+
# ------------------------------------------------------------------
|
| 228 |
train_dataset = datasets.ImageFolder(
|
| 229 |
root=train_dir,
|
| 230 |
transform=train_transform,
|
|
|
|
| 237 |
count = train_dataset.targets.count(idx)
|
| 238 |
print(f" [{idx:2d}] {cls:<25} → {count} görsel")
|
| 239 |
|
| 240 |
+
# ------------------------------------------------------------------
|
| 241 |
# 2. VALID DATASET (Remap ile)
|
| 242 |
+
# ------------------------------------------------------------------
|
| 243 |
# Varsayılan remap: blast_test_valid → Blast
|
| 244 |
if valid_remap is None:
|
| 245 |
valid_remap = {"blast_test_valid": "Blast"}
|
|
|
|
| 253 |
print(f"\n✅ Valid sınıfları ({len(valid_dataset.class_to_idx)} adet) — "
|
| 254 |
f"Remap uygulandı: {valid_remap}")
|
| 255 |
|
| 256 |
+
# ------------------------------------------------------------------
|
| 257 |
# 3. TEST DATASET
|
| 258 |
+
# ------------------------------------------------------------------
|
| 259 |
test_dataset = datasets.ImageFolder(
|
| 260 |
root=test_dir,
|
| 261 |
transform=val_test_transform,
|
|
|
|
| 263 |
)
|
| 264 |
print(f"\n✅ Test sınıfları ({len(test_dataset.class_to_idx)} adet)")
|
| 265 |
|
| 266 |
+
# ------------------------------------------------------------------
|
| 267 |
# 4. CLASS WEIGHTS (Dengesizlik için)
|
| 268 |
+
# ------------------------------------------------------------------
|
| 269 |
class_weights = compute_class_weights(
|
| 270 |
targets=train_dataset.targets,
|
| 271 |
num_classes=len(class_to_idx)
|
|
|
|
| 275 |
for i, w in enumerate(class_weights):
|
| 276 |
print(f" [{i:2d}] {idx_to_class[i]:<25} → weight: {w:.4f}")
|
| 277 |
|
| 278 |
+
# ------------------------------------------------------------------
|
| 279 |
# 5. DATALOADERS
|
| 280 |
+
# ------------------------------------------------------------------
|
| 281 |
train_loader = DataLoader(
|
| 282 |
train_dataset,
|
| 283 |
batch_size=batch_size,
|
|
|
|
| 311 |
return train_loader, valid_loader, test_loader, class_to_idx, class_weights
|
| 312 |
|
| 313 |
|
| 314 |
+
# ============================================================================
|
| 315 |
+
# 🧪 TEST
|
| 316 |
+
# ============================================================================
|
| 317 |
|
| 318 |
if __name__ == "__main__":
|
| 319 |
import sys
|
{utils → BACKEND/utils}/download_data.py
RENAMED
|
File without changes
|
BACKEND/utils/fix_folders.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
def fix_folder_names(data_dir):
|
| 4 |
+
"""
|
| 5 |
+
valid ve test klasörlerindeki sınıf isimlerini train klasöründeki ile aynı yapar.
|
| 6 |
+
Örn: 'smut_valid' -> 'Smut'
|
| 7 |
+
"""
|
| 8 |
+
train_dir = os.path.join(data_dir, 'train')
|
| 9 |
+
valid_dir = os.path.join(data_dir, 'valid')
|
| 10 |
+
test_dir = os.path.join(data_dir, 'test')
|
| 11 |
+
|
| 12 |
+
# Train klasöründen hedeflenen isimleri çekelim
|
| 13 |
+
# Örn: 'Smut', 'Stem fly', vs.
|
| 14 |
+
target_names = [d for d in os.listdir(train_dir) if os.path.isdir(os.path.join(train_dir, d))]
|
| 15 |
+
|
| 16 |
+
# İsimleri küçük harf ve boşluksuz/alt çizgili yapıya getirme haritası
|
| 17 |
+
# Örn: 'Smut' -> 'smut', 'Yellow Rust' -> 'yellow_rust'
|
| 18 |
+
name_map = {}
|
| 19 |
+
for name in target_names:
|
| 20 |
+
normalized = name.lower().replace(" ", "_").replace("-", "_")
|
| 21 |
+
name_map[normalized] = name
|
| 22 |
+
|
| 23 |
+
# valid için düzeltme
|
| 24 |
+
for subdir in os.listdir(valid_dir):
|
| 25 |
+
old_path = os.path.join(valid_dir, subdir)
|
| 26 |
+
if os.path.isdir(old_path):
|
| 27 |
+
# 'smut_valid' -> 'smut' bul
|
| 28 |
+
base_name = subdir.replace("_valid", "").replace("_val", "").lower()
|
| 29 |
+
if base_name in name_map:
|
| 30 |
+
new_path = os.path.join(valid_dir, name_map[base_name])
|
| 31 |
+
if not os.path.exists(new_path):
|
| 32 |
+
os.rename(old_path, new_path)
|
| 33 |
+
print(f"Renamed: {old_path} -> {new_path}")
|
| 34 |
+
else:
|
| 35 |
+
print(f"Bypass (already exists): {new_path}")
|
| 36 |
+
else:
|
| 37 |
+
print(f"[UYARI] Eşleşme bulunamadı: {subdir}")
|
| 38 |
+
|
| 39 |
+
# test için düzeltme
|
| 40 |
+
for subdir in os.listdir(test_dir):
|
| 41 |
+
old_path = os.path.join(test_dir, subdir)
|
| 42 |
+
if os.path.isdir(old_path):
|
| 43 |
+
base_name = subdir.replace("_test", "").lower()
|
| 44 |
+
if base_name in name_map:
|
| 45 |
+
new_path = os.path.join(test_dir, name_map[base_name])
|
| 46 |
+
if not os.path.exists(new_path):
|
| 47 |
+
os.rename(old_path, new_path)
|
| 48 |
+
print(f"Renamed: {old_path} -> {new_path}")
|
| 49 |
+
else:
|
| 50 |
+
print(f"Bypass (already exists): {new_path}")
|
| 51 |
+
else:
|
| 52 |
+
print(f"[UYARI] Eşleşme bulunamadı: {subdir}")
|
| 53 |
+
|
| 54 |
+
if __name__ == "__main__":
|
| 55 |
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
| 56 |
+
project_root = os.path.dirname(current_dir)
|
| 57 |
+
data_path = os.path.join(project_root, "data")
|
| 58 |
+
fix_folder_names(data_path)
|
README.md
CHANGED
|
@@ -1,49 +1,37 @@
|
|
| 1 |
-
|
| 2 |
-
title: Wheat Disease Detection
|
| 3 |
-
emoji: 🌾
|
| 4 |
-
colorFrom: green
|
| 5 |
-
colorTo: yellow
|
| 6 |
-
sdk: docker
|
| 7 |
-
pinned: false
|
| 8 |
-
---
|
| 9 |
|
| 10 |
-
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
Proje, üretime hazır (production-ready) bir altyapıya sahip olup nesne tespiti veya karmaşık segmentasyon işlemlerinden arındırılmış, tamamen yüksek isabet oranlı görüntü sınıflandırmasına (Image Classification) odaklanmıştır.
|
| 15 |
|
| 16 |
---
|
| 17 |
|
| 18 |
## 🎯 Proje Özellikleri
|
| 19 |
|
| 20 |
-
- **
|
| 21 |
-
- **Model:**
|
| 22 |
-
- **Aşırı Öğrenme Kontrolleri:** Dinamik Learning Rate Scheduler (Cosine Annealing)
|
| 23 |
-
- **
|
| 24 |
-
- **
|
| 25 |
|
| 26 |
---
|
| 27 |
|
| 28 |
## 📂 Dizin Yapısı / Mimari
|
| 29 |
|
| 30 |
```text
|
| 31 |
-
|
| 32 |
-
├── api
|
| 33 |
-
├──
|
| 34 |
-
|
| 35 |
-
├──
|
| 36 |
-
├──
|
| 37 |
-
├──
|
| 38 |
-
│ ├──
|
| 39 |
-
│ ├──
|
| 40 |
-
│
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
├──
|
| 44 |
-
│ └── predict.py # Klasör ve resim bazlı yerel tahmin betiği
|
| 45 |
-
├── utils/
|
| 46 |
-
│ └── dataset.py # PyTorch DataLoader işlemleri ve Augmentation
|
| 47 |
└── README.md
|
| 48 |
```
|
| 49 |
|
|
@@ -56,79 +44,64 @@ wheat_disease_project/
|
|
| 56 |
Proje için sanal bir ortam (virtual environment) oluşturmanız önerilir.
|
| 57 |
|
| 58 |
```bash
|
| 59 |
-
# Repo
|
| 60 |
-
cd
|
| 61 |
|
| 62 |
-
#
|
| 63 |
-
pip install
|
| 64 |
```
|
| 65 |
|
| 66 |
### 2️⃣ Model Eğitimi (Training)
|
| 67 |
|
| 68 |
-
Elinizdeki veri setini `data/train`
|
| 69 |
|
| 70 |
```bash
|
| 71 |
-
python
|
| 72 |
```
|
| 73 |
-
*Not: En iyi ağırlıklar `models/
|
| 74 |
|
| 75 |
---
|
| 76 |
|
| 77 |
## 🌐 API Kullanımı (Inference)
|
| 78 |
|
| 79 |
-
Eğitilmiş modelinizi diğer platformlardan
|
| 80 |
|
| 81 |
```bash
|
| 82 |
-
|
| 83 |
```
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
API çalışmaya başlayınca `http://127.0.0.1:8000/docs` adresinde **Swagger UI** üzerinden test edebilirsiniz.
|
| 87 |
-
|
| 88 |
-
### Önemli Uç Noktalar (Endpoints)
|
| 89 |
-
|
| 90 |
-
- `GET /health` : API'nin ve modelin durumunu kontrol eder.
|
| 91 |
-
- `GET /classes` : Modelin eğiltildiği tüm sınıfların listesini döndürür.
|
| 92 |
-
- `POST /analyze` (veya `/classify`) : Fotoğraf yükleyerek analiz yaptırdığınız ana uç nokta.
|
| 93 |
|
| 94 |
### Örnek API Çıktısı (JSON Response)
|
| 95 |
-
|
| 96 |
```json
|
| 97 |
{
|
| 98 |
-
"
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
"
|
| 102 |
-
"
|
| 103 |
-
{
|
| 104 |
-
"class": "Yellow Rust",
|
| 105 |
-
"score": 0.9821
|
| 106 |
-
},
|
| 107 |
-
{
|
| 108 |
-
"class": "Brown Rust",
|
| 109 |
-
"score": 0.0125
|
| 110 |
-
},
|
| 111 |
-
{
|
| 112 |
-
"class": "Healthy",
|
| 113 |
-
"score": 0.0054
|
| 114 |
-
}
|
| 115 |
-
]
|
| 116 |
-
},
|
| 117 |
-
"quality": {
|
| 118 |
-
"is_valid": true,
|
| 119 |
-
"blur_score": 145.6,
|
| 120 |
-
"warnings": [],
|
| 121 |
-
"rejection_reason": null
|
| 122 |
},
|
| 123 |
-
"
|
| 124 |
-
"
|
| 125 |
-
"
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
}
|
| 129 |
}
|
| 130 |
}
|
| 131 |
```
|
| 132 |
|
| 133 |
---
|
| 134 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🌾 Buğday Hastalık Tespiti ve Çözüm Motoru (Wheat Disease Detection)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
+
Bu proje, buğday yaprağı ve başak görüntülerinden hastalıkları derin öğrenme (*Transfer Learning ile EfficientNet-B3*) tespit eden ve çıkan sonuca göre ziraat standartlarında **çözüm önerileri** üreten uçtan uca (End-to-End) bir çözümdür.
|
| 4 |
|
| 5 |
+
Proje safi bir yapay zeka modelinin ötesinde; bir web API'si sunacak şekilde tasarlanmış, üretime hazır (production-ready) bir altyapıya sahiptir.
|
|
|
|
|
|
|
| 6 |
|
| 7 |
---
|
| 8 |
|
| 9 |
## 🎯 Proje Özellikleri
|
| 10 |
|
| 11 |
+
- **7 Farklı Sınıf Tespiti:** Sağlıklı (Healthy), Sarı Pas (Yellow Rust), Kahverengi Pas (Brown Rust), Sap Pası (Stem Rust), Külleme (Powdery Mildew), Septoria ve Fusaryum.
|
| 12 |
+
- **Model:** Transfer Learning ile pre-trained [EfficientNet-B3](https://arxiv.org/abs/1905.11946) mimarisi. (Hızlı çıkarım süresi ve yüksek doğruluk için seçilmiştir.)
|
| 13 |
+
- **Aşırı Öğrenme Kontrolleri:** Dinamik Learning Rate Scheduler (Cosine Annealing), early-stopping (planlandı) ve zengin Veri Artırma (Data Augmentation).
|
| 14 |
+
- **Zengin API Mimarisi:** Python tabanlı [FastAPI](https://fastapi.tiangolo.com/) kullanılarak yüksek performanslı RESTful entegrasyonu.
|
| 15 |
+
- **Bilgi Tabanı (Knowledge Base):** Modele bağlı basit bir uzman sistem. Tespiti yapılan hastalığa göre kimyasal/doğal tarım çözümleri ve acil aksiyon planları sunar.
|
| 16 |
|
| 17 |
---
|
| 18 |
|
| 19 |
## 📂 Dizin Yapısı / Mimari
|
| 20 |
|
| 21 |
```text
|
| 22 |
+
wheat-project/
|
| 23 |
+
├── api/
|
| 24 |
+
│ ├── main.py # FastAPI uç noktaları (Endpoints)
|
| 25 |
+
│ └── knowledge_base.py # Hastalık -> Çözüm mantık sözlükleri
|
| 26 |
+
├── data/ # İşlenmiş ve ham veri (gitignore'da)
|
| 27 |
+
├── models/ # Eğitilmiş .pth model ağırlık dosyaları
|
| 28 |
+
├── src/
|
| 29 |
+
│ ├── dataset.py # PyTorch DataLoader işlemleri ve Augmentation
|
| 30 |
+
│ ├── model.py # Model tanımlama (EfficientNet Backbone)
|
| 31 |
+
│ ├── train.py # Eğitim (Training & Validation) döngüleri
|
| 32 |
+
│ └── inference.py # API'nin modeli kullanmasını sağlayan tekil tahmin (Prediction) class'ı
|
| 33 |
+
├── Dockerfile # Konteynerizasyon
|
| 34 |
+
├── requirements.txt # Gerekli kütüphaneler
|
|
|
|
|
|
|
|
|
|
| 35 |
└── README.md
|
| 36 |
```
|
| 37 |
|
|
|
|
| 44 |
Proje için sanal bir ortam (virtual environment) oluşturmanız önerilir.
|
| 45 |
|
| 46 |
```bash
|
| 47 |
+
# Repo clonelandıktan sonra ilgili klasöre gidin
|
| 48 |
+
cd wheat-project
|
| 49 |
|
| 50 |
+
# Python paketlerini indirin
|
| 51 |
+
pip install -r requirements.txt
|
| 52 |
```
|
| 53 |
|
| 54 |
### 2️⃣ Model Eğitimi (Training)
|
| 55 |
|
| 56 |
+
Elinizdeki veri setini (Örn: PlantVillage alt setini) `data/processed/train` ve `data/processed/val` altına yerleştirin. Ardından eğitimi başlatın:
|
| 57 |
|
| 58 |
```bash
|
| 59 |
+
python src/train.py --data_dir data/processed --epochs 20 --batch_size 32
|
| 60 |
```
|
| 61 |
+
*Not: En iyi ağırlıklar `models/best_model.pth` içerisine kaydedilecektir.*
|
| 62 |
|
| 63 |
---
|
| 64 |
|
| 65 |
## 🌐 API Kullanımı (Inference)
|
| 66 |
|
| 67 |
+
Eğitilmiş modelinizi diğer platformlardan çağırmak için FastAPI sunucusunu ayağa kaldırın:
|
| 68 |
|
| 69 |
```bash
|
| 70 |
+
uvicorn api.main:app --reload
|
| 71 |
```
|
| 72 |
+
API çalışmaya başlayınca `http://127.0.0.1:8000/docs` adresinde **Swagger UI** üzerinden test edebilirsiniz. `POST /predict/` endpoint'ine bir yaprak görseli yüklemeniz yeterlidir.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
### Örnek API Çıktısı (JSON Response)
|
|
|
|
| 75 |
```json
|
| 76 |
{
|
| 77 |
+
"success": true,
|
| 78 |
+
"latency_seconds": 0.125,
|
| 79 |
+
"prediction": {
|
| 80 |
+
"class": "yellow_rust",
|
| 81 |
+
"confidence": 0.9821
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
},
|
| 83 |
+
"disease_details": {
|
| 84 |
+
"name_tr": "Sarı Pas (Yellow Rust)",
|
| 85 |
+
"description": "Yapraklarda sarı-portakal renginde püstüller...",
|
| 86 |
+
"action": "Acil ilaçlama yapılması tavsiye edilir...",
|
| 87 |
+
"solution": "1. Ruhsatlı triazol veya strobilurin grubu fungisitler kullanın..."
|
|
|
|
| 88 |
}
|
| 89 |
}
|
| 90 |
```
|
| 91 |
|
| 92 |
---
|
| 93 |
+
|
| 94 |
+
## 🐳 Docker ile Çalıştırma
|
| 95 |
+
|
| 96 |
+
Uygulamayı ortam bağımsız (sunucu, cloud vb.) çalıştırmak için tek tuşla Dockerize edebilirsiniz.
|
| 97 |
+
|
| 98 |
+
```bash
|
| 99 |
+
# Docker imajını oluştur
|
| 100 |
+
docker build -t wheat-disease-api .
|
| 101 |
+
|
| 102 |
+
# Konteyneri başlat ve 8000 portuna bağla
|
| 103 |
+
docker run -d -p 8000:8000 --name wheat-api wheat-disease-api
|
| 104 |
+
```
|
| 105 |
+
|
| 106 |
+
---
|
| 107 |
+
**Geliştirici:** (Kendi İsminizi Yazın) | *Bu proje bir Makine Öğrenmesi & Yazılım Mühendisliği portfolyo projesidir.*
|