diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..03d9549
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..0b8763d
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..5f3a278
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/monorepo-starter-template.iml b/.idea/monorepo-starter-template.iml
new file mode 100644
index 0000000..67954c8
--- /dev/null
+++ b/.idea/monorepo-starter-template.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 1d84219..06c01ec 100644
--- a/README.md
+++ b/README.md
@@ -71,3 +71,48 @@ docker compose up -d cockroach-1 cockroach-2 cockroach-3 init-cluster redis mini
docker compose up -d
```
คำสั่ง docker compose down -v จะลบ Volume และฐานข้อมูลทั้งหมด ใช้เฉพาะตอนต้องการเริ่มต้นฐานข้อมูลใหม่
+
+
+# บริการโมเดล AI ทางการแพทย์ (Medical AI Models)
+
+โครงการนี้มุ่งมั่นที่จะนำเสนอโมเดล AI คุณภาพสูงเพื่อช่วยในการวิเคราะห์ภาพทางการแพทย์
+
+➡️ **สถานะ:** กำลังพัฒนาโมเดลใหม่ ๆ เพิ่มเติมอย่างต่อเนื่อง
+
+---
+
+## 1. ⚙️ Spleen Segmentation (ม้าม)
+
+### ภาพรวม
+
+API นี้ให้บริการ **Segmentation** (การระบุขอบเขต) ของอวัยวะ **ม้าม** จากไฟล์ภาพ CT (NIfTI format) โดยเฉพาะ โดยใช้โมเดล **MONAI UNet** ที่ผ่านการฝึกฝนมาเพื่อตอบโจทย์ทางคลินิก
+
+### ตอบคำถาม: "ม้ามอยู่ที่ไหนและมีปริมาตรเท่าไหร่ในภาพนี้"
+
+บริการนี้ถูกออกแบบมาเพื่อส่งคืนข้อมูลที่สำคัญสำหรับรายงานทางการแพทย์โดยอัตโนมัติ:
+
+1. **การระบุตำแหน่ง:** ตำแหน่งของม้ามถูกระบุผ่าน **Segmentation Map** ซึ่งแสดงตำแหน่งของม้ามในระบบพิกัด RAS ที่ได้มาตรฐาน
+2. **การคำนวณปริมาตร:** คำนวณปริมาตรของม้ามในหน่วยลูกบาศก์เซนติเมตร ($\text{cm}^3$) โดยใช้ข้อมูล Voxel Spacing
+3. **การวิเคราะห์ม้ามโต (Splenomegaly):** ประเมินขนาดม้ามตามเกณฑ์มาตรฐาน และส่งคืนผลการวินิจฉัยเบื้องต้น (เช่น Normal Spleen Size, Borderline Enlarged, หรือ Splenomegaly Detected)
+
+### Endpoint
+
+| Method | Path | Description |
+| :--- | :--- | :--- |
+| `POST` | `/inference/spleen` | รับไฟล์ NIfTI และส่งคืนผลลัพธ์ Segmentation และ Volume Analysis |
+
+### 🛠️ เทคโนโลยีโมเดล
+
+* **เฟรมเวิร์ก:** MONAI / PyTorch
+* **ไฟล์โมเดล:** `spleen_ct_spleen_model.ts` (โหลดจาก MinIO)
+* **Input:** 3D CT Scan (NIfTI)
+* **Output:** JSON ที่มีปริมาตร ($\text{cm}^3$) และการวินิจฉัยม้ามโต
+
+---
+
+## 2. ⏳ โมเดลที่กำลังพัฒนา
+
+เราวางแผนที่จะขยายบริการไปยังอวัยวะและพยาธิสภาพอื่น ๆ ในอนาคตอันใกล้ เช่น:
+* Liver and Tumor Segmentation
+* Kidney Segmentation
+* Lung Nodule Detection
\ No newline at end of file
diff --git a/ai-medical/app.py b/ai-medical/app.py
index 7d1d656..e44e566 100644
--- a/ai-medical/app.py
+++ b/ai-medical/app.py
@@ -4,10 +4,15 @@ import torch
import logging
import boto3
from botocore.client import Config
-from fastapi import FastAPI, HTTPException
+from fastapi import FastAPI, HTTPException, UploadFile, File
from contextlib import asynccontextmanager
from pydantic_settings import BaseSettings
+import numpy as np
+import torch.nn.functional as F
+# MONAI Dependencies ที่จำเป็นสำหรับการประมวลผล
+from monai.inferers import sliding_window_inference
+from monai.transforms import Compose, LoadImage, EnsureChannelFirst, ScaleIntensityRange, SpatialPad, Spacing,Resize,CenterSpatialCrop, Orientation, NormalizeIntensity
# --- Logging setup ---
logger = logging.getLogger("uvicorn")
@@ -101,3 +106,131 @@ async def reload_model():
return {"message": f"Model '{settings.MODEL_FILE}' reloaded successfully"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
+
+# --- 7. MONAI Inference Endpoint ---
+@app.post("/inference/spleen")
+async def spleen_segmentation(file: UploadFile = File(...)):
+ """
+ รับไฟล์ภาพทางการแพทย์ (NIfTI) และดำเนินการ Segmentation ม้าม
+ พร้อมคำนวณปริมาตรจริงและวิเคราะห์ภาวะม้ามโต
+ """
+ if model is None:
+ raise HTTPException(status_code=503, detail="Model is not loaded. Please wait or check logs.")
+
+ # เกณฑ์การวิเคราะห์ม้ามโต (Splenomegaly Thresholds)
+ SPLENOMEGALY_THRESHOLD_CM3 = 450.0
+
+ # 1. บันทึกไฟล์ที่ได้รับชั่วคราว
+ with tempfile.TemporaryDirectory() as temp_dir:
+ input_path = os.path.join(temp_dir, file.filename)
+
+ try:
+ content = await file.read()
+ with open(input_path, "wb") as f:
+ f.write(content)
+ logger.info(f"Received file: {file.filename} saved to {input_path}")
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Failed to read/save uploaded file: {e}")
+
+ # 2. Pre-processing, Load Image และดึง Voxel Spacing (NEW)
+ try:
+ target_spacing = (1.5, 1.5, 2.0)
+ target_size = (96, 96, 96) # roi_size ที่โมเดลคาดหวัง
+
+ # --- ดึง Original Spacing ก่อน Transform ---
+ try:
+ nifti_img = nib.load(input_path)
+ # Dims [1, 2, 3] คือ Spacing สำหรับ x, y, z (มักเป็น mm)
+ original_spacing_mm = tuple(nifti_img.header['pixdim'][1:4].tolist())
+ logger.info(f"Original Voxel Spacing (mm): {original_spacing_mm}")
+
+ # คำนวณปริมาตรของ 1 Voxel ในภาพเดิม (mm³)
+ original_voxel_volume_mm3 = float(np.prod(original_spacing_mm))
+ except Exception as e:
+ logger.warning(f"Failed to load NIfTI Header/Spacing via nibabel. Falling back to target_spacing for volume calculation. Error: {e}")
+ # หากดึง Spacing เดิมไม่ได้ ให้ใช้ Spacing ของภาพที่ Resample แล้วเป็นค่าประมาณ
+ original_voxel_volume_mm3 = float(np.prod(target_spacing))
+
+ # --- MONAI Transforms (Resampling to target_spacing เกิดขึ้นที่นี่) ---
+ transform = Compose([
+ LoadImage(image_only=True, ensure_channel_first=True, reader='NibabelReader'),
+ Orientation(axcodes='RAS'),
+ Spacing(pixdim=target_spacing, mode='bilinear'), # <--- Resampling ที่นี่!
+
+ NormalizeIntensity(subtrahend=None, divisor=None, channel_wise=False),
+ SpatialPad(spatial_size=target_size),
+ CenterSpatialCrop(roi_size=target_size),
+ ])
+
+ img_data = transform(input_path)
+
+ logger.info(f"Input shape after transform: {img_data.shape}, dtype: {img_data.dtype}, min={img_data.min().item():.4f}, max={img_data.max().item():.4f}")
+ input_tensor = torch.as_tensor(img_data, dtype=torch.float32, device=settings.DEVICE).unsqueeze(0)
+
+ except Exception as e:
+ logger.error(f"Pre-processing failed: {e}")
+ raise HTTPException(status_code=500, detail=f"Image Pre-processing Error: {e}")
+
+ # 3. Inference (Same)
+ # 4. Post-processing (Same)
+ model.eval()
+ with torch.no_grad():
+ roi_size = (96, 96, 96)
+ sw_batch_size = 4
+ prediction_raw = sliding_window_inference(
+ inputs=input_tensor, roi_size=roi_size, sw_batch_size=sw_batch_size,
+ predictor=model, overlap=0.5, mode="gaussian"
+ )
+
+ # ... (Softmax/Sigmoid/Argmax Logic - เหมือนเดิม) ...
+ if prediction_raw.shape[1] > 1:
+ prediction_prob = torch.softmax(prediction_raw, dim=1)
+ segmentation_map = torch.argmax(prediction_prob, dim=1).cpu().numpy()[0]
+ elif prediction_raw.shape[1] == 1:
+ prediction_prob = torch.sigmoid(prediction_raw)
+ segmentation_map = (prediction_prob > 0.5).cpu().numpy()[0, 0]
+ else:
+ raise RuntimeError(f"Unexpected model output channel count: {prediction_raw.shape[1]}")
+
+ unique_labels = np.unique(segmentation_map)
+ logger.info(f"Unique labels in segmentation map: {unique_labels}")
+
+ # 5. Post-processing (คำนวณสถิติและปริมาตรจริง) (MODIFIED)
+ if not isinstance(segmentation_map, np.ndarray) or segmentation_map.ndim != 3:
+ logger.error("Segmentation map is not a 3D numpy array.")
+ raise RuntimeError("Post-processing failed to produce a valid 3D map.")
+
+ spleen_voxels = int(np.sum(segmentation_map == 1))
+
+ # ต้อง Inverse Transform Segmentation Map กลับไปยัง Spacing เดิม
+ # อย่างไรก็ตาม ในการใช้งานจริง มักใช้ Voxel Volume ของ Resampled Image (ซึ่งมี Spacing คงที่)
+ # เนื่องจาก Monai/Inferer มักจะทำการ Resample ก่อน และ Volume Calculation ในงานวิจัย
+ # ส่วนใหญ่จะใช้น้ำหนัก Spacing หลัง Resample แล้ว (target_spacing) เพื่อรักษาความสม่ำเสมอ
+
+ # เราจะใช้ target_spacing เพื่อความสอดคล้องกับ Segmentation Map ที่ได้
+ resampled_voxel_volume_mm3 = float(np.prod(target_spacing))
+
+ # ปริมาตรม้าม (cm³) คำนวณจาก Voxel ที่ Segmented และ Spacing หลัง Resample
+ spleen_volume_cm3 = (spleen_voxels * resampled_voxel_volume_mm3) / 1000.0 # mm³ → cm³
+
+ # วินิจฉัย Splenomegaly
+ if spleen_volume_cm3 < 350:
+ diagnosis = "Normal Spleen Size"
+ elif 350 <= spleen_volume_cm3 < SPLENOMEGALY_THRESHOLD_CM3:
+ diagnosis = "Borderline Enlarged"
+ else:
+ diagnosis = "Splenomegaly Detected"
+
+ logger.info(f"Spleen volume: {spleen_volume_cm3:.2f} cm³ → {diagnosis}")
+
+ # 6. ส่งผลลัพธ์กลับ
+ return {
+ "filename": file.filename,
+ "status": "Success",
+ "spleen_voxels_count": spleen_voxels,
+ "resampled_voxel_volume_cm3": round(resampled_voxel_volume_mm3 / 1000.0, 6),
+ "estimated_spleen_volume_cm3": round(spleen_volume_cm3, 2),
+ "diagnosis": diagnosis,
+ "splenomegaly_threshold_cm3": SPLENOMEGALY_THRESHOLD_CM3,
+ "message": "Segmentation, volume calculation using resampled spacing, and splenomegaly analysis complete."
+ }
\ No newline at end of file