hllerdgn commited on
Commit
fa44bdc
·
1 Parent(s): f3edbef

Backend restructuring and YOLOv8 removal

Browse files
Files changed (37) hide show
  1. .gitattributes +1 -0
  2. .gitignore +32 -51
  3. Dockerfile → BACKEND/Dockerfile +9 -18
  4. api.py → BACKEND/api.py +80 -37
  5. app.py → BACKEND/app.py +36 -40
  6. config.py → BACKEND/config.py +55 -0
  7. {inference → BACKEND/inference}/__init__.py +0 -0
  8. {inference → BACKEND/inference}/predict.py +22 -0
  9. {knowledge_base → BACKEND/knowledge_base}/__init__.py +0 -0
  10. {knowledge_base → BACKEND/knowledge_base}/diseases.json +0 -0
  11. {models → BACKEND/models}/__init__.py +0 -0
  12. BACKEND/models/__pycache__/__init__.cpython-310.pyc +0 -0
  13. BACKEND/models/__pycache__/__init__.cpython-312.pyc +0 -0
  14. BACKEND/models/__pycache__/model.cpython-310.pyc +0 -0
  15. BACKEND/models/__pycache__/model.cpython-312.pyc +0 -0
  16. BACKEND/models/checkpoints/best_swin_model.pth +3 -0
  17. {models → BACKEND/models}/class_mapping.json +0 -0
  18. {models → BACKEND/models}/evaluate.py +18 -43
  19. {models → BACKEND/models}/model.py +22 -10
  20. {models → BACKEND/models}/training_history.json +0 -0
  21. pipeline.py → BACKEND/pipeline.py +111 -40
  22. preprocessing.py → BACKEND/preprocessing.py +17 -0
  23. requirements.txt → BACKEND/requirements.txt +0 -2
  24. {training → BACKEND/training}/__init__.py +0 -0
  25. BACKEND/training/find_corrupt_images.py +41 -0
  26. BACKEND/training/find_corrupt_images_fast.py +43 -0
  27. {training → BACKEND/training}/train.py +82 -75
  28. {utils → BACKEND/utils}/__init__.py +0 -0
  29. BACKEND/utils/__pycache__/__init__.cpython-310.pyc +0 -0
  30. BACKEND/utils/__pycache__/__init__.cpython-312.pyc +0 -0
  31. BACKEND/utils/__pycache__/dataset.cpython-310.pyc +0 -0
  32. BACKEND/utils/__pycache__/dataset.cpython-312.pyc +0 -0
  33. BACKEND/utils/analyze_dataset.py +50 -0
  34. {utils → BACKEND/utils}/dataset.py +29 -0
  35. {utils → BACKEND/utils}/download_data.py +0 -0
  36. BACKEND/utils/fix_folders.py +58 -0
  37. 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
- # --- Python & Backend (BACKEND/) ---
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
- # Large model weights (Use Git LFS for these)
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 files in models
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
- # (Only if you don't want to share testing scripts)
 
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
- # Sistem bağımlılıklarını yükle (OpenCV için gerekli)
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 -r requirements.txt
 
19
  RUN pip install --no-cache-dir uvicorn gunicorn
20
 
21
- # Proje dosyalarını kopyala
22
  COPY . .
23
 
24
- # Veri ve model klasörlerini oluştur (boş olabilirler)
25
- RUN mkdir -p data models/checkpoints results
26
-
27
- # API portunu aç
28
  EXPOSE 7860
29
 
30
- # API'yi başlat (Hugging Face Spaces varsayılan olarak 7860 portunu bekler)
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 (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
- # UYGULAMA BAŞLATMA
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 yükleniyor...")
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 hazır, API isteklere açık.")
40
  except Exception as e:
41
  startup_error = str(e)
42
- print(f"Pipeline yüklenemedi: {e}")
43
  yield
44
- print("API kapatılıyor.")
 
45
 
46
  app = FastAPI(
47
- title = "Wheat Disease Classification API",
48
- description = "Buğday hastalıklarını sınıflandıran yapay zeka (Swin-T) API'si.",
 
 
 
 
 
 
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
- # YANIT ŞEMALARI
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
- # YARDIMCI
97
  # ============================================================================
98
 
99
  def _check_pipeline():
 
100
  if pipeline is None:
101
  raise HTTPException(
102
  status_code=503,
103
  detail={
104
- "error" : "Pipeline yüklenemedi",
105
  "message": startup_error or "Bilinmeyen hata",
 
106
  },
107
  )
108
 
 
109
  def _validate_image(file: UploadFile):
110
- allowed = {"image/jpeg", "image/jpg", "image/png", "image/bmp", "image/tiff", "image/webp"}
 
 
111
  if file.content_type not in allowed:
112
- raise HTTPException(status_code=415, detail="Desteklenmeyen format")
 
 
 
 
 
113
  MAX_SIZE = 20 * 1024 * 1024
114
  if file.size and file.size > MAX_SIZE:
115
- raise HTTPException(status_code=413, detail="Dosya çok büyük (Maksimum: 20 MB)")
 
 
 
116
 
117
 
118
  # ============================================================================
119
  # ENDPOINTS
120
  # ============================================================================
121
 
122
- @app.get("/")
123
- async def root():
124
- return {
125
- "message": "Wheat Disease Classification API is running!",
126
- "status": "online",
127
- "docs": "/docs",
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
- @app.get("/classes")
 
 
 
 
 
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
- @app.post("/analyze", response_model=AnalyzeResponse)
 
 
 
 
 
 
159
  async def analyze(
160
- file : UploadFile = 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="Boş dosya yüklendi")
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"Analiz hatası: {str(e)}")
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
- from pathlib import Path
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
- # Initialize Pipeline
16
  pipeline = WheatDiseasePipeline(
17
  cls_checkpoint = str(config.MODEL_CHECKPOINT_PATH),
18
  cls_mapping = str(config.MODELS_DIR / "class_mapping.json"),
19
- device = "cpu", # Spaces typically use CPU unless paid
20
- cls_conf = 0.50,
21
  )
22
 
23
  def predict(image):
24
  if image is None:
25
- return "Lütfen bir görüntü yükleyin.", None, None
26
 
27
- # Run pipeline
28
- result = pipeline.run(image)
29
- data = pipeline.result_to_dict(result)
 
30
 
31
- # Format results for Gradio
32
- classification = data["classification"]
33
- top3 = classification["top3_predictions"]
34
-
35
- label_output = {item["class"]: item["score"] for item in top3}
36
-
37
- # Quality info
38
- quality = data["quality"]
39
- quality_text = f"Geçerli: {quality['is_valid']}\nBulanıklık Skoru: {quality['blur_score']}\n"
40
- if quality["warnings"]:
41
- quality_text += f"Uyarılar: {', '.join(quality['warnings'])}"
42
 
43
- return classification["predicted_class"], label_output, quality_text
 
 
 
 
44
 
45
- # Create Gradio Interface
46
- with gr.Blocks(theme=gr.themes.Soft()) as demo:
47
- gr.Markdown("# 🌾 Buğday Hastalık Teşhis Sistemi")
48
- gr.Markdown("Bu uygulama, Swin Transformer (Tiny) mimarisini kullanarak buğday yapraklarındaki hastalıkları tespit eder.")
49
 
50
  with gr.Row():
51
  with gr.Column():
52
- input_img = gr.Image(type="pil", label="Buğday Yaprağı Görüntüsü")
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.Examples(
63
- examples=[], # Add example images if available
64
- inputs=input_img
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
- # Sadece test klasörünü yüklü (train klasörü yoksa get_dataloaders hatası verir)
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, # Zaten kendi eğittiğimiz ağırlıkları yükleyeceğiz
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 "model_state_dict" in checkpoint:
79
- model.load_state_dict(checkpoint["model_state_dict"])
80
- epoch_info = checkpoint.get("epoch", "Bilinmiyor")
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 = 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 = sum(p.numel() for p in self.parameters())
89
  trainable = sum(p.numel() for p in self.parameters() if p.requires_grad)
90
- frozen = total - trainable
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 = model(dummy)
115
- print(f"Output shape: {out.shape}") # [2, 15]
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 = device or ("cuda" if torch.cuda.is_available() else "cpu")
45
- self.cls_conf = 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
- self.preprocessor = ImagePreprocessor(target_size=(640, 640))
 
 
 
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 hazır | device={self.device}")
 
 
60
 
61
- def run(self, source, skip_quality: bool = False) -> PipelineResult:
 
 
 
 
 
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
- return PipelineResult(
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 "Belirsiz",
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 = "Reddedildi",
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("Geçersiz görüntü bytes verisi")
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"Görüntü bulunamadı: {source}")
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("class_mapping.json bulunamadı.")
 
 
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
- print(f"⚠️ UYARI: Checkpoint bulunamadı: {checkpoint_path}")
155
- print("💡 İPUCU: Modeli henüz eğitmediyseniz 'training/train.py' scriptini çalıştırın")
156
- print(" veya hazır 'best_swin_model.pth' dosyasının doğru konumda olduğundan emin olun.")
157
- return model # Boş model döner, tahminler hatalı olur ama API çökmez
158
-
159
- try:
160
- ckpt = torch.load(checkpoint_path, map_location=self.device)
161
- state = ckpt["model_state_dict"] if "model_state_dict" in ckpt else ckpt
162
- model.load_state_dict(state)
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
- precision_score(labels, preds, average="weighted", zero_division=0)
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
- labels, preds, average=None, zero_division=0
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
- get_dataloaders(
188
- data_dir=str(config.DATA_DIR),
189
- batch_size=config.BATCH_SIZE,
190
- num_workers=config.NUM_WORKERS,
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
- early_stopping = (
228
- EarlyStopping(
229
- patience=config.EARLY_STOPPING_PATIENCE,
230
- delta=config.EARLY_STOPPING_DELTA,
231
- logger=logger,
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["model_state_dict"])
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
- "epoch": epoch,
452
- "model_state_dict": model.state_dict(),
453
- "optimizer_state_dict": optimizer.state_dict(),
454
- "scheduler_state_dict": scheduler.state_dict(),
455
- "val_acc": epoch_val_acc,
456
- "history": history,
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
- "📋 Sklearn Classification Report:\n"
502
- + classification_report(
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
- # 🌾 Buğday Hastalık Tespiti API (Wheat Disease Detection)
11
 
12
- Bu proje, buğday yaprağı ve başak görüntülerinden hastalıkları derin öğrenme (**Swin Transformer (Swin-T)**) modeli kullanarak tespit eden ve bir web API'si sunan uçtan uca (End-to-End) bir çözümdür.
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
- - **15 Farklı Sınıf Tespiti:** Sağlıklı durumlar ve buğdayda sık görülen çeşitli mantar/zararlı hastalıkları (Pas, Külleme, Septoria vb.) dâhil olmak üzere 15 farklı sınıfı ayırt eder.
21
- - **Model:** Görüntü işleme alanında son teknoloji olan **Swin Transformer Tiny (Swin-T)** mimarisi.
22
- - **Aşırı Öğrenme Kontrolleri:** Dinamik Learning Rate Scheduler (Cosine Annealing) ve Mixed Precision (AMP) ile optimize edilmiş PyTorch eğitim döngüsü.
23
- - **Yüksek Performanslı API:** Python tabanlı [FastAPI](https://fastapi.tiangolo.com/) kullanılarak geliştirilmiş, hızlı, asenkron RESTful entegrasyonu.
24
- - **Kalite Kontrol (Image Quality Control):** Yüklenen fotoğrafın bulanık (blur) olup olmadığını Laplasyan varyans yöntemiyle tespit ederek hatalı tahminlerin önüne geçer.
25
 
26
  ---
27
 
28
  ## 📂 Dizin Yapısı / Mimari
29
 
30
  ```text
31
- wheat_disease_project/
32
- ├── api.py # FastAPI uç noktaları (Endpoints) ve Pydantic Şemaları
33
- ├── pipeline.py # Görüntü ön işleme ve Swin-T model tahmini (Pipeline)
34
- ── config.py # Tüm hiperparametreler ve klasör yolları ayarları
35
- ├── preprocessing.py # Görüntü bulanıklık kontrolü ve boyutlandırma
36
- ├── data/ # İşlenmiş ve ham veri setleri (gitignore'da)
37
- ├── models/ # class_mapping.json dosyası ve ağırlıklar
38
- │ ├── checkpoints/ # Eğitilmiş Swin-T .pth model ağırlık dosyaları
39
- │ ├── evaluate.py # Modeli test verisi üzerinde değerlendirme betiği
40
- ── model.py # Model tanımlama (Swin Transformer Backbone)
41
- ── training/
42
- │ └── train.py # Eğitim (Training & Validation) döngüleri
43
- ├── inference/
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 klonlandıktan sonra ilgili klasöre gidin
60
- cd wheat_disease_project
61
 
62
- # Gerekli Python kütüphanelerini indirin
63
- pip install torch torchvision numpy opencv-python Pillow fastapi uvicorn pydantic python-multipart scikit-learn
64
  ```
65
 
66
  ### 2️⃣ Model Eğitimi (Training)
67
 
68
- Elinizdeki veri setini `data/train`, `data/valid` ve `data/test` altına (her sınıf için bir klasör olacak şekilde) yerleştirin. Ardından eğitimi başlatın:
69
 
70
  ```bash
71
- python training/train.py
72
  ```
73
- *Not: En iyi ağırlıklar `models/checkpoints/best_swin_model.pth` içerisine otomatik olarak kaydedilecektir.*
74
 
75
  ---
76
 
77
  ## 🌐 API Kullanımı (Inference)
78
 
79
- Eğitilmiş modelinizi diğer platformlardan veya frontend üzerinden çağırmak için FastAPI sunucusunu ayağa kaldırın:
80
 
81
  ```bash
82
- python api.py
83
  ```
84
- *(Varsayılan olarak `http://localhost:8000` adresinde çalışmaya başlar.)*
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
- "classification": {
99
- "predicted_class": "Yellow Rust",
100
- "confidence": 0.9821,
101
- "is_certain": true,
102
- "top3_predictions": [
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
- "meta": {
124
- "processing_time_ms": 125.4,
125
- "image_size": {
126
- "width": 640,
127
- "height": 640
128
- }
129
  }
130
  }
131
  ```
132
 
133
  ---
134
- **Not:** Bu proje, üretim ortamına (Production) alınmaya uygun, temizlenmiş ve optimize edilmiş bir kod tabanına sahiptir. Büyük model ağırlıkları (335MB+) `.gitignore` kapsamında olduğundan GitHub'a yüklenmez. Canlı sunucuya (VPS, AWS vb.) aktarırken model dosyalarını (`best_swin_model.pth`) manuel olarak veya S3 gibi servisler aracılığıyla sunucuya çekmeniz gerekmektedir.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 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.*