Harshith Reddy commited on
Commit
5ee6658
·
1 Parent(s): 716e195

backend+modell

Browse files
Dockerfile ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9
2
+
3
+ RUN useradd -m -u 1000 user
4
+
5
+ USER user
6
+
7
+ ENV PATH="/home/user/.local/bin:$PATH"
8
+
9
+ WORKDIR /app
10
+
11
+ COPY --chown=user ./requirements.txt requirements.txt
12
+
13
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
14
+
15
+ COPY --chown=user . /app
16
+
17
+ ENV MODEL_PATH=/app/files/checkpoint.pth
18
+ ENV MODEL_DEVICE=cuda
19
+ ENV LOG_LEVEL=INFO
20
+
21
+ EXPOSE 7860
22
+
23
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
24
+
app.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from app.core.config import settings
4
+ from app.core.logging_config import setup_logging
5
+ from app.api.v1.router import api_router
6
+ from app.core.exceptions import setup_exception_handlers
7
+ from app.core.startup import startup_event, shutdown_event
8
+
9
+ setup_logging()
10
+
11
+ app = FastAPI(
12
+ title=settings.APP_NAME,
13
+ version=settings.APP_VERSION,
14
+ description=settings.APP_DESCRIPTION
15
+ )
16
+
17
+ app.add_middleware(
18
+ CORSMiddleware,
19
+ allow_origins=settings.ALLOWED_ORIGINS,
20
+ allow_credentials=True,
21
+ allow_methods=["*"],
22
+ allow_headers=["*"],
23
+ )
24
+
25
+ setup_exception_handlers(app)
26
+ app.include_router(api_router, prefix="/api/v1")
27
+
28
+ @app.on_event("startup")
29
+ async def startup():
30
+ await startup_event(app)
31
+
32
+ @app.on_event("shutdown")
33
+ async def shutdown():
34
+ await shutdown_event(app)
35
+
36
+ @app.get("/")
37
+ async def root():
38
+ return {"status": "healthy", "service": settings.APP_NAME}
39
+
40
+ @app.get("/health")
41
+ async def health_check():
42
+ return {"status": "healthy"}
43
+
app/__init__.py ADDED
File without changes
app/api/__init__.py ADDED
File without changes
app/api/v1/__init__.py ADDED
File without changes
app/api/v1/endpoints/__init__.py ADDED
File without changes
app/api/v1/endpoints/health.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, status
2
+ from fastapi.responses import JSONResponse
3
+ import logging
4
+ from app.services.model_loader import model_loader
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ router = APIRouter()
9
+
10
+ @router.get("/")
11
+ async def health_check():
12
+ try:
13
+ model_loaded = model_loader.model is not None
14
+ return JSONResponse(
15
+ status_code=status.HTTP_200_OK,
16
+ content={
17
+ "status": "healthy",
18
+ "model_loaded": model_loaded
19
+ }
20
+ )
21
+ except Exception as e:
22
+ logger.error(f"Health check error: {str(e)}")
23
+ return JSONResponse(
24
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
25
+ content={
26
+ "status": "unhealthy",
27
+ "error": str(e)
28
+ }
29
+ )
30
+
31
+ @router.get("/model")
32
+ async def model_status():
33
+ try:
34
+ model_loaded = model_loader.model is not None
35
+ device = str(model_loader.device) if model_loader.device else None
36
+
37
+ return JSONResponse(
38
+ status_code=status.HTTP_200_OK,
39
+ content={
40
+ "model_loaded": model_loaded,
41
+ "device": device
42
+ }
43
+ )
44
+ except Exception as e:
45
+ logger.error(f"Model status error: {str(e)}")
46
+ return JSONResponse(
47
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
48
+ content={"error": str(e)}
49
+ )
50
+
app/api/v1/endpoints/inference.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, UploadFile, File, HTTPException, status
2
+ from fastapi.responses import Response
3
+ import logging
4
+ from app.services.inference_service import inference_service
5
+ from app.services.image_processor import image_processor
6
+ from app.core.exceptions import ImageProcessingError, InferenceError
7
+ from app.core.config import settings
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ router = APIRouter()
12
+
13
+ @router.post("/predict")
14
+ async def predict_segmentation(
15
+ file: UploadFile = File(...)
16
+ ):
17
+ try:
18
+ if not file.content_type or not file.content_type.startswith("image/"):
19
+ raise HTTPException(
20
+ status_code=status.HTTP_400_BAD_REQUEST,
21
+ detail="File must be an image"
22
+ )
23
+
24
+ image_bytes = await file.read()
25
+ image_processor.validate_image(image_bytes)
26
+
27
+ image = image_processor.decode_image(image_bytes)
28
+ image_tensor = image_processor.preprocess(image)
29
+
30
+ result = inference_service.predict(image_tensor)
31
+
32
+ return Response(
33
+ content=result["mask"],
34
+ media_type="image/jpeg",
35
+ headers={
36
+ "X-Inference-Time": str(result["inference_time"]),
37
+ "X-Image-Shape": str(result["image_shape"])
38
+ }
39
+ )
40
+
41
+ except ImageProcessingError as e:
42
+ logger.error(f"Image processing error: {str(e)}")
43
+ raise HTTPException(
44
+ status_code=status.HTTP_400_BAD_REQUEST,
45
+ detail=str(e)
46
+ )
47
+ except InferenceError as e:
48
+ logger.error(f"Inference error: {str(e)}")
49
+ raise HTTPException(
50
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
51
+ detail=str(e)
52
+ )
53
+ except Exception as e:
54
+ logger.exception(f"Unexpected error: {str(e)}")
55
+ raise HTTPException(
56
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
57
+ detail="Internal server error"
58
+ )
59
+
60
+ @router.post("/predict-with-metadata")
61
+ async def predict_with_metadata(
62
+ file: UploadFile = File(...)
63
+ ):
64
+ try:
65
+ if not file.content_type or not file.content_type.startswith("image/"):
66
+ raise HTTPException(
67
+ status_code=status.HTTP_400_BAD_REQUEST,
68
+ detail="File must be an image"
69
+ )
70
+
71
+ image_bytes = await file.read()
72
+ image_processor.validate_image(image_bytes)
73
+
74
+ image = image_processor.decode_image(image_bytes)
75
+ image_tensor = image_processor.preprocess(image)
76
+
77
+ result = inference_service.predict(image_tensor)
78
+
79
+ from app.schemas.response_schemas import InferenceResponse
80
+ return InferenceResponse(**result)
81
+
82
+ except ImageProcessingError as e:
83
+ logger.error(f"Image processing error: {str(e)}")
84
+ raise HTTPException(
85
+ status_code=status.HTTP_400_BAD_REQUEST,
86
+ detail=str(e)
87
+ )
88
+ except InferenceError as e:
89
+ logger.error(f"Inference error: {str(e)}")
90
+ raise HTTPException(
91
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
92
+ detail=str(e)
93
+ )
94
+ except Exception as e:
95
+ logger.exception(f"Unexpected error: {str(e)}")
96
+ raise HTTPException(
97
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
98
+ detail="Internal server error"
99
+ )
100
+
app/api/v1/router.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, UploadFile, File, HTTPException
2
+ from fastapi.responses import Response
3
+ from app.api.v1.endpoints import inference, health
4
+ from app.core.config import settings
5
+
6
+ api_router = APIRouter()
7
+
8
+ api_router.include_router(
9
+ inference.router,
10
+ prefix="/inference",
11
+ tags=["inference"]
12
+ )
13
+
14
+ api_router.include_router(
15
+ health.router,
16
+ prefix="/health",
17
+ tags=["health"]
18
+ )
19
+
app/core/config.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from pydantic_settings import BaseSettings
3
+ from typing import List
4
+
5
+ class Settings(BaseSettings):
6
+ APP_NAME: str = "Dentimap API"
7
+ APP_VERSION: str = "1.0.0"
8
+ APP_DESCRIPTION: str = "Medical Image Segmentation API for Dental Mapping"
9
+
10
+ MODEL_PATH: str = os.getenv("MODEL_PATH", "/app/files/checkpoint.pth")
11
+ MODEL_DEVICE: str = os.getenv("MODEL_DEVICE", "cuda")
12
+ NUM_CLASSES: int = 4
13
+ IMAGE_WIDTH: int = 512
14
+ IMAGE_HEIGHT: int = 256
15
+
16
+ MAX_IMAGE_SIZE: int = 10 * 1024 * 1024
17
+ ALLOWED_EXTENSIONS: List[str] = [".jpg", ".jpeg", ".png", ".bmp"]
18
+
19
+ ALLOWED_ORIGINS: List[str] = ["*"]
20
+
21
+ LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO")
22
+
23
+ COLORMAP: List[List[int]] = [
24
+ [0, 0, 0],
25
+ [255, 243, 28],
26
+ [255, 52, 255],
27
+ [70, 74, 255],
28
+ ]
29
+
30
+ CLASS_NAMES: List[str] = [
31
+ "background",
32
+ "class_1",
33
+ "class_2",
34
+ "class_3"
35
+ ]
36
+
37
+ class Config:
38
+ env_file = ".env"
39
+ case_sensitive = True
40
+
41
+ settings = Settings()
42
+
app/core/exceptions.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, Request, status
2
+ from fastapi.responses import JSONResponse
3
+ from fastapi.exceptions import RequestValidationError
4
+ from starlette.exceptions import HTTPException as StarletteHTTPException
5
+ import logging
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ class InferenceError(Exception):
10
+ pass
11
+
12
+ class ModelLoadError(Exception):
13
+ pass
14
+
15
+ class ImageProcessingError(Exception):
16
+ pass
17
+
18
+ async def validation_exception_handler(request: Request, exc: RequestValidationError):
19
+ logger.warning(f"Validation error: {exc.errors()}")
20
+ return JSONResponse(
21
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
22
+ content={"detail": exc.errors(), "body": exc.body}
23
+ )
24
+
25
+ async def http_exception_handler(request: Request, exc: StarletteHTTPException):
26
+ logger.warning(f"HTTP exception: {exc.status_code} - {exc.detail}")
27
+ return JSONResponse(
28
+ status_code=exc.status_code,
29
+ content={"detail": exc.detail}
30
+ )
31
+
32
+ async def inference_exception_handler(request: Request, exc: InferenceError):
33
+ logger.error(f"Inference error: {str(exc)}")
34
+ return JSONResponse(
35
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
36
+ content={"detail": f"Inference error: {str(exc)}"}
37
+ )
38
+
39
+ async def model_load_exception_handler(request: Request, exc: ModelLoadError):
40
+ logger.error(f"Model load error: {str(exc)}")
41
+ return JSONResponse(
42
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
43
+ content={"detail": f"Model load error: {str(exc)}"}
44
+ )
45
+
46
+ async def image_processing_exception_handler(request: Request, exc: ImageProcessingError):
47
+ logger.error(f"Image processing error: {str(exc)}")
48
+ return JSONResponse(
49
+ status_code=status.HTTP_400_BAD_REQUEST,
50
+ content={"detail": f"Image processing error: {str(exc)}"}
51
+ )
52
+
53
+ async def general_exception_handler(request: Request, exc: Exception):
54
+ logger.exception(f"Unhandled exception: {str(exc)}")
55
+ return JSONResponse(
56
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
57
+ content={"detail": "Internal server error"}
58
+ )
59
+
60
+ def setup_exception_handlers(app: FastAPI):
61
+ app.add_exception_handler(RequestValidationError, validation_exception_handler)
62
+ app.add_exception_handler(StarletteHTTPException, http_exception_handler)
63
+ app.add_exception_handler(InferenceError, inference_exception_handler)
64
+ app.add_exception_handler(ModelLoadError, model_load_exception_handler)
65
+ app.add_exception_handler(ImageProcessingError, image_processing_exception_handler)
66
+ app.add_exception_handler(Exception, general_exception_handler)
67
+
app/core/logging_config.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import sys
3
+ from app.core.config import settings
4
+
5
+ def setup_logging():
6
+ logging.basicConfig(
7
+ level=getattr(logging, settings.LOG_LEVEL.upper()),
8
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
9
+ handlers=[
10
+ logging.StreamHandler(sys.stdout)
11
+ ]
12
+ )
13
+
14
+ logging.getLogger("uvicorn").setLevel(logging.INFO)
15
+ logging.getLogger("fastapi").setLevel(logging.INFO)
16
+
app/core/startup.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from fastapi import FastAPI
3
+ from app.services.model_loader import model_loader
4
+ from app.services.inference_service import inference_service
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ async def startup_event(app: FastAPI):
9
+ try:
10
+ logger.info("Starting up application...")
11
+ model_loader.load_model()
12
+ inference_service.initialize()
13
+ logger.info("Application startup complete")
14
+ except Exception as e:
15
+ logger.error(f"Startup error: {str(e)}")
16
+ raise
17
+
18
+ async def shutdown_event(app: FastAPI):
19
+ logger.info("Shutting down application...")
20
+
app/models/unet_model.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+
4
+ class ConvBlock(nn.Module):
5
+ def __init__(self, in_c, out_c):
6
+ super().__init__()
7
+ self.conv = nn.Sequential(
8
+ nn.Conv2d(in_c, out_c, kernel_size=3, padding=1),
9
+ nn.BatchNorm2d(out_c),
10
+ nn.ReLU(inplace=True),
11
+ nn.Conv2d(out_c, out_c, kernel_size=3, padding=1),
12
+ nn.BatchNorm2d(out_c),
13
+ nn.ReLU(inplace=True)
14
+ )
15
+
16
+ def forward(self, inputs):
17
+ return self.conv(inputs)
18
+
19
+ class EncoderBlock(nn.Module):
20
+ def __init__(self, in_c, out_c):
21
+ super().__init__()
22
+ self.conv = ConvBlock(in_c, out_c)
23
+ self.pool = nn.MaxPool2d((2, 2))
24
+
25
+ def forward(self, inputs):
26
+ x = self.conv(inputs)
27
+ p = self.pool(x)
28
+ return x, p
29
+
30
+ class DecoderBlock(nn.Module):
31
+ def __init__(self, in_c, out_c):
32
+ super().__init__()
33
+ self.up = nn.ConvTranspose2d(in_c, out_c, kernel_size=2, stride=2, padding=0)
34
+ self.conv = ConvBlock(out_c+out_c, out_c)
35
+
36
+ def forward(self, inputs, skip):
37
+ x = self.up(inputs)
38
+ x = torch.cat([x, skip], axis=1)
39
+ x = self.conv(x)
40
+ return x
41
+
42
+ class BuildUNet(nn.Module):
43
+ def __init__(self, num_classes=4):
44
+ super().__init__()
45
+ self.e1 = EncoderBlock(3, 64)
46
+ self.e2 = EncoderBlock(64, 128)
47
+ self.e3 = EncoderBlock(128, 256)
48
+ self.e4 = EncoderBlock(256, 512)
49
+ self.b = ConvBlock(512, 1024)
50
+ self.d1 = DecoderBlock(1024, 512)
51
+ self.d2 = DecoderBlock(512, 256)
52
+ self.d3 = DecoderBlock(256, 128)
53
+ self.d4 = DecoderBlock(128, 64)
54
+ self.outputs = nn.Conv2d(64, num_classes, kernel_size=1, padding=0)
55
+
56
+ def forward(self, inputs):
57
+ s1, p1 = self.e1(inputs)
58
+ s2, p2 = self.e2(p1)
59
+ s3, p3 = self.e3(p2)
60
+ s4, p4 = self.e4(p3)
61
+ b = self.b(p4)
62
+ d1 = self.d1(b, s4)
63
+ d2 = self.d2(d1, s3)
64
+ d3 = self.d3(d2, s2)
65
+ d4 = self.d4(d3, s1)
66
+ outputs = self.outputs(d4)
67
+ return outputs
68
+
app/schemas/request_schemas.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from typing import Optional
3
+
4
+ class InferenceRequest(BaseModel):
5
+ image: bytes = Field(..., description="Image file as bytes")
6
+ return_overlay: Optional[bool] = Field(False, description="Return overlay visualization")
7
+ format: Optional[str] = Field(".jpg", description="Output format (.jpg or .png)")
8
+
app/schemas/response_schemas.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+ from typing import Dict
3
+
4
+ class ClassDistribution(BaseModel):
5
+ pixel_count: int
6
+ percentage: float
7
+
8
+ class InferenceResponse(BaseModel):
9
+ mask: bytes
10
+ inference_time: float
11
+ class_distribution: Dict[str, ClassDistribution]
12
+ image_shape: tuple
13
+
14
+ class HealthResponse(BaseModel):
15
+ status: str
16
+ model_loaded: bool
17
+
app/services/image_processor.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ import torch
4
+ from typing import Tuple
5
+ from app.core.config import settings
6
+ from app.core.exceptions import ImageProcessingError
7
+ import logging
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ class ImageProcessor:
12
+ def __init__(self):
13
+ self.target_size = (settings.IMAGE_WIDTH, settings.IMAGE_HEIGHT)
14
+ self.max_size = settings.MAX_IMAGE_SIZE
15
+
16
+ def validate_image(self, image_bytes: bytes) -> None:
17
+ if len(image_bytes) > self.max_size:
18
+ raise ImageProcessingError(f"Image size exceeds maximum allowed size of {self.max_size} bytes")
19
+
20
+ def decode_image(self, image_bytes: bytes) -> np.ndarray:
21
+ try:
22
+ nparr = np.frombuffer(image_bytes, np.uint8)
23
+ image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
24
+ if image is None:
25
+ raise ImageProcessingError("Failed to decode image")
26
+ return image
27
+ except Exception as e:
28
+ raise ImageProcessingError(f"Error decoding image: {str(e)}")
29
+
30
+ def preprocess(self, image: np.ndarray) -> torch.Tensor:
31
+ try:
32
+ resized = cv2.resize(image, self.target_size)
33
+ normalized = resized.astype(np.float32) / 255.0
34
+ transposed = np.transpose(normalized, (2, 0, 1))
35
+ tensor = torch.from_numpy(transposed).unsqueeze(0)
36
+ return tensor
37
+ except Exception as e:
38
+ raise ImageProcessingError(f"Error preprocessing image: {str(e)}")
39
+
40
+ def postprocess_mask(self, mask: np.ndarray) -> np.ndarray:
41
+ h, w = mask.shape
42
+ output = np.zeros((h, w, 3), dtype=np.uint8)
43
+ for idx, color in enumerate(settings.COLORMAP):
44
+ output[mask == idx] = color
45
+ return output
46
+
47
+ def encode_image(self, image: np.ndarray, format: str = ".jpg") -> bytes:
48
+ try:
49
+ encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 95]
50
+ if format == ".png":
51
+ encode_param = [int(cv2.IMWRITE_PNG_COMPRESSION), 9]
52
+
53
+ success, encoded = cv2.imencode(format, image, encode_param)
54
+ if not success:
55
+ raise ImageProcessingError("Failed to encode image")
56
+ return encoded.tobytes()
57
+ except Exception as e:
58
+ raise ImageProcessingError(f"Error encoding image: {str(e)}")
59
+
60
+ image_processor = ImageProcessor()
61
+
app/services/inference_service.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import numpy as np
3
+ import time
4
+ import logging
5
+ from app.services.model_loader import model_loader
6
+ from app.services.image_processor import image_processor
7
+ from app.core.exceptions import InferenceError
8
+ from app.core.config import settings
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ class InferenceService:
13
+ def __init__(self):
14
+ self.model = None
15
+ self.device = None
16
+
17
+ def initialize(self):
18
+ if self.model is None:
19
+ model_loader.load_model()
20
+ self.model = model_loader.get_model()
21
+ self.device = model_loader.get_device()
22
+ logger.info("Inference service initialized")
23
+
24
+ def predict(self, image_tensor: torch.Tensor) -> dict:
25
+ if self.model is None:
26
+ self.initialize()
27
+
28
+ try:
29
+ start_time = time.time()
30
+
31
+ image_tensor = image_tensor.to(self.device)
32
+
33
+ with torch.no_grad():
34
+ logits = self.model(image_tensor)
35
+ probs = torch.softmax(logits, dim=1)
36
+ pred_mask = torch.argmax(probs, dim=1)
37
+ pred_mask = pred_mask.squeeze(0).cpu().numpy().astype(np.uint8)
38
+
39
+ inference_time = time.time() - start_time
40
+
41
+ colored_mask = image_processor.postprocess_mask(pred_mask)
42
+
43
+ mask_bytes = image_processor.encode_image(colored_mask)
44
+
45
+ class_counts = self._calculate_class_distribution(pred_mask)
46
+
47
+ return {
48
+ "mask": mask_bytes,
49
+ "inference_time": round(inference_time, 4),
50
+ "class_distribution": class_counts,
51
+ "image_shape": pred_mask.shape
52
+ }
53
+
54
+ except Exception as e:
55
+ logger.error(f"Inference error: {str(e)}")
56
+ raise InferenceError(f"Inference failed: {str(e)}")
57
+
58
+ def _calculate_class_distribution(self, mask: np.ndarray) -> dict:
59
+ unique, counts = np.unique(mask, return_counts=True)
60
+ total_pixels = mask.size
61
+ distribution = {}
62
+ for class_idx, count in zip(unique, counts):
63
+ class_name = settings.CLASS_NAMES[class_idx] if class_idx < len(settings.CLASS_NAMES) else f"class_{class_idx}"
64
+ distribution[class_name] = {
65
+ "pixel_count": int(count),
66
+ "percentage": round((count / total_pixels) * 100, 2)
67
+ }
68
+ return distribution
69
+
70
+ inference_service = InferenceService()
71
+
app/services/model_loader.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import logging
3
+ from pathlib import Path
4
+ from app.models.unet_model import BuildUNet
5
+ from app.core.config import settings
6
+ from app.core.exceptions import ModelLoadError
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ class ModelLoader:
11
+ def __init__(self):
12
+ self.model = None
13
+ self.device = None
14
+ self._load_device()
15
+
16
+ def _load_device(self):
17
+ if settings.MODEL_DEVICE == "cuda" and torch.cuda.is_available():
18
+ self.device = torch.device("cuda")
19
+ logger.info(f"Using GPU: {torch.cuda.get_device_name(0)}")
20
+ else:
21
+ self.device = torch.device("cpu")
22
+ logger.info("Using CPU")
23
+
24
+ def load_model(self):
25
+ try:
26
+ model_path = Path(settings.MODEL_PATH)
27
+ if not model_path.exists():
28
+ raise ModelLoadError(f"Model file not found at {settings.MODEL_PATH}")
29
+
30
+ logger.info(f"Loading model from {settings.MODEL_PATH}")
31
+ self.model = BuildUNet(num_classes=settings.NUM_CLASSES)
32
+
33
+ checkpoint = torch.load(model_path, map_location=self.device)
34
+ if isinstance(checkpoint, dict) and "model_state_dict" in checkpoint:
35
+ self.model.load_state_dict(checkpoint["model_state_dict"])
36
+ else:
37
+ self.model.load_state_dict(checkpoint)
38
+
39
+ self.model.to(self.device)
40
+ self.model.eval()
41
+ logger.info("Model loaded successfully")
42
+
43
+ except Exception as e:
44
+ logger.error(f"Failed to load model: {str(e)}")
45
+ raise ModelLoadError(f"Failed to load model: {str(e)}")
46
+
47
+ def get_model(self):
48
+ if self.model is None:
49
+ raise ModelLoadError("Model not loaded. Call load_model() first.")
50
+ return self.model
51
+
52
+ def get_device(self):
53
+ return self.device
54
+
55
+ model_loader = ModelLoader()
56
+
app/utils/__init__.py ADDED
File without changes
app/utils/helpers.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from pathlib import Path
3
+
4
+ def ensure_directory(path: str):
5
+ Path(path).mkdir(parents=True, exist_ok=True)
6
+
7
+ def get_file_extension(filename: str) -> str:
8
+ return os.path.splitext(filename)[1].lower()
9
+
10
+ def is_valid_image_extension(extension: str) -> bool:
11
+ from app.core.config import settings
12
+ return extension in settings.ALLOWED_EXTENSIONS
13
+
files/checkpoint.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:78915f441ab9c40b36fa65924e3dc94643af8b57811f71bd364aee37a6dcf7b8
3
+ size 372687676
files/score.csv ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ Class,F1,Jaccard
2
+ background ,0.99707,0.99418
3
+ class_1 ,0.50255,0.37771
4
+ class_2 ,0.20351,0.15947
5
+ class_3 ,0.24261,0.21064
6
+ Mean ,0.31622,0.24927
files/train.log ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ INFO:root:Image Size: (512, 256) - Batch Size: 16 - LR: 0.0001 - Epochs: 500 - Num Classes: 4 - Early Stopping Patience: 50 - Checkpoint Path: files/checkpoint.pth
2
+ INFO:root:Dataset Size: Train: 997 - Valid: 125 - Test: 125
3
+ INFO:root:Optimizer: Adam - Loss: Dice + Cross Entropy Loss
4
+ INFO:root:[01/500] | Epoch Time: 0m 45s - Train Loss: 1.7755 - Val. Loss: 1.5772
5
+ INFO:root:[02/500] | Epoch Time: 0m 43s - Train Loss: 1.4858 - Val. Loss: 1.4231
6
+ INFO:root:[03/500] | Epoch Time: 0m 45s - Train Loss: 1.3734 - Val. Loss: 1.3292
7
+ INFO:root:[04/500] | Epoch Time: 0m 45s - Train Loss: 1.2654 - Val. Loss: 1.2256
8
+ INFO:root:[05/500] | Epoch Time: 0m 43s - Train Loss: 1.1833 - Val. Loss: 1.1446
9
+ INFO:root:[06/500] | Epoch Time: 0m 44s - Train Loss: 1.1178 - Val. Loss: 1.0963
10
+ INFO:root:[07/500] | Epoch Time: 0m 45s - Train Loss: 1.0638 - Val. Loss: 1.0379
11
+ INFO:root:[08/500] | Epoch Time: 0m 44s - Train Loss: 1.0216 - Val. Loss: 1.0046
12
+ INFO:root:[09/500] | Epoch Time: 0m 45s - Train Loss: 0.9865 - Val. Loss: 0.9672
13
+ INFO:root:[10/500] | Epoch Time: 0m 45s - Train Loss: 0.9562 - Val. Loss: 0.9460
14
+ INFO:root:[11/500] | Epoch Time: 0m 44s - Train Loss: 0.9275 - Val. Loss: 0.9191
15
+ INFO:root:[12/500] | Epoch Time: 0m 44s - Train Loss: 0.8998 - Val. Loss: 0.8899
16
+ INFO:root:[13/500] | Epoch Time: 0m 45s - Train Loss: 0.8679 - Val. Loss: 0.8517
17
+ INFO:root:[14/500] | Epoch Time: 0m 44s - Train Loss: 0.8437 - Val. Loss: 0.8339
18
+ INFO:root:[15/500] | Epoch Time: 0m 44s - Train Loss: 0.8209 - Val. Loss: 0.8063
19
+ INFO:root:[16/500] | Epoch Time: 0m 45s - Train Loss: 0.8010 - Val. Loss: 0.8048
20
+ INFO:root:[17/500] | Epoch Time: 0m 44s - Train Loss: 0.7818 - Val. Loss: 0.7597
21
+ INFO:root:[18/500] | Epoch Time: 0m 44s - Train Loss: 0.7642 - Val. Loss: 0.7592
22
+ INFO:root:[19/500] | Epoch Time: 0m 44s - Train Loss: 0.7497 - Val. Loss: 0.7385
23
+ INFO:root:[20/500] | Epoch Time: 0m 43s - Train Loss: 0.7323 - Val. Loss: 0.7252
24
+ INFO:root:[21/500] | Epoch Time: 0m 44s - Train Loss: 0.7137 - Val. Loss: 0.7007
25
+ INFO:root:[22/500] | Epoch Time: 0m 45s - Train Loss: 0.6846 - Val. Loss: 0.6937
26
+ INFO:root:[23/500] | Epoch Time: 0m 43s - Train Loss: 0.6579 - Val. Loss: 0.6572
27
+ INFO:root:[24/500] | Epoch Time: 0m 43s - Train Loss: 0.6364 - Val. Loss: 0.6567
28
+ INFO:root:[25/500] | Epoch Time: 0m 44s - Train Loss: 0.6198 - Val. Loss: 0.6438
29
+ INFO:root:[26/500] | Epoch Time: 0m 44s - Train Loss: 0.5977 - Val. Loss: 0.6061
30
+ INFO:root:[27/500] | Epoch Time: 0m 44s - Train Loss: 0.5792 - Val. Loss: 0.6231
31
+ INFO:root:[28/500] | Epoch Time: 0m 44s - Train Loss: 0.5741 - Val. Loss: 0.5708
32
+ INFO:root:[29/500] | Epoch Time: 0m 45s - Train Loss: 0.5648 - Val. Loss: 0.5588
33
+ INFO:root:[30/500] | Epoch Time: 0m 44s - Train Loss: 0.5409 - Val. Loss: 0.5468
34
+ INFO:root:[31/500] | Epoch Time: 0m 44s - Train Loss: 0.5281 - Val. Loss: 0.5506
35
+ INFO:root:[32/500] | Epoch Time: 0m 45s - Train Loss: 0.5159 - Val. Loss: 0.5223
36
+ INFO:root:[33/500] | Epoch Time: 0m 46s - Train Loss: 0.5047 - Val. Loss: 0.5157
37
+ INFO:root:[34/500] | Epoch Time: 0m 44s - Train Loss: 0.4786 - Val. Loss: 0.4986
38
+ INFO:root:[35/500] | Epoch Time: 0m 43s - Train Loss: 0.4561 - Val. Loss: 0.4820
39
+ INFO:root:[36/500] | Epoch Time: 0m 43s - Train Loss: 0.4309 - Val. Loss: 0.4508
40
+ INFO:root:[37/500] | Epoch Time: 0m 44s - Train Loss: 0.4149 - Val. Loss: 0.4227
41
+ INFO:root:[38/500] | Epoch Time: 0m 43s - Train Loss: 0.3990 - Val. Loss: 0.4630
42
+ INFO:root:[39/500] | Epoch Time: 0m 43s - Train Loss: 0.3912 - Val. Loss: 0.4266
43
+ INFO:root:[40/500] | Epoch Time: 0m 44s - Train Loss: 0.3742 - Val. Loss: 0.4057
44
+ INFO:root:[41/500] | Epoch Time: 0m 43s - Train Loss: 0.3660 - Val. Loss: 0.4384
45
+ INFO:root:[42/500] | Epoch Time: 0m 44s - Train Loss: 0.3825 - Val. Loss: 0.3945
46
+ INFO:root:[43/500] | Epoch Time: 0m 44s - Train Loss: 0.3646 - Val. Loss: 0.3884
47
+ INFO:root:[44/500] | Epoch Time: 0m 45s - Train Loss: 0.3517 - Val. Loss: 0.3977
48
+ INFO:root:[45/500] | Epoch Time: 0m 44s - Train Loss: 0.3455 - Val. Loss: 0.3796
49
+ INFO:root:[46/500] | Epoch Time: 0m 44s - Train Loss: 0.3314 - Val. Loss: 0.3821
50
+ INFO:root:[47/500] | Epoch Time: 0m 44s - Train Loss: 0.3292 - Val. Loss: 0.3628
51
+ INFO:root:[48/500] | Epoch Time: 0m 44s - Train Loss: 0.3312 - Val. Loss: 0.4099
52
+ INFO:root:[49/500] | Epoch Time: 0m 48s - Train Loss: 0.3316 - Val. Loss: 0.3946
53
+ INFO:root:[50/500] | Epoch Time: 0m 46s - Train Loss: 0.3257 - Val. Loss: 0.3603
54
+ INFO:root:[51/500] | Epoch Time: 0m 46s - Train Loss: 0.3187 - Val. Loss: 0.3697
55
+ INFO:root:[52/500] | Epoch Time: 0m 45s - Train Loss: 0.3075 - Val. Loss: 0.3591
56
+ INFO:root:[53/500] | Epoch Time: 0m 45s - Train Loss: 0.3054 - Val. Loss: 0.3669
57
+ INFO:root:[54/500] | Epoch Time: 0m 44s - Train Loss: 0.3056 - Val. Loss: 0.3759
58
+ INFO:root:[55/500] | Epoch Time: 0m 45s - Train Loss: 0.2982 - Val. Loss: 0.3600
59
+ INFO:root:[56/500] | Epoch Time: 0m 46s - Train Loss: 0.3139 - Val. Loss: 0.3601
60
+ INFO:root:[57/500] | Epoch Time: 0m 45s - Train Loss: 0.2982 - Val. Loss: 0.3492
61
+ INFO:root:[58/500] | Epoch Time: 0m 44s - Train Loss: 0.2901 - Val. Loss: 0.3509
62
+ INFO:root:[59/500] | Epoch Time: 0m 45s - Train Loss: 0.2882 - Val. Loss: 0.3435
63
+ INFO:root:[60/500] | Epoch Time: 0m 45s - Train Loss: 0.3006 - Val. Loss: 0.3488
64
+ INFO:root:[61/500] | Epoch Time: 0m 45s - Train Loss: 0.2909 - Val. Loss: 0.3451
65
+ INFO:root:[62/500] | Epoch Time: 0m 45s - Train Loss: 0.2773 - Val. Loss: 0.3355
66
+ INFO:root:[63/500] | Epoch Time: 0m 45s - Train Loss: 0.2825 - Val. Loss: 0.3429
67
+ INFO:root:[64/500] | Epoch Time: 0m 45s - Train Loss: 0.2780 - Val. Loss: 0.3552
68
+ INFO:root:[65/500] | Epoch Time: 0m 45s - Train Loss: 0.2746 - Val. Loss: 0.3410
69
+ INFO:root:[66/500] | Epoch Time: 0m 44s - Train Loss: 0.2721 - Val. Loss: 0.3723
70
+ INFO:root:[67/500] | Epoch Time: 0m 45s - Train Loss: 0.2732 - Val. Loss: 0.3285
71
+ INFO:root:[68/500] | Epoch Time: 0m 46s - Train Loss: 0.2609 - Val. Loss: 0.3399
72
+ INFO:root:[69/500] | Epoch Time: 0m 45s - Train Loss: 0.2624 - Val. Loss: 0.3225
73
+ INFO:root:[70/500] | Epoch Time: 0m 45s - Train Loss: 0.2581 - Val. Loss: 0.3274
74
+ INFO:root:[71/500] | Epoch Time: 0m 46s - Train Loss: 0.2552 - Val. Loss: 0.3262
75
+ INFO:root:[72/500] | Epoch Time: 0m 46s - Train Loss: 0.2590 - Val. Loss: 0.3256
76
+ INFO:root:[73/500] | Epoch Time: 0m 45s - Train Loss: 0.2525 - Val. Loss: 0.3265
77
+ INFO:root:[74/500] | Epoch Time: 0m 44s - Train Loss: 0.2468 - Val. Loss: 0.3360
78
+ INFO:root:[75/500] | Epoch Time: 0m 44s - Train Loss: 0.2521 - Val. Loss: 0.3264
79
+ INFO:root:[76/500] | Epoch Time: 0m 45s - Train Loss: 0.2282 - Val. Loss: 0.3195
80
+ INFO:root:[77/500] | Epoch Time: 0m 45s - Train Loss: 0.2253 - Val. Loss: 0.3177
81
+ INFO:root:[78/500] | Epoch Time: 0m 44s - Train Loss: 0.2172 - Val. Loss: 0.3139
82
+ INFO:root:[79/500] | Epoch Time: 0m 45s - Train Loss: 0.2129 - Val. Loss: 0.3127
83
+ INFO:root:[80/500] | Epoch Time: 0m 45s - Train Loss: 0.2131 - Val. Loss: 0.3142
84
+ INFO:root:[81/500] | Epoch Time: 0m 45s - Train Loss: 0.2151 - Val. Loss: 0.3115
85
+ INFO:root:[82/500] | Epoch Time: 0m 45s - Train Loss: 0.2107 - Val. Loss: 0.3152
86
+ INFO:root:[83/500] | Epoch Time: 0m 44s - Train Loss: 0.2113 - Val. Loss: 0.3127
87
+ INFO:root:[84/500] | Epoch Time: 0m 46s - Train Loss: 0.2122 - Val. Loss: 0.3145
88
+ INFO:root:[85/500] | Epoch Time: 0m 44s - Train Loss: 0.2051 - Val. Loss: 0.3147
89
+ INFO:root:[86/500] | Epoch Time: 0m 46s - Train Loss: 0.2066 - Val. Loss: 0.3137
90
+ INFO:root:[87/500] | Epoch Time: 0m 46s - Train Loss: 0.2065 - Val. Loss: 0.3177
91
+ INFO:root:[88/500] | Epoch Time: 0m 45s - Train Loss: 0.2032 - Val. Loss: 0.3159
92
+ INFO:root:[89/500] | Epoch Time: 0m 44s - Train Loss: 0.2030 - Val. Loss: 0.3155
93
+ INFO:root:[90/500] | Epoch Time: 0m 44s - Train Loss: 0.2021 - Val. Loss: 0.3152
94
+ INFO:root:[91/500] | Epoch Time: 0m 46s - Train Loss: 0.2036 - Val. Loss: 0.3140
95
+ INFO:root:[92/500] | Epoch Time: 0m 45s - Train Loss: 0.2042 - Val. Loss: 0.3151
96
+ INFO:root:[93/500] | Epoch Time: 0m 45s - Train Loss: 0.2025 - Val. Loss: 0.3152
97
+ INFO:root:[94/500] | Epoch Time: 0m 46s - Train Loss: 0.2093 - Val. Loss: 0.3145
98
+ INFO:root:[95/500] | Epoch Time: 0m 45s - Train Loss: 0.2048 - Val. Loss: 0.3147
99
+ INFO:root:[96/500] | Epoch Time: 0m 45s - Train Loss: 0.2015 - Val. Loss: 0.3149
100
+ INFO:root:[97/500] | Epoch Time: 0m 44s - Train Loss: 0.2011 - Val. Loss: 0.3142
101
+ INFO:root:[98/500] | Epoch Time: 0m 44s - Train Loss: 0.2020 - Val. Loss: 0.3148
102
+ INFO:root:[99/500] | Epoch Time: 0m 44s - Train Loss: 0.2054 - Val. Loss: 0.3145
103
+ INFO:root:[100/500] | Epoch Time: 0m 45s - Train Loss: 0.2028 - Val. Loss: 0.3147
104
+ INFO:root:[101/500] | Epoch Time: 0m 45s - Train Loss: 0.2036 - Val. Loss: 0.3150
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.104.1
2
+ uvicorn[standard]==0.24.0
3
+ pydantic==2.5.0
4
+ pydantic-settings==2.1.0
5
+ torch>=2.0.0
6
+ torchvision>=0.15.0
7
+ numpy>=1.24.0
8
+ opencv-python-headless>=4.8.0
9
+ python-multipart==0.0.6
10
+ Pillow>=8.3.0