Spleen Segmentation (ม้าม)
This commit is contained in:
parent
80aacd6325
commit
2186016f23
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal 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
|
||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal 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
9
.idea/misc.xml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
9
.idea/monorepo-starter-template.iml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
45
README.md
45
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
|
||||
@ -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."
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user