Spleen Segmentation (ม้าม)

This commit is contained in:
Flook 2025-11-07 05:26:00 +07:00
parent 80aacd6325
commit 2186016f23
8 changed files with 225 additions and 1 deletions

8
.idea/.gitignore generated vendored Normal file
View File

@ -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

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

9
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.12" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="Python 3.12 (2)" project-jdk-type="Python SDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/monorepo-starter-template.iml" filepath="$PROJECT_DIR$/.idea/monorepo-starter-template.iml" />
</modules>
</component>
</project>

9
.idea/monorepo-starter-template.iml generated Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.12" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -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

View File

@ -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."
}