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 up -d
``` ```
คำสั่ง docker compose down -v จะลบ Volume และฐานข้อมูลทั้งหมด ใช้เฉพาะตอนต้องการเริ่มต้นฐานข้อมูลใหม่ คำสั่ง 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 logging
import boto3 import boto3
from botocore.client import Config from botocore.client import Config
from fastapi import FastAPI, HTTPException from fastapi import FastAPI, HTTPException, UploadFile, File
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from pydantic_settings import BaseSettings 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 --- # --- Logging setup ---
logger = logging.getLogger("uvicorn") logger = logging.getLogger("uvicorn")
@ -101,3 +106,131 @@ async def reload_model():
return {"message": f"Model '{settings.MODEL_FILE}' reloaded successfully"} return {"message": f"Model '{settings.MODEL_FILE}' reloaded successfully"}
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(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."
}