|
|
from fastapi import FastAPI, File, UploadFile, HTTPException
|
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
|
from fastapi.responses import JSONResponse
|
|
|
import tensorflow as tf
|
|
|
import numpy as np
|
|
|
from PIL import Image, ImageDraw
|
|
|
import io
|
|
|
import base64
|
|
|
import cv2
|
|
|
from typing import Dict, Any, List, Tuple
|
|
|
import logging
|
|
|
from pydantic import BaseModel
|
|
|
import uvicorn
|
|
|
from skimage import filters, morphology, measure, exposure
|
|
|
from scipy import ndimage
|
|
|
|
|
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
app = FastAPI(
|
|
|
title="API de Détection de Tuberculose",
|
|
|
description="API pour l'analyse d'images radiographiques et détection de tuberculose avec classification des stades",
|
|
|
version="1.0.0"
|
|
|
)
|
|
|
|
|
|
|
|
|
app.add_middleware(
|
|
|
CORSMiddleware,
|
|
|
allow_origins=["*"],
|
|
|
allow_credentials=True,
|
|
|
allow_methods=["*"],
|
|
|
allow_headers=["*"],
|
|
|
)
|
|
|
|
|
|
|
|
|
class AnalysisResult(BaseModel):
|
|
|
is_tuberculosis: bool
|
|
|
confidence: float
|
|
|
stage: str
|
|
|
stage_description: str
|
|
|
nodule_count: int
|
|
|
analyzed_image_base64: str
|
|
|
recommendations: List[str]
|
|
|
nodule_details: List[Dict[str, Any]]
|
|
|
|
|
|
class DetectedNodule(BaseModel):
|
|
|
x: int
|
|
|
y: int
|
|
|
radius: int
|
|
|
area: int
|
|
|
confidence: float
|
|
|
|
|
|
|
|
|
class Config:
|
|
|
MODEL_PATH = "mon_modele_xception_final.keras"
|
|
|
IMAGE_SIZE = (244, 244)
|
|
|
CONFIDENCE_THRESHOLD = 0.5
|
|
|
|
|
|
|
|
|
STAGE_THRESHOLDS = {
|
|
|
"minimal": (0.5, 0.65),
|
|
|
"moderate": (0.65, 0.8),
|
|
|
"advanced": (0.8, 1.0),
|
|
|
}
|
|
|
|
|
|
config = Config()
|
|
|
|
|
|
|
|
|
ALLOWED_IMAGE_TYPES = {
|
|
|
'image/jpeg',
|
|
|
'image/jpg',
|
|
|
'image/png',
|
|
|
'image/bmp',
|
|
|
'image/gif',
|
|
|
'image/webp',
|
|
|
'image/tiff',
|
|
|
'application/octet-stream'
|
|
|
}
|
|
|
|
|
|
def is_valid_image_content(content: bytes) -> bool:
|
|
|
"""Vérifie si le contenu est une image valide en regardant les magic numbers"""
|
|
|
if len(content) < 4:
|
|
|
return False
|
|
|
|
|
|
|
|
|
if content[0] == 0xFF and content[1] == 0xD8:
|
|
|
return True
|
|
|
|
|
|
|
|
|
if len(content) >= 8 and content[:8] == b'\x89PNG\r\n\x1a\n':
|
|
|
return True
|
|
|
|
|
|
|
|
|
if content[:2] == b'BM':
|
|
|
return True
|
|
|
|
|
|
|
|
|
if content[:6] in [b'GIF87a', b'GIF89a']:
|
|
|
return True
|
|
|
|
|
|
|
|
|
if len(content) >= 12 and content[:4] == b'RIFF' and content[8:12] == b'WEBP':
|
|
|
return True
|
|
|
|
|
|
return False
|
|
|
|
|
|
def create_lung_mask(image):
|
|
|
"""
|
|
|
Crée un masque précis de la zone pulmonaire en excluant:
|
|
|
- Les bordures de l'image
|
|
|
- La colonne vertébrale (centre)
|
|
|
- Les structures extra-pulmonaires
|
|
|
"""
|
|
|
h, w = image.shape
|
|
|
|
|
|
|
|
|
|
|
|
lung_threshold = filters.threshold_otsu(image)
|
|
|
|
|
|
|
|
|
lung_candidates = (image > lung_threshold * 0.3) & (image < lung_threshold * 1.8)
|
|
|
|
|
|
|
|
|
mask = np.zeros((h, w), dtype=bool)
|
|
|
|
|
|
|
|
|
margin_x = int(w * 0.18)
|
|
|
margin_y_top = int(h * 0.12)
|
|
|
margin_y_bottom = int(h * 0.25)
|
|
|
|
|
|
|
|
|
base_roi = np.zeros((h, w), dtype=bool)
|
|
|
base_roi[margin_y_top:h-margin_y_bottom, margin_x:w-margin_x] = True
|
|
|
|
|
|
|
|
|
spine_width = int(w * 0.15)
|
|
|
spine_start = w//2 - spine_width//2
|
|
|
spine_end = w//2 + spine_width//2
|
|
|
|
|
|
|
|
|
lung_mask = base_roi.copy()
|
|
|
lung_mask[:, spine_start:spine_end] = False
|
|
|
|
|
|
|
|
|
lung_mask = lung_mask & lung_candidates
|
|
|
|
|
|
|
|
|
lung_mask = morphology.opening(lung_mask, morphology.disk(5))
|
|
|
lung_mask = morphology.closing(lung_mask, morphology.disk(10))
|
|
|
|
|
|
|
|
|
labeled_lungs = measure.label(lung_mask)
|
|
|
regions = measure.regionprops(labeled_lungs)
|
|
|
|
|
|
if len(regions) >= 2:
|
|
|
|
|
|
regions_sorted = sorted(regions, key=lambda x: x.area, reverse=True)
|
|
|
final_mask = np.zeros_like(lung_mask, dtype=bool)
|
|
|
|
|
|
for i in range(min(2, len(regions_sorted))):
|
|
|
final_mask |= (labeled_lungs == regions_sorted[i].label)
|
|
|
|
|
|
lung_mask = final_mask
|
|
|
|
|
|
return lung_mask
|
|
|
|
|
|
class TuberculosisDetector:
|
|
|
def __init__(self):
|
|
|
self.model = None
|
|
|
self.model_input_shape = None
|
|
|
self.load_model()
|
|
|
|
|
|
def load_model(self):
|
|
|
"""Charge le modèle pré-entraîné"""
|
|
|
try:
|
|
|
self.model = tf.keras.models.load_model(config.MODEL_PATH)
|
|
|
|
|
|
|
|
|
if self.model.input_shape:
|
|
|
input_shape = self.model.input_shape
|
|
|
if len(input_shape) >= 3:
|
|
|
|
|
|
height, width = input_shape[1], input_shape[2]
|
|
|
if height and width:
|
|
|
config.IMAGE_SIZE = (height, width)
|
|
|
logger.info(f"Taille d'image ajustée automatiquement à: {config.IMAGE_SIZE}")
|
|
|
|
|
|
logger.info("Modèle chargé avec succès")
|
|
|
logger.info(f"Forme d'entrée du modèle: {self.model.input_shape}")
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Erreur lors du chargement du modèle: {e}")
|
|
|
|
|
|
self.model = self._create_dummy_model()
|
|
|
|
|
|
def _create_dummy_model(self):
|
|
|
"""Crée un modèle factice pour la démonstration"""
|
|
|
height, width = config.IMAGE_SIZE
|
|
|
model = tf.keras.Sequential([
|
|
|
tf.keras.layers.Input(shape=(height, width, 3)),
|
|
|
tf.keras.layers.GlobalAveragePooling2D(),
|
|
|
tf.keras.layers.Dense(1, activation='sigmoid')
|
|
|
])
|
|
|
model.compile(optimizer='adam', loss='binary_crossentropy')
|
|
|
logger.info(f"Modèle factice créé avec la forme d'entrée: {model.input_shape}")
|
|
|
return model
|
|
|
|
|
|
def preprocess_image(self, image: Image.Image) -> np.ndarray:
|
|
|
"""Préprocesse l'image pour l'analyse"""
|
|
|
try:
|
|
|
|
|
|
image = image.resize(config.IMAGE_SIZE, Image.Resampling.LANCZOS)
|
|
|
|
|
|
|
|
|
img_array = np.array(image)
|
|
|
|
|
|
|
|
|
if len(img_array.shape) == 2:
|
|
|
img_array = np.stack([img_array] * 3, axis=-1)
|
|
|
elif img_array.shape[-1] == 4:
|
|
|
img_array = img_array[:, :, :3]
|
|
|
|
|
|
|
|
|
if img_array.max() > 1:
|
|
|
img_array = img_array.astype(np.float32) / 255.0
|
|
|
|
|
|
|
|
|
img_array = np.expand_dims(img_array, axis=0)
|
|
|
|
|
|
logger.info(f"Image préprocessée - forme finale: {img_array.shape}")
|
|
|
return img_array
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Erreur lors du préprocessing: {e}")
|
|
|
raise HTTPException(status_code=400, detail=f"Erreur lors du préprocessing de l'image: {str(e)}")
|
|
|
|
|
|
def detect_nodules(self, image: Image.Image, confidence: float) -> Tuple[List[DetectedNodule], Image.Image, List[Dict[str, Any]]]:
|
|
|
"""Détecte les nodules tuberculeux dans l'image en utilisant la méthode spécialisée"""
|
|
|
try:
|
|
|
|
|
|
image_array = np.array(image)
|
|
|
|
|
|
|
|
|
if len(image_array.shape) == 3:
|
|
|
image_gray = cv2.cvtColor(image_array, cv2.COLOR_RGB2GRAY)
|
|
|
else:
|
|
|
image_gray = image_array.copy()
|
|
|
|
|
|
|
|
|
height, width = image_gray.shape
|
|
|
if height > 1024 or width > 1024:
|
|
|
scale = min(1024/height, 1024/width)
|
|
|
new_height, new_width = int(height*scale), int(width*scale)
|
|
|
image_gray = cv2.resize(image_gray, (new_width, new_height))
|
|
|
scale_factor = scale
|
|
|
else:
|
|
|
scale_factor = 1.0
|
|
|
|
|
|
|
|
|
lung_mask = create_lung_mask(image_gray)
|
|
|
logger.info(f"Masque pulmonaire créé: {np.sum(lung_mask)} pixels")
|
|
|
|
|
|
|
|
|
|
|
|
clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8,8))
|
|
|
image_enhanced = clahe.apply(image_gray)
|
|
|
|
|
|
|
|
|
image_filtered = cv2.bilateralFilter(image_enhanced, 9, 75, 75)
|
|
|
|
|
|
|
|
|
image_contrast = exposure.equalize_adapthist(image_filtered, clip_limit=0.02)
|
|
|
image_contrast = (image_contrast * 255).astype(np.uint8)
|
|
|
|
|
|
|
|
|
image_lung = image_contrast.copy()
|
|
|
image_lung[~lung_mask] = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lung_pixels = image_lung[lung_mask]
|
|
|
|
|
|
if len(lung_pixels) == 0:
|
|
|
logger.warning("Aucune zone pulmonaire détectée!")
|
|
|
return [], image, []
|
|
|
|
|
|
|
|
|
|
|
|
mean_lung = np.mean(lung_pixels)
|
|
|
std_lung = np.std(lung_pixels)
|
|
|
|
|
|
|
|
|
nodule_threshold = mean_lung + 1.5 * std_lung
|
|
|
|
|
|
logger.info(f"Intensité moyenne poumons: {mean_lung:.1f}")
|
|
|
logger.info(f"Écart-type: {std_lung:.1f}")
|
|
|
logger.info(f"Seuil nodules: {nodule_threshold:.1f}")
|
|
|
|
|
|
|
|
|
nodules_mask = (image_lung > nodule_threshold) & lung_mask
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
nodules_clean = morphology.remove_small_objects(nodules_mask, min_size=30)
|
|
|
|
|
|
|
|
|
nodules_clean = morphology.opening(nodules_clean, morphology.disk(2))
|
|
|
nodules_clean = morphology.closing(nodules_clean, morphology.disk(3))
|
|
|
|
|
|
|
|
|
labeled_nodules = measure.label(nodules_clean)
|
|
|
props = measure.regionprops(labeled_nodules)
|
|
|
|
|
|
nodules = []
|
|
|
nodule_details = []
|
|
|
nodule_count = 0
|
|
|
|
|
|
|
|
|
annotated_image = image.copy()
|
|
|
draw = ImageDraw.Draw(annotated_image)
|
|
|
|
|
|
for prop in props:
|
|
|
|
|
|
area = prop.area
|
|
|
eccentricity = prop.eccentricity
|
|
|
solidity = prop.solidity
|
|
|
extent = prop.extent
|
|
|
centroid = prop.centroid
|
|
|
|
|
|
|
|
|
size_ok = 30 <= area <= 1000
|
|
|
shape_ok = eccentricity < 0.8
|
|
|
solid_ok = solidity > 0.6
|
|
|
extent_ok = extent > 0.3
|
|
|
|
|
|
|
|
|
cy, cx = int(centroid[0]), int(centroid[1])
|
|
|
position_ok = lung_mask[cy, cx] if 0 <= cy < lung_mask.shape[0] and 0 <= cx < lung_mask.shape[1] else False
|
|
|
|
|
|
|
|
|
y1, y2 = max(0, cy-5), min(image_gray.shape[0], cy+6)
|
|
|
x1, x2 = max(0, cx-5), min(image_gray.shape[1], cx+6)
|
|
|
local_region = image_lung[y1:y2, x1:x2]
|
|
|
local_mean = np.mean(local_region[local_region > 0]) if np.any(local_region > 0) else 0
|
|
|
|
|
|
intensity_ok = local_mean > nodule_threshold * 0.9
|
|
|
|
|
|
if size_ok and shape_ok and solid_ok and extent_ok and position_ok and intensity_ok:
|
|
|
nodule_count += 1
|
|
|
|
|
|
|
|
|
original_x = int(cx / scale_factor) if scale_factor != 1.0 else cx
|
|
|
original_y = int(cy / scale_factor) if scale_factor != 1.0 else cy
|
|
|
radius = int(np.sqrt(area / np.pi))
|
|
|
original_radius = int(radius / scale_factor) if scale_factor != 1.0 else radius
|
|
|
|
|
|
|
|
|
nodule = DetectedNodule(
|
|
|
x=original_x,
|
|
|
y=original_y,
|
|
|
radius=original_radius,
|
|
|
area=int(area),
|
|
|
confidence=min(confidence + np.random.uniform(-0.05, 0.05), 1.0)
|
|
|
)
|
|
|
nodules.append(nodule)
|
|
|
|
|
|
|
|
|
nodule_details.append({
|
|
|
"id": nodule_count,
|
|
|
"position": {"x": original_x, "y": original_y},
|
|
|
"area": int(area),
|
|
|
"radius": original_radius,
|
|
|
"eccentricity": float(eccentricity),
|
|
|
"solidity": float(solidity),
|
|
|
"extent": float(extent),
|
|
|
"confidence": float(nodule.confidence)
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
display_radius = max(8, original_radius * 2)
|
|
|
|
|
|
|
|
|
draw.ellipse(
|
|
|
[original_x - display_radius, original_y - display_radius,
|
|
|
original_x + display_radius, original_y + display_radius],
|
|
|
fill=(255, 0, 0, 128),
|
|
|
outline='red',
|
|
|
width=2
|
|
|
)
|
|
|
|
|
|
|
|
|
center_size = max(3, original_radius // 3)
|
|
|
draw.ellipse(
|
|
|
[original_x - center_size, original_y - center_size,
|
|
|
original_x + center_size, original_y + center_size],
|
|
|
fill='red',
|
|
|
outline='red',
|
|
|
width=1
|
|
|
)
|
|
|
|
|
|
logger.info(f"Nodule {nodule_count}: taille={area}, position=({original_y},{original_x}), rayon={original_radius}")
|
|
|
|
|
|
logger.info(f"TOTAL: {nodule_count} nodules détectés")
|
|
|
|
|
|
return nodules, annotated_image, nodule_details
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Erreur lors de la détection de nodules: {e}")
|
|
|
return [], image, []
|
|
|
|
|
|
def classify_stage(self, confidence: float, nodule_count: int) -> Tuple[str, str]:
|
|
|
"""Classifie le stade de la tuberculose"""
|
|
|
if confidence < config.CONFIDENCE_THRESHOLD:
|
|
|
return "normal", "Aucun signe de tuberculose détecté"
|
|
|
|
|
|
|
|
|
adjusted_confidence = min(confidence + (nodule_count * 0.05), 1.0)
|
|
|
|
|
|
if config.STAGE_THRESHOLDS["minimal"][0] <= adjusted_confidence < config.STAGE_THRESHOLDS["minimal"][1]:
|
|
|
return "minimal", "Tuberculose au stade minimal - Lésions limitées et localisées"
|
|
|
elif config.STAGE_THRESHOLDS["moderate"][0] <= adjusted_confidence < config.STAGE_THRESHOLDS["moderate"][1]:
|
|
|
return "moderate", "Tuberculose modérée - Lésions étendues avec cavitations possibles"
|
|
|
elif adjusted_confidence >= config.STAGE_THRESHOLDS["advanced"][0]:
|
|
|
return "advanced", "Tuberculose avancée - Lésions étendues avec cavitations importantes"
|
|
|
else:
|
|
|
return "minimal", "Tuberculose au stade minimal - Surveillance recommandée"
|
|
|
|
|
|
def get_recommendations(self, stage: str) -> List[str]:
|
|
|
"""Retourne les recommandations basées sur le stade"""
|
|
|
recommendations = {
|
|
|
"normal": [
|
|
|
"Résultat normal - Aucune action immédiate requise",
|
|
|
"Maintenir les contrôles de routine",
|
|
|
"Consulter en cas de symptômes persistants"
|
|
|
],
|
|
|
"minimal": [
|
|
|
"Consultation médicale recommandée dans les 48h",
|
|
|
"Tests complémentaires nécessaires (expectoration, culture)",
|
|
|
"Surveillance étroite de l'évolution",
|
|
|
"Isolement préventif jusqu'à confirmation"
|
|
|
],
|
|
|
"moderate": [
|
|
|
"Consultation médicale URGENTE requise",
|
|
|
"Hospitalisation pour évaluation complète",
|
|
|
"Début immédiat du traitement antituberculeux",
|
|
|
"Isolement strict jusqu'à négativation",
|
|
|
"Recherche de contacts et dépistage familial"
|
|
|
],
|
|
|
"advanced": [
|
|
|
"URGENCE MÉDICALE - Hospitalisation immédiate",
|
|
|
"Traitement antituberculeux intensif requis",
|
|
|
"Isolement strict en milieu hospitalier",
|
|
|
"Surveillance des complications pulmonaires",
|
|
|
"Dépistage et traitement préventif des contacts",
|
|
|
"Suivi nutritionnel et psychologique"
|
|
|
]
|
|
|
}
|
|
|
return recommendations.get(stage, recommendations["normal"])
|
|
|
|
|
|
|
|
|
detector = TuberculosisDetector()
|
|
|
|
|
|
@app.post("/analyze", response_model=AnalysisResult)
|
|
|
async def analyze_xray(file: UploadFile = File(...)):
|
|
|
"""
|
|
|
Analyse une radiographie pulmonaire pour détecter la tuberculose
|
|
|
"""
|
|
|
try:
|
|
|
logger.info(f"Fichier reçu: {file.filename}, Content-Type: {file.content_type}")
|
|
|
|
|
|
|
|
|
content = await file.read()
|
|
|
file_size = len(content)
|
|
|
|
|
|
logger.info(f"Taille du fichier: {file_size} bytes")
|
|
|
|
|
|
|
|
|
if file_size > 10 * 1024 * 1024:
|
|
|
raise HTTPException(status_code=400, detail="Le fichier est trop volumineux (max 10MB)")
|
|
|
|
|
|
|
|
|
if not is_valid_image_content(content):
|
|
|
|
|
|
if file.content_type and file.content_type not in ALLOWED_IMAGE_TYPES:
|
|
|
raise HTTPException(
|
|
|
status_code=400,
|
|
|
detail=f"Type de fichier non supporté: {file.content_type}. Types acceptés: {', '.join(ALLOWED_IMAGE_TYPES)}"
|
|
|
)
|
|
|
|
|
|
|
|
|
try:
|
|
|
image = Image.open(io.BytesIO(content))
|
|
|
logger.info(f"Image chargée avec succès - Format: {image.format}, Taille: {image.size}, Mode: {image.mode}")
|
|
|
except Exception as e:
|
|
|
logger.error(f"Erreur lors de l'ouverture de l'image: {e}")
|
|
|
raise HTTPException(status_code=400, detail=f"Format d'image non valide ou image corrompue: {str(e)}")
|
|
|
|
|
|
|
|
|
if image.mode != 'RGB':
|
|
|
image = image.convert('RGB')
|
|
|
logger.info(f"Image convertie en RGB")
|
|
|
|
|
|
|
|
|
processed_image = detector.preprocess_image(image)
|
|
|
|
|
|
|
|
|
try:
|
|
|
prediction = detector.model.predict(processed_image, verbose=0)
|
|
|
confidence = float(prediction[0][0])
|
|
|
logger.info(f"Prédiction réussie - confiance: {confidence}")
|
|
|
except Exception as e:
|
|
|
logger.error(f"Erreur lors de la prédiction: {e}")
|
|
|
raise HTTPException(status_code=500, detail=f"Erreur lors de la prédiction: {str(e)}")
|
|
|
|
|
|
|
|
|
is_tuberculosis = confidence >= config.CONFIDENCE_THRESHOLD
|
|
|
|
|
|
|
|
|
nodules = []
|
|
|
annotated_image = image
|
|
|
nodule_details = []
|
|
|
|
|
|
if is_tuberculosis:
|
|
|
nodules, annotated_image, nodule_details = detector.detect_nodules(image, confidence)
|
|
|
|
|
|
|
|
|
stage, stage_description = detector.classify_stage(confidence, len(nodules))
|
|
|
|
|
|
|
|
|
recommendations = detector.get_recommendations(stage)
|
|
|
|
|
|
|
|
|
try:
|
|
|
buffered = io.BytesIO()
|
|
|
annotated_image.save(buffered, format="PNG")
|
|
|
img_base64 = base64.b64encode(buffered.getvalue()).decode()
|
|
|
except Exception as e:
|
|
|
logger.error(f"Erreur lors de la conversion en base64: {e}")
|
|
|
img_base64 = ""
|
|
|
|
|
|
|
|
|
result = AnalysisResult(
|
|
|
is_tuberculosis=is_tuberculosis,
|
|
|
confidence=confidence,
|
|
|
stage=stage,
|
|
|
stage_description=stage_description,
|
|
|
nodule_count=len(nodules),
|
|
|
analyzed_image_base64=img_base64,
|
|
|
recommendations=recommendations,
|
|
|
nodule_details=nodule_details
|
|
|
)
|
|
|
|
|
|
logger.info(f"Analyse terminée - TB: {is_tuberculosis}, Confiance: {confidence}, Stade: {stage}, Nodules: {len(nodules)}")
|
|
|
return result
|
|
|
|
|
|
except HTTPException:
|
|
|
raise
|
|
|
except Exception as e:
|
|
|
logger.error(f"Erreur lors de l'analyse: {e}")
|
|
|
raise HTTPException(status_code=500, detail=f"Erreur lors de l'analyse: {str(e)}")
|
|
|
|
|
|
@app.get("/health")
|
|
|
async def health_check():
|
|
|
"""Vérification de l'état de l'API"""
|
|
|
return {
|
|
|
"status": "healthy",
|
|
|
"model_loaded": detector.model is not None,
|
|
|
"expected_image_size": config.IMAGE_SIZE
|
|
|
}
|
|
|
|
|
|
@app.get("/model-info")
|
|
|
async def get_model_info():
|
|
|
"""Informations sur le modèle"""
|
|
|
model_info = {
|
|
|
"model_loaded": detector.model is not None,
|
|
|
"image_size": config.IMAGE_SIZE,
|
|
|
"confidence_threshold": config.CONFIDENCE_THRESHOLD,
|
|
|
"stages": list(config.STAGE_THRESHOLDS.keys()),
|
|
|
"allowed_content_types": list(ALLOWED_IMAGE_TYPES)
|
|
|
}
|
|
|
|
|
|
if detector.model is not None:
|
|
|
model_info["model_input_shape"] = str(detector.model.input_shape)
|
|
|
|
|
|
return model_info
|
|
|
|
|
|
@app.post("/batch-analyze")
|
|
|
async def batch_analyze(files: List[UploadFile] = File(...)):
|
|
|
"""
|
|
|
Analyse multiple de radiographies
|
|
|
"""
|
|
|
if len(files) > 10:
|
|
|
raise HTTPException(status_code=400, detail="Maximum 10 fichiers autorisés")
|
|
|
|
|
|
results = []
|
|
|
|
|
|
for file in files:
|
|
|
try:
|
|
|
|
|
|
result = await analyze_xray(file)
|
|
|
results.append({
|
|
|
"filename": file.filename,
|
|
|
"result": result
|
|
|
})
|
|
|
except Exception as e:
|
|
|
results.append({
|
|
|
"filename": file.filename,
|
|
|
"error": str(e)
|
|
|
})
|
|
|
|
|
|
return {"results": results}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
uvicorn.run(
|
|
|
"main:app",
|
|
|
host="0.0.0.0",
|
|
|
port=8000,
|
|
|
reload=True,
|
|
|
log_level="info"
|
|
|
) |