Update app.py
Browse files
app.py
CHANGED
|
@@ -6,7 +6,7 @@ import os
|
|
| 6 |
import io
|
| 7 |
from PIL import Image
|
| 8 |
|
| 9 |
-
#
|
| 10 |
import matplotlib
|
| 11 |
matplotlib.use('Agg')
|
| 12 |
import matplotlib.pyplot as plt
|
|
@@ -36,6 +36,18 @@ st.markdown("""
|
|
| 36 |
color: #1565C0;
|
| 37 |
margin-top: 20px;
|
| 38 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
</style>
|
| 40 |
""", unsafe_allow_html=True)
|
| 41 |
|
|
@@ -70,10 +82,10 @@ def format_kspace_display(k_data):
|
|
| 70 |
|
| 71 |
kspace_bg_image = format_kspace_display(kspace_raw)
|
| 72 |
|
| 73 |
-
# --- ฟังก์ชันแปลงรูปภาพ (
|
| 74 |
def get_image_from_plot(fig):
|
| 75 |
buf = io.BytesIO()
|
| 76 |
-
plt.savefig(buf, format='png', bbox_inches='tight', pad_inches=0, dpi=100)
|
| 77 |
plt.close(fig)
|
| 78 |
buf.seek(0)
|
| 79 |
return Image.open(buf)
|
|
@@ -127,11 +139,74 @@ def draw_mri(mri_result):
|
|
| 127 |
ax.axis('off')
|
| 128 |
return get_image_from_plot(fig)
|
| 129 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
# ==========================================================
|
| 131 |
# --- 3. ส่วนแสดงผลเว็บ (ใช้ Container บีบขอบซ้าย-ขวา) ---
|
| 132 |
# ==========================================================
|
| 133 |
|
| 134 |
-
# แบ่งเป็น 3 คอลัมน์ โดยให้คอลัม��์กลางใหญ่สุด (สัดส่วน 1 : 6 : 1) เพื่อเว้นขอบเนื้อหาหลัก
|
| 135 |
_, col_main, _ = st.columns([1, 6, 1])
|
| 136 |
|
| 137 |
with col_main:
|
|
@@ -149,7 +224,6 @@ with col_main:
|
|
| 149 |
ข้อมูลใน k-space มักจะถูกนำมาแสดงผลในรูปแบบตารางสี่เหลี่ยม (Grid) โดยมีแกนหลักคือ **kx (แนวนอน - Frequency)** และ **ky (แนวตั้ง - Phase)** จุดสำคัญคือ **แกน kx และ ky เหล่านี้ ไม่ได้บอกตำแหน่งพิกัดในภาพอวัยวะ** แต่มันคือแกนที่บอกถึงลักษณะของ **"ความถี่เชิงพื้นที่"** ที่เป็นคลื่น (Sinusoidal wave) ด้วยเหตุนี้ **จุดแต่ละจุดบน K-space จึงไม่ได้จับคู่แบบ 1 ต่อ 1 กับพิกเซลบนภาพ MRI** (เช่น จุดมุมซ้ายบนของ K-space ไม่ได้สร้างภาพมุมซ้ายบนของอวัยวะ)
|
| 150 |
""")
|
| 151 |
|
| 152 |
-
# จัดให้รูปเดี่ยวองค์ประกอบ K-Space อยู่กึ่งกลางหน้าจอพอดี
|
| 153 |
_, col_img_center, _ = st.columns([1.5, 3, 1.5])
|
| 154 |
with col_img_center:
|
| 155 |
if os.path.exists("Screenshot 2026-05-07 205051.png"):
|
|
@@ -159,6 +233,54 @@ with col_main:
|
|
| 159 |
else:
|
| 160 |
st.info("กรุณาอัปโหลดรูปภาพ Screenshot 2026-05-07 205051 (เป็น .png หรือ .jpg ก็ได้)")
|
| 161 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
st.markdown("## 📍 1 จุดบน k-space")
|
| 163 |
|
| 164 |
st.markdown("""
|
|
@@ -175,10 +297,8 @@ with col_main:
|
|
| 175 |
- **จุดมืดหรือจาง:** ภาพ MRI แทบจะไม่มีลวดลายชนิดนี้ประกอบอยู่เลย
|
| 176 |
""")
|
| 177 |
|
| 178 |
-
# --- ส่วน Interactive 1 ---
|
| 179 |
st.markdown("### 🎛️ ลองปรับตำแหน่งของจุด K-Space เพื่อดูคลื่นความถี่")
|
| 180 |
|
| 181 |
-
# บีบให้แถบสไลเดอร์และคู่รูปภาพขยับมาอยู่ตรงกลางพร้อมกัน
|
| 182 |
_, col_slide1, col_slide2, _ = st.columns([0.5, 2.5, 2.5, 0.5])
|
| 183 |
with col_slide1:
|
| 184 |
kx_val = st.slider("พิกัด kx (แนวนอน)", -112, 111, 15)
|
|
@@ -224,7 +344,6 @@ with col_main:
|
|
| 224 |
อนุญาตให้เฉพาะข้อมูล **ขอบนอก (ความถี่สูง)** ผ่านไปได้ ข้อมูลตรงกลางทิ้งไป ผลลัพธ์ที่ได้คือภาพจะสูญเสียคอนทราสต์จนเกือบมืดสนิท แต่จะปรากฏ **"เส้นขอบร่าง" (Outline)** ขึ้นมาอย่างคมชัด
|
| 225 |
""")
|
| 226 |
|
| 227 |
-
# --- ส่วน Interactive 2 ---
|
| 228 |
st.markdown("### 🎛️ ลองปรับตัวกรอง (Interactive Filter)")
|
| 229 |
|
| 230 |
_, col_radio_center, _ = st.columns([2, 3, 2])
|
|
@@ -240,7 +359,6 @@ with col_main:
|
|
| 240 |
|
| 241 |
filtered_k, mri_result = apply_filter(kspace_raw, mode, radius)
|
| 242 |
|
| 243 |
-
# จัดรูปภาพคู่ของพาร์ทฟิลเตอร์ให้อยู่กึ่งกลางจออย่างสมดุล
|
| 244 |
_, col_fimg1, col_fimg2, _ = st.columns([0.5, 2.5, 2.5, 0.5])
|
| 245 |
with col_fimg1:
|
| 246 |
st.image(draw_filtered_kspace(filtered_k), caption="ภาพ K-Space ที่ถูกตัวกรอง", use_container_width=True)
|
|
|
|
| 6 |
import io
|
| 7 |
from PIL import Image
|
| 8 |
|
| 9 |
+
# บังคับให้ Matplotlib ทำงานแบบ Background เพื่อแก้ปัญหาภาพสั่น
|
| 10 |
import matplotlib
|
| 11 |
matplotlib.use('Agg')
|
| 12 |
import matplotlib.pyplot as plt
|
|
|
|
| 36 |
color: #1565C0;
|
| 37 |
margin-top: 20px;
|
| 38 |
}
|
| 39 |
+
/* ปรับแต่งปุ่มให้ดูสวยงาม */
|
| 40 |
+
div.stButton > button:first-child {
|
| 41 |
+
background-color: #f0f2f6;
|
| 42 |
+
color: #0D47A1;
|
| 43 |
+
border-radius: 8px;
|
| 44 |
+
border: 1px solid #1E88E5;
|
| 45 |
+
font-weight: bold;
|
| 46 |
+
}
|
| 47 |
+
div.stButton > button:first-child:hover {
|
| 48 |
+
background-color: #1E88E5;
|
| 49 |
+
color: white;
|
| 50 |
+
}
|
| 51 |
</style>
|
| 52 |
""", unsafe_allow_html=True)
|
| 53 |
|
|
|
|
| 82 |
|
| 83 |
kspace_bg_image = format_kspace_display(kspace_raw)
|
| 84 |
|
| 85 |
+
# --- ฟังก์ชันแปลงรูปภาพ (ลดการสั่น) ---
|
| 86 |
def get_image_from_plot(fig):
|
| 87 |
buf = io.BytesIO()
|
| 88 |
+
plt.savefig(buf, format='png', bbox_inches='tight', pad_inches=0.1, dpi=100)
|
| 89 |
plt.close(fig)
|
| 90 |
buf.seek(0)
|
| 91 |
return Image.open(buf)
|
|
|
|
| 139 |
ax.axis('off')
|
| 140 |
return get_image_from_plot(fig)
|
| 141 |
|
| 142 |
+
# --- ฟังก์ชันวาดระบบเติม K-Space ทีละบรรทัด (Interactive 3) ---
|
| 143 |
+
def draw_gy_sequence(current_step, total_steps):
|
| 144 |
+
# กราฟแสดง Phase Encoding (Gy) ด้านซ้าย
|
| 145 |
+
fig, ax = plt.subplots(figsize=(3, 4))
|
| 146 |
+
|
| 147 |
+
# วาดแกน
|
| 148 |
+
ax.axhline(0, color='black', lw=1.5)
|
| 149 |
+
ax.axvline(0, color='black', lw=1.5)
|
| 150 |
+
|
| 151 |
+
y_vals = np.linspace(1, -1, total_steps)
|
| 152 |
+
|
| 153 |
+
for i, y in enumerate(y_vals):
|
| 154 |
+
# ไฮไลต์บรรทัดที่กำลังทำงานให้เป็นสีแดงและหนาขึ้น
|
| 155 |
+
if i == current_step:
|
| 156 |
+
ax.plot([0.1, 0.9], [y, y], color='red', lw=4)
|
| 157 |
+
elif i < current_step:
|
| 158 |
+
ax.plot([0.1, 0.9], [y, y], color='gray', lw=2, alpha=0.5)
|
| 159 |
+
else:
|
| 160 |
+
ax.plot([0.1, 0.9], [y, y], color='blue', lw=2)
|
| 161 |
+
|
| 162 |
+
ax.set_xlim(-0.1, 1.1)
|
| 163 |
+
ax.set_ylim(-1.2, 1.2)
|
| 164 |
+
ax.set_title("Gy (Phase Encoding)", fontweight='bold')
|
| 165 |
+
ax.axis('off')
|
| 166 |
+
return get_image_from_plot(fig)
|
| 167 |
+
|
| 168 |
+
def draw_kspace_filling(current_step, total_steps, mode_type, real_bg):
|
| 169 |
+
# กราฟแสดง K-Space ที่ค่อยๆ ถูกเติม
|
| 170 |
+
fig, ax = plt.subplots(figsize=(4, 4))
|
| 171 |
+
|
| 172 |
+
# สร้างภาพเปล่าสีดำ
|
| 173 |
+
display_img = np.zeros((224, 224))
|
| 174 |
+
|
| 175 |
+
# คำนวณขอบเขตบรรทัดที่จะเติม
|
| 176 |
+
lines_per_step = 224 // total_steps
|
| 177 |
+
current_line_idx = current_step * lines_per_step
|
| 178 |
+
|
| 179 |
+
if mode_type == "ข้อมูลจำลอง (Simulated)":
|
| 180 |
+
# สร้างภาพเส้นจำลองสีเหลืองแนวนอน
|
| 181 |
+
Y, X = np.ogrid[:224, :224]
|
| 182 |
+
# จำลองสัญญาณให้ตรงกลางสว่าง ขอบมืด
|
| 183 |
+
intensity = np.exp(-((Y - 112)**2) / (2 * 50**2))
|
| 184 |
+
for i in range(current_line_idx + lines_per_step):
|
| 185 |
+
display_img[i, :] = intensity[i, 0] * 255
|
| 186 |
+
|
| 187 |
+
ax.imshow(display_img, cmap='inferno', extent=[-112, 112, -112, 112], vmin=0, vmax=255)
|
| 188 |
+
else:
|
| 189 |
+
# ใช้ข้อมูล kspace.mat จริง
|
| 190 |
+
if current_step >= total_steps - 1:
|
| 191 |
+
display_img = real_bg
|
| 192 |
+
else:
|
| 193 |
+
display_img[:current_line_idx + lines_per_step, :] = real_bg[:current_line_idx + lines_per_step, :]
|
| 194 |
+
|
| 195 |
+
ax.imshow(display_img, cmap='gray', extent=[-112, 112, -112, 112], vmin=0, vmax=1)
|
| 196 |
+
|
| 197 |
+
ax.set_xlim(-112, 112)
|
| 198 |
+
ax.set_ylim(112, -112) # Invert Y axis ให้เติมจากบนลงล่างตามกราฟ Gy
|
| 199 |
+
ax.axhline(0, color='white', linewidth=0.5, linestyle='--')
|
| 200 |
+
ax.axvline(0, color='white', linewidth=0.5, linestyle='--')
|
| 201 |
+
ax.set_title("K-Space Filling", fontweight='bold')
|
| 202 |
+
ax.axis('off')
|
| 203 |
+
return get_image_from_plot(fig)
|
| 204 |
+
|
| 205 |
+
|
| 206 |
# ==========================================================
|
| 207 |
# --- 3. ส่วนแสดงผลเว็บ (ใช้ Container บีบขอบซ้าย-ขวา) ---
|
| 208 |
# ==========================================================
|
| 209 |
|
|
|
|
| 210 |
_, col_main, _ = st.columns([1, 6, 1])
|
| 211 |
|
| 212 |
with col_main:
|
|
|
|
| 224 |
ข้อมูลใน k-space มักจะถูกนำมาแสดงผลในรูปแบบตารางสี่เหลี่ยม (Grid) โดยมีแกนหลักคือ **kx (แนวนอน - Frequency)** และ **ky (แนวตั้ง - Phase)** จุดสำคัญคือ **แกน kx และ ky เหล่านี้ ไม่ได้บอกตำแหน่งพิกัดในภาพอวัยวะ** แต่มันคือแกนที่บอกถึงลักษณะของ **"ความถี่เชิงพื้นที่"** ที่เป็นคลื่น (Sinusoidal wave) ด้วยเหตุนี้ **จุดแต่ละจุดบน K-space จึงไม่ได้จับคู่แบบ 1 ต่อ 1 กับพิกเซลบนภาพ MRI** (เช่น จุดมุมซ้ายบนของ K-space ไม่ได้สร้างภาพมุมซ้ายบนของอวัยวะ)
|
| 225 |
""")
|
| 226 |
|
|
|
|
| 227 |
_, col_img_center, _ = st.columns([1.5, 3, 1.5])
|
| 228 |
with col_img_center:
|
| 229 |
if os.path.exists("Screenshot 2026-05-07 205051.png"):
|
|
|
|
| 233 |
else:
|
| 234 |
st.info("กรุณาอัปโหลดรูปภาพ Screenshot 2026-05-07 205051 (เป็น .png หรือ .jpg ก็ได้)")
|
| 235 |
|
| 236 |
+
# ---------------------------------------------------------
|
| 237 |
+
# NEW INTERACTIVE: การเติม K-Space ทีละบรรทัด (Line-by-Line Filling)
|
| 238 |
+
# ---------------------------------------------------------
|
| 239 |
+
st.markdown("### 🎛️ จำลองการเติมข้อมูล K-Space ตามรอบของ Phase Encoding (Gy)")
|
| 240 |
+
st.markdown("สังเกตการทำงานของเกรเดียนท์ Gy ด้านซ้าย ว่าในแต่ละรอบการกระตุ้น (TR) จะดึงข้อมูลมาเติมใน K-Space ทีละบรรทัดอย่างไร")
|
| 241 |
+
|
| 242 |
+
# State Management สำหรับการควบคุมแอนิเมชัน
|
| 243 |
+
total_anim_steps = 15
|
| 244 |
+
if 'fill_step' not in st.session_state:
|
| 245 |
+
st.session_state.fill_step = 0
|
| 246 |
+
|
| 247 |
+
def step_forward():
|
| 248 |
+
if st.session_state.fill_step < total_anim_steps - 1:
|
| 249 |
+
st.session_state.fill_step += 1
|
| 250 |
+
|
| 251 |
+
def step_backward():
|
| 252 |
+
if st.session_state.fill_step > 0:
|
| 253 |
+
st.session_state.fill_step -= 1
|
| 254 |
+
|
| 255 |
+
def run_all():
|
| 256 |
+
st.session_state.fill_step = total_anim_steps - 1
|
| 257 |
+
|
| 258 |
+
def reset_anim():
|
| 259 |
+
st.session_state.fill_step = 0
|
| 260 |
+
|
| 261 |
+
# แถบควบคุมบน
|
| 262 |
+
_, col_ctrl1, col_ctrl2, _ = st.columns([1, 2, 2, 1])
|
| 263 |
+
with col_ctrl1:
|
| 264 |
+
data_mode = st.radio("เลือกรูปแบบข้อมูลแสดงผล:", ["ข้อมูลจำลอง (Simulated)", "ข้อมูลจริง (kspace.mat)"])
|
| 265 |
+
with col_ctrl2:
|
| 266 |
+
st.write("ควบคุมการเติมบรรทัด:")
|
| 267 |
+
c1, c2, c3, c4 = st.columns(4)
|
| 268 |
+
c1.button("⏮ Reset", on_click=reset_anim)
|
| 269 |
+
c2.button("◀ ก่อนหน้า", on_click=step_backward)
|
| 270 |
+
c3.button("ถัดไป ▶", on_click=step_forward)
|
| 271 |
+
c4.button("⏭ รันจนจบ", on_click=run_all)
|
| 272 |
+
|
| 273 |
+
st.progress((st.session_state.fill_step + 1) / total_anim_steps)
|
| 274 |
+
|
| 275 |
+
# แสดงผลรูปภาพ
|
| 276 |
+
_, col_anim1, col_anim2, _ = st.columns([1.5, 2, 3, 1.5])
|
| 277 |
+
with col_anim1:
|
| 278 |
+
st.image(draw_gy_sequence(st.session_state.fill_step, total_anim_steps), use_container_width=True)
|
| 279 |
+
with col_anim2:
|
| 280 |
+
st.image(draw_kspace_filling(st.session_state.fill_step, total_anim_steps, data_mode, kspace_bg_image), use_container_width=True)
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
st.markdown("---")
|
| 284 |
st.markdown("## 📍 1 จุดบน k-space")
|
| 285 |
|
| 286 |
st.markdown("""
|
|
|
|
| 297 |
- **จุดมืดหรือจาง:** ภาพ MRI แทบจะไม่มีลวดลายชนิดนี้ประกอบอยู่เลย
|
| 298 |
""")
|
| 299 |
|
|
|
|
| 300 |
st.markdown("### 🎛️ ลองปรับตำแหน่งของจุด K-Space เพื่อดูคลื่นความถี่")
|
| 301 |
|
|
|
|
| 302 |
_, col_slide1, col_slide2, _ = st.columns([0.5, 2.5, 2.5, 0.5])
|
| 303 |
with col_slide1:
|
| 304 |
kx_val = st.slider("พิกัด kx (แนวนอน)", -112, 111, 15)
|
|
|
|
| 344 |
อนุญาตให้เฉพาะข้อมูล **ขอบนอก (ความถี่สูง)** ผ่านไปได้ ข้อมูลตรงกลางทิ้งไป ผลลัพธ์ที่ได้คือภาพจะสูญเสียคอนทราสต์จนเกือบมืดสนิท แต่จะปรากฏ **"เส้นขอบร่าง" (Outline)** ขึ้นมาอย่างคมชัด
|
| 345 |
""")
|
| 346 |
|
|
|
|
| 347 |
st.markdown("### 🎛️ ลองปรับตัวกรอง (Interactive Filter)")
|
| 348 |
|
| 349 |
_, col_radio_center, _ = st.columns([2, 3, 2])
|
|
|
|
| 359 |
|
| 360 |
filtered_k, mri_result = apply_filter(kspace_raw, mode, radius)
|
| 361 |
|
|
|
|
| 362 |
_, col_fimg1, col_fimg2, _ = st.columns([0.5, 2.5, 2.5, 0.5])
|
| 363 |
with col_fimg1:
|
| 364 |
st.image(draw_filtered_kspace(filtered_k), caption="ภาพ K-Space ที่ถูกตัวกรอง", use_container_width=True)
|