Update app.py
Browse files
app.py
CHANGED
|
@@ -57,7 +57,6 @@ st.markdown("""
|
|
| 57 |
background-color: #1E88E5;
|
| 58 |
color: white;
|
| 59 |
}
|
| 60 |
-
/* Info box — force dark text in both light and dark mode */
|
| 61 |
.kspace-info-box {
|
| 62 |
text-align: center;
|
| 63 |
background-color: #dbeafe !important;
|
|
@@ -115,12 +114,9 @@ def get_image_from_plot(fig):
|
|
| 115 |
buf = io.BytesIO()
|
| 116 |
plt.savefig(buf, format='png', dpi=100)
|
| 117 |
plt.close(fig)
|
| 118 |
-
return buf.getvalue()
|
| 119 |
|
| 120 |
def st_image(img_bytes, caption=None):
|
| 121 |
-
"""Display image as inline base64 data URI.
|
| 122 |
-
Prevents layout jiggle: browser renders it instantly with no HTTP round-trip,
|
| 123 |
-
so no 0-height placeholder → image-pop-in → scrollbar-flicker cycle."""
|
| 124 |
b64 = base64.b64encode(img_bytes).decode('utf-8')
|
| 125 |
cap_html = (f'<p style="text-align:center;font-size:14px;color:gray;margin-top:4px">'
|
| 126 |
+ caption + '</p>') if caption else ''
|
|
@@ -135,27 +131,19 @@ def st_image(img_bytes, caption=None):
|
|
| 135 |
def draw_kspace_diagram():
|
| 136 |
fig, ax = plt.subplots(figsize=(6, 6))
|
| 137 |
|
| 138 |
-
# วาดกรอบนอก
|
| 139 |
rect = plt.Rectangle((-1, -1), 2, 2, fill=False, edgecolor='black', lw=3)
|
| 140 |
ax.add_patch(rect)
|
| 141 |
|
| 142 |
-
# วาดแกนลูกศรตัดกันตรงกลาง (อยู่ภายในกรอบ)
|
| 143 |
ax.annotate('', xy=(0.95, 0), xytext=(-0.95, 0), arrowprops=dict(arrowstyle='<|-|>', color='black', lw=2))
|
| 144 |
ax.annotate('', xy=(0, 0.95), xytext=(0, -0.95), arrowprops=dict(arrowstyle='<|-|>', color='black', lw=2))
|
| 145 |
|
| 146 |
-
# ตัวอักษรบอกทิศทาง +kx, -kx, +ky, -ky
|
| 147 |
ax.text(1.1, 0, '+kx', fontsize=20, fontweight='bold', va='center')
|
| 148 |
-
|
| 149 |
-
# ขยับ -kx ออกไปทางซ้ายให้มีช่องว่าง (ha='right' ทำให้ขอบขวาของตัวหนังสืออยู่ที่พิกัด -1.1 ซึ่งไม่ทับกรอบ)
|
| 150 |
ax.text(-1.1, 0, '-kx', fontsize=20, fontweight='bold', va='center', ha='right')
|
| 151 |
-
|
| 152 |
ax.text(0, 1.1, '+ky', fontsize=20, fontweight='bold', ha='center')
|
| 153 |
ax.text(0, -1.1, '-ky', fontsize=20, fontweight='bold', ha='center', va='top')
|
| 154 |
|
| 155 |
-
# Label บอกชื่อแกน (เอาไว้ข้างนอกกรอบ)
|
| 156 |
ax.annotate('', xy=(1, -1.4), xytext=(-1, -1.4), arrowprops=dict(arrowstyle='<|-|>', color='black', lw=4))
|
| 157 |
ax.text(0, -1.5, 'kx (Frequency)', ha='center', va='top', fontsize=16, fontweight='bold')
|
| 158 |
-
|
| 159 |
ax.annotate('', xy=(-1.5, 1), xytext=(-1.5, -1), arrowprops=dict(arrowstyle='<|-|>', color='black', lw=4))
|
| 160 |
ax.text(-1.6, 0, 'ky (Phase)', ha='right', va='center', rotation=90, fontsize=16, fontweight='bold')
|
| 161 |
|
|
@@ -163,7 +151,6 @@ def draw_kspace_diagram():
|
|
| 163 |
ax.set_ylim(-1.8, 1.5)
|
| 164 |
ax.axis('off')
|
| 165 |
|
| 166 |
-
# ล็อกระยะขอบให้ตายตัวป้องกันการกระตุก
|
| 167 |
fig.subplots_adjust(left=0.1, right=0.9, top=0.9, bottom=0.1)
|
| 168 |
return get_image_from_plot(fig)
|
| 169 |
|
|
@@ -231,59 +218,94 @@ def draw_mri(mri_result):
|
|
| 231 |
|
| 232 |
@st.cache_data
|
| 233 |
def draw_pulse_sequence(current_step, total_steps):
|
| 234 |
-
|
| 235 |
-
|
|
|
|
| 236 |
|
| 237 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
ax = axes[0]
|
| 239 |
-
rf90 = np.sinc((t -
|
| 240 |
-
rf180 = np.sinc((t -
|
| 241 |
ax.plot(t, rf90 + rf180, color='black', lw=2)
|
| 242 |
-
ax.text(
|
| 243 |
-
ax.text(
|
| 244 |
-
|
| 245 |
-
ax.text(-0.05, 0.2, 'RF', fontsize=14, fontweight='bold', va='center', ha='right', transform=ax.transAxes)
|
| 246 |
ax.axis('off')
|
| 247 |
|
| 248 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
ax = axes[1]
|
| 250 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
for i, gy_amp in enumerate(gy_vals):
|
| 252 |
gy = np.zeros_like(t)
|
| 253 |
-
gy[(t >
|
| 254 |
if i == current_step:
|
| 255 |
-
ax.plot(t, gy, color='
|
| 256 |
else:
|
| 257 |
-
ax.plot(t, gy, color='
|
| 258 |
-
ax.
|
|
|
|
|
|
|
| 259 |
ax.axis('off')
|
| 260 |
|
| 261 |
-
#
|
| 262 |
-
ax = axes[
|
| 263 |
gx = np.zeros_like(t)
|
| 264 |
-
|
| 265 |
-
gx[(t >
|
| 266 |
-
|
| 267 |
-
ax.
|
| 268 |
-
ax.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
ax.axis('off')
|
| 270 |
|
| 271 |
-
#
|
| 272 |
-
ax = axes[
|
| 273 |
-
|
| 274 |
-
echo_env = np.exp(-((t - 8.5)**2) / 0.3) * (t > 7.5) * (t < 9.5)
|
| 275 |
ax.plot(t, echo_env, color='black', lw=2)
|
| 276 |
ax.fill_between(t, echo_env, 0, alpha=0.3, color='gray')
|
| 277 |
-
ax.text(-0.05, 0.5, '
|
| 278 |
ax.axis('off')
|
| 279 |
|
| 280 |
-
# ถ่างระยะขอบซ้าย
|
| 281 |
-
fig.subplots_adjust(left=0.
|
| 282 |
return get_image_from_plot(fig)
|
| 283 |
|
| 284 |
@st.cache_data
|
| 285 |
def draw_kspace_filling(current_step, total_steps):
|
| 286 |
-
fig, ax = plt.subplots(figsize=(5, 6))
|
| 287 |
|
| 288 |
ax.set_facecolor('black')
|
| 289 |
ax.axhline(0, color='gray', lw=1, ls='--')
|
|
@@ -308,7 +330,6 @@ def draw_kspace_filling(current_step, total_steps):
|
|
| 308 |
fig.patch.set_facecolor('black')
|
| 309 |
|
| 310 |
ax.axis('off')
|
| 311 |
-
# ล็อกระยะกรอบตายตัวเพื่อหยุดอาการสั่น
|
| 312 |
fig.subplots_adjust(left=0.05, right=0.95, top=0.9, bottom=0.05)
|
| 313 |
return get_image_from_plot(fig)
|
| 314 |
|
|
@@ -394,7 +415,6 @@ with col_main:
|
|
| 394 |
st.markdown("---")
|
| 395 |
st.markdown("## 📍 1 จุดบน k-space")
|
| 396 |
|
| 397 |
-
# Center Text
|
| 398 |
st.markdown("""
|
| 399 |
<div class="kspace-info-box">
|
| 400 |
<b>k-space 1 จุด = ข้อมูลของภาพทั้งภาพ และ ภาพ 1 พิกเซล = ผลรวมของ k-space ทุกจุด</b><br><br>
|
|
@@ -482,6 +502,4 @@ with col_main:
|
|
| 482 |
with col_fimg2:
|
| 483 |
st_image(draw_mri(mri_result), caption="ภาพ MRI ผลลัพธ์")
|
| 484 |
|
| 485 |
-
# Spacer: keeps page taller than viewport at all zoom levels so the
|
| 486 |
-
# scrollbar never disappears and never causes a layout-shift jiggle.
|
| 487 |
st.markdown('<div style="height: 300px;"></div>', unsafe_allow_html=True)
|
|
|
|
| 57 |
background-color: #1E88E5;
|
| 58 |
color: white;
|
| 59 |
}
|
|
|
|
| 60 |
.kspace-info-box {
|
| 61 |
text-align: center;
|
| 62 |
background-color: #dbeafe !important;
|
|
|
|
| 114 |
buf = io.BytesIO()
|
| 115 |
plt.savefig(buf, format='png', dpi=100)
|
| 116 |
plt.close(fig)
|
| 117 |
+
return buf.getvalue()
|
| 118 |
|
| 119 |
def st_image(img_bytes, caption=None):
|
|
|
|
|
|
|
|
|
|
| 120 |
b64 = base64.b64encode(img_bytes).decode('utf-8')
|
| 121 |
cap_html = (f'<p style="text-align:center;font-size:14px;color:gray;margin-top:4px">'
|
| 122 |
+ caption + '</p>') if caption else ''
|
|
|
|
| 131 |
def draw_kspace_diagram():
|
| 132 |
fig, ax = plt.subplots(figsize=(6, 6))
|
| 133 |
|
|
|
|
| 134 |
rect = plt.Rectangle((-1, -1), 2, 2, fill=False, edgecolor='black', lw=3)
|
| 135 |
ax.add_patch(rect)
|
| 136 |
|
|
|
|
| 137 |
ax.annotate('', xy=(0.95, 0), xytext=(-0.95, 0), arrowprops=dict(arrowstyle='<|-|>', color='black', lw=2))
|
| 138 |
ax.annotate('', xy=(0, 0.95), xytext=(0, -0.95), arrowprops=dict(arrowstyle='<|-|>', color='black', lw=2))
|
| 139 |
|
|
|
|
| 140 |
ax.text(1.1, 0, '+kx', fontsize=20, fontweight='bold', va='center')
|
|
|
|
|
|
|
| 141 |
ax.text(-1.1, 0, '-kx', fontsize=20, fontweight='bold', va='center', ha='right')
|
|
|
|
| 142 |
ax.text(0, 1.1, '+ky', fontsize=20, fontweight='bold', ha='center')
|
| 143 |
ax.text(0, -1.1, '-ky', fontsize=20, fontweight='bold', ha='center', va='top')
|
| 144 |
|
|
|
|
| 145 |
ax.annotate('', xy=(1, -1.4), xytext=(-1, -1.4), arrowprops=dict(arrowstyle='<|-|>', color='black', lw=4))
|
| 146 |
ax.text(0, -1.5, 'kx (Frequency)', ha='center', va='top', fontsize=16, fontweight='bold')
|
|
|
|
| 147 |
ax.annotate('', xy=(-1.5, 1), xytext=(-1.5, -1), arrowprops=dict(arrowstyle='<|-|>', color='black', lw=4))
|
| 148 |
ax.text(-1.6, 0, 'ky (Phase)', ha='right', va='center', rotation=90, fontsize=16, fontweight='bold')
|
| 149 |
|
|
|
|
| 151 |
ax.set_ylim(-1.8, 1.5)
|
| 152 |
ax.axis('off')
|
| 153 |
|
|
|
|
| 154 |
fig.subplots_adjust(left=0.1, right=0.9, top=0.9, bottom=0.1)
|
| 155 |
return get_image_from_plot(fig)
|
| 156 |
|
|
|
|
| 218 |
|
| 219 |
@st.cache_data
|
| 220 |
def draw_pulse_sequence(current_step, total_steps):
|
| 221 |
+
# ปรับเพิ่มเป็น 5 กราฟเพื่อแทรก G_SS เข้าไปให้เหมือนรูปอ้างอิง
|
| 222 |
+
fig, axes = plt.subplots(5, 1, figsize=(6, 6.5), sharex=True, gridspec_kw={'height_ratios': [1.5, 1, 1, 1, 1]})
|
| 223 |
+
t = np.linspace(0, 11, 1000)
|
| 224 |
|
| 225 |
+
t_90 = 1.5
|
| 226 |
+
t_180 = 5.0
|
| 227 |
+
t_echo = 8.5
|
| 228 |
+
|
| 229 |
+
# 1. Apply RF
|
| 230 |
ax = axes[0]
|
| 231 |
+
rf90 = np.sinc((t - t_90)*4) * (t > t_90 - 0.5) * (t < t_90 + 0.5)
|
| 232 |
+
rf180 = np.sinc((t - t_180)*4) * (t > t_180 - 0.5) * (t < t_180 + 0.5) * 1.5
|
| 233 |
ax.plot(t, rf90 + rf180, color='black', lw=2)
|
| 234 |
+
ax.text(t_90, 1.2, '90°', ha='right', fontsize=12, fontweight='bold')
|
| 235 |
+
ax.text(t_180, 1.7, '180°', ha='left', fontsize=12, fontweight='bold')
|
| 236 |
+
ax.text(-0.05, 0.2, 'Apply RF', fontsize=14, fontweight='bold', va='center', ha='right', transform=ax.transAxes)
|
|
|
|
| 237 |
ax.axis('off')
|
| 238 |
|
| 239 |
+
# เพิ่มเส้นประบอกเวลา (Dashed Lines)
|
| 240 |
+
for ax_i in axes:
|
| 241 |
+
ax_i.axvline(t_90, color='black', linestyle='--', alpha=0.5)
|
| 242 |
+
ax_i.axvline(t_180, color='black', linestyle='--', alpha=0.5)
|
| 243 |
+
ax_i.axvline(t_echo, color='black', linestyle='--', alpha=0.5)
|
| 244 |
+
|
| 245 |
+
# เพิ่มตัวอักษรบอกช่วงเวลา TE และ TE/2
|
| 246 |
+
ax.annotate('', xy=(t_90, 2.2), xytext=(t_180, 2.2), arrowprops=dict(arrowstyle='<->', color='black'))
|
| 247 |
+
ax.text((t_90+t_180)/2, 2.3, 'TE/2', ha='center', va='bottom', fontsize=12, fontweight='bold')
|
| 248 |
+
ax.annotate('', xy=(t_90, 2.8), xytext=(t_echo, 2.8), arrowprops=dict(arrowstyle='<->', color='black'))
|
| 249 |
+
ax.text((t_90+t_echo)/2, 2.9, 'TE', ha='center', va='bottom', fontsize=12, fontweight='bold')
|
| 250 |
+
ax.set_ylim(-0.5, 3.5)
|
| 251 |
+
|
| 252 |
+
# 2. G_SS (Slice Selection - สีเขียวแบบในรูป)
|
| 253 |
ax = axes[1]
|
| 254 |
+
gss = np.zeros_like(t)
|
| 255 |
+
gss[(t > t_90 - 0.5) & (t < t_90 + 0.5)] = 0.8
|
| 256 |
+
gss[(t > t_90 + 0.5) & (t < t_90 + 1.0)] = -0.4
|
| 257 |
+
gss[(t > t_180 - 0.5) & (t < t_180 + 0.5)] = 0.8
|
| 258 |
+
ax.plot(t, gss, color='#7cb342', lw=2)
|
| 259 |
+
ax.fill_between(t, gss, 0, alpha=0.2, color='#7cb342')
|
| 260 |
+
ax.text(-0.05, 0, '$G_{SS}$', fontsize=14, fontweight='bold', va='center', ha='right', transform=ax.transAxes)
|
| 261 |
+
ax.set_ylim(-1.2, 1.2)
|
| 262 |
+
ax.axis('off')
|
| 263 |
+
|
| 264 |
+
# 3. G_PE (Phase Encoding - สีฟ้าแบบในรูป ทำเป็นกล่องเรียงเส้น)
|
| 265 |
+
ax = axes[2]
|
| 266 |
+
gy_vals = np.linspace(0.8, -0.8, total_steps)
|
| 267 |
+
t_pe_start, t_pe_end = 2.8, 3.8
|
| 268 |
for i, gy_amp in enumerate(gy_vals):
|
| 269 |
gy = np.zeros_like(t)
|
| 270 |
+
gy[(t > t_pe_start) & (t < t_pe_end)] = gy_amp
|
| 271 |
if i == current_step:
|
| 272 |
+
ax.plot(t, gy, color='#2c7bb6', lw=3, zorder=10)
|
| 273 |
else:
|
| 274 |
+
ax.plot(t, gy, color='#2c7bb6', lw=1, alpha=0.3)
|
| 275 |
+
ax.plot([t_pe_start, t_pe_end, t_pe_end, t_pe_start, t_pe_start], [0.8, 0.8, -0.8, -0.8, 0.8], color='#2c7bb6', lw=1.5)
|
| 276 |
+
ax.text(-0.05, 0, '$G_{PE}$', fontsize=14, fontweight='bold', va='center', ha='right', transform=ax.transAxes)
|
| 277 |
+
ax.set_ylim(-1.2, 1.2)
|
| 278 |
ax.axis('off')
|
| 279 |
|
| 280 |
+
# 4. G_FE (Frequency Encoding - สีแดง)
|
| 281 |
+
ax = axes[3]
|
| 282 |
gx = np.zeros_like(t)
|
| 283 |
+
# ตามที่คุณบอกเป๊ะ: อันแรกติดลบเพื่อกวาดไปด้านซ้าย
|
| 284 |
+
gx[(t > 2.8) & (t < 3.8)] = -0.6
|
| 285 |
+
gx[(t > t_echo - 1.0) & (t < t_echo + 1.0)] = 0.6
|
| 286 |
+
ax.plot(t, gx, color='#d32f2f', lw=2)
|
| 287 |
+
ax.fill_between(t, gx, 0, alpha=0.2, color='#d32f2f')
|
| 288 |
+
ax.text(3.3, -0.4, '-', color='#d32f2f', fontsize=16, fontweight='bold', ha='center', va='center')
|
| 289 |
+
ax.text(t_echo, 0.4, '+', color='#d32f2f', fontsize=16, fontweight='bold', ha='center', va='center')
|
| 290 |
+
ax.text(-0.05, 0, '$G_{FE}$', fontsize=14, fontweight='bold', va='center', ha='right', transform=ax.transAxes)
|
| 291 |
+
ax.set_ylim(-1.2, 1.2)
|
| 292 |
ax.axis('off')
|
| 293 |
|
| 294 |
+
# 5. Echo Signal
|
| 295 |
+
ax = axes[4]
|
| 296 |
+
echo_env = np.exp(-((t - t_echo)**2) / 0.15) * (t > t_echo - 1.0) * (t < t_echo + 1.0)
|
|
|
|
| 297 |
ax.plot(t, echo_env, color='black', lw=2)
|
| 298 |
ax.fill_between(t, echo_env, 0, alpha=0.3, color='gray')
|
| 299 |
+
ax.text(-0.05, 0.5, 'Echo signal', fontsize=14, fontweight='bold', va='center', ha='right', transform=ax.transAxes)
|
| 300 |
ax.axis('off')
|
| 301 |
|
| 302 |
+
# ถ่างระยะขอบซ้ายให้กว้างพอเพื่อไม่ให้ตัวหนังสือตกจอ
|
| 303 |
+
fig.subplots_adjust(left=0.20, right=0.95, top=0.95, bottom=0.05, hspace=0.1)
|
| 304 |
return get_image_from_plot(fig)
|
| 305 |
|
| 306 |
@st.cache_data
|
| 307 |
def draw_kspace_filling(current_step, total_steps):
|
| 308 |
+
fig, ax = plt.subplots(figsize=(5, 6.5)) # ปรับความสูงให้สัมพันธ์กัน
|
| 309 |
|
| 310 |
ax.set_facecolor('black')
|
| 311 |
ax.axhline(0, color='gray', lw=1, ls='--')
|
|
|
|
| 330 |
fig.patch.set_facecolor('black')
|
| 331 |
|
| 332 |
ax.axis('off')
|
|
|
|
| 333 |
fig.subplots_adjust(left=0.05, right=0.95, top=0.9, bottom=0.05)
|
| 334 |
return get_image_from_plot(fig)
|
| 335 |
|
|
|
|
| 415 |
st.markdown("---")
|
| 416 |
st.markdown("## 📍 1 จุดบน k-space")
|
| 417 |
|
|
|
|
| 418 |
st.markdown("""
|
| 419 |
<div class="kspace-info-box">
|
| 420 |
<b>k-space 1 จุด = ข้อมูลของภาพทั้งภาพ และ ภาพ 1 พิกเซล = ผลรวมของ k-space ทุกจุด</b><br><br>
|
|
|
|
| 502 |
with col_fimg2:
|
| 503 |
st_image(draw_mri(mri_result), caption="ภาพ MRI ผลลัพธ์")
|
| 504 |
|
|
|
|
|
|
|
| 505 |
st.markdown('<div style="height: 300px;"></div>', unsafe_allow_html=True)
|