import streamlit as st
import numpy as np
import base64
import scipy.io
import zipfile
import os
import io
from PIL import Image
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
# --- 1. ตั้งค่าหน้าเว็บ ---
st.set_page_config(layout="wide", page_title="K-Space to MRI")
st.markdown("""
""", unsafe_allow_html=True)
# --- 2. ฟังก์ชันโหลดข้อมูลและการคำนวณ ---
@st.cache_data
def load_kspace_data():
try:
if os.path.exists('kspace.mat'):
mat_data = scipy.io.loadmat('kspace.mat')
elif os.path.exists('kspace.zip'):
with zipfile.ZipFile("kspace.zip", 'r') as zip_ref:
zip_ref.extractall("temp_kspace")
mat_data = scipy.io.loadmat("temp_kspace/kspace.mat")
else:
return np.zeros((224, 224))
return mat_data['kspace']
except:
return np.zeros((224, 224))
kspace_raw = load_kspace_data()
def format_kspace_display(k_data):
k_mag = np.abs(k_data)
max_val = np.max(k_mag)
if max_val == 0:
return np.zeros_like(k_mag, dtype=np.float32)
c = 255.0 / np.log(1 + max_val)
log_img = c * np.log(1 + k_mag)
log_norm = (log_img - np.min(log_img)) / (np.max(log_img) - np.min(log_img) + 1e-8)
log_boosted = np.power(log_norm, 0.3)
return log_boosted
kspace_bg_image = format_kspace_display(kspace_raw)
def get_image_from_plot(fig):
buf = io.BytesIO()
plt.savefig(buf, format='png', dpi=100)
plt.close(fig)
return buf.getvalue() # Return bytes — reliably cacheable by st.cache_data
def st_image(img_bytes, caption=None):
"""Display image as inline base64 data URI.
Prevents layout jiggle: browser renders it instantly with no HTTP round-trip,
so no 0-height placeholder → image-pop-in → scrollbar-flicker cycle."""
b64 = base64.b64encode(img_bytes).decode('utf-8')
cap_html = (f'
'
+ caption + '
') if caption else ''
st.markdown(
f''
f'

'
f'{cap_html}
',
unsafe_allow_html=True
)
@st.cache_data
def draw_kspace_diagram():
fig, ax = plt.subplots(figsize=(6, 6))
# วาดกรอบนอก
rect = plt.Rectangle((-1, -1), 2, 2, fill=False, edgecolor='black', lw=3)
ax.add_patch(rect)
# วาดแกนลูกศรตัดกันตรงกลาง (อยู่ภายในกรอบ)
ax.annotate('', xy=(0.95, 0), xytext=(-0.95, 0), arrowprops=dict(arrowstyle='<|-|>', color='black', lw=2))
ax.annotate('', xy=(0, 0.95), xytext=(0, -0.95), arrowprops=dict(arrowstyle='<|-|>', color='black', lw=2))
# ตัวอักษรบอกทิศทาง +kx, -kx, +ky, -ky
ax.text(1.1, 0, '+kx', fontsize=20, fontweight='bold', va='center')
# ขยับ -kx ออกไปทางซ้ายให้มีช่องว่าง (ha='right' ทำให้ขอบขวาของตัวหนังสืออยู่ที่พิกัด -1.1 ซึ่งไม่ทับกรอบ)
ax.text(-1.1, 0, '-kx', fontsize=20, fontweight='bold', va='center', ha='right')
ax.text(0, 1.1, '+ky', fontsize=20, fontweight='bold', ha='center')
ax.text(0, -1.1, '-ky', fontsize=20, fontweight='bold', ha='center', va='top')
# Label บอกชื่อแกน (เอาไว้ข้างนอกกรอบ)
ax.annotate('', xy=(1, -1.4), xytext=(-1, -1.4), arrowprops=dict(arrowstyle='<|-|>', color='black', lw=4))
ax.text(0, -1.5, 'kx (Frequency)', ha='center', va='top', fontsize=16, fontweight='bold')
ax.annotate('', xy=(-1.5, 1), xytext=(-1.5, -1), arrowprops=dict(arrowstyle='<|-|>', color='black', lw=4))
ax.text(-1.6, 0, 'ky (Phase)', ha='right', va='center', rotation=90, fontsize=16, fontweight='bold')
ax.set_xlim(-2.0, 1.5)
ax.set_ylim(-1.8, 1.5)
ax.axis('off')
# ล็อกระยะขอบให้ตายตัวป้องกันการกระตุก
fig.subplots_adjust(left=0.1, right=0.9, top=0.9, bottom=0.1)
return get_image_from_plot(fig)
@st.cache_data
def draw_kspace_point(kx, ky, bg_image):
fig, ax = plt.subplots(figsize=(4, 4))
ax.imshow(bg_image, cmap='gray', extent=[-112, 112, -112, 112], vmin=0, vmax=1)
ax.plot(kx, ky, 'ro', markersize=6)
ax.annotate('', xy=(kx, ky), xytext=(0, 0), arrowprops=dict(arrowstyle='->', color='yellow', lw=2))
ax.axhline(0, color='white', linewidth=0.5, linestyle='--')
ax.axvline(0, color='white', linewidth=0.5, linestyle='--')
ax.set_xlim(-112, 112)
ax.set_ylim(-112, 112)
ax.axis('off')
fig.subplots_adjust(left=0, right=1, top=1, bottom=0)
return get_image_from_plot(fig)
@st.cache_data
def draw_wave(kx, ky):
fig, ax = plt.subplots(figsize=(4, 4))
x = np.linspace(-112, 112, 224)
y = np.linspace(112, -112, 224)
X, Y = np.meshgrid(x, y)
freq_x = kx / 224.0
freq_y = ky / 224.0
wave = np.cos(2 * np.pi * (freq_x * X + freq_y * Y))
ax.imshow(wave, cmap='gray', extent=[-112, 112, -112, 112])
ax.axis('off')
fig.subplots_adjust(left=0, right=1, top=1, bottom=0)
return get_image_from_plot(fig)
@st.cache_data
def apply_filter(k_data, mode, radius):
Y, X = np.ogrid[:224, :224]
dist = np.sqrt((X - 112)**2 + (Y - 112)**2)
mask = np.ones((224, 224))
if mode == "Low frequency":
mask[dist > radius] = 0
else:
mask[dist < radius] = 0
filtered_k = k_data * mask
img = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift(filtered_k)))
return filtered_k, np.abs(img)
@st.cache_data
def draw_filtered_kspace(filtered_k):
fig, ax = plt.subplots(figsize=(4, 4))
ax.imshow(format_kspace_display(filtered_k), cmap='gray', vmin=0, vmax=1)
ax.axis('off')
fig.subplots_adjust(left=0, right=1, top=1, bottom=0)
return get_image_from_plot(fig)
@st.cache_data
def draw_mri(mri_result):
fig, ax = plt.subplots(figsize=(4, 4))
if np.max(mri_result) > 0:
vmin, vmax = np.percentile(mri_result, (1, 99.5))
else:
vmin, vmax = 0, 1
ax.imshow(np.flipud(mri_result), cmap='gray', vmin=vmin, vmax=vmax)
ax.axis('off')
fig.subplots_adjust(left=0, right=1, top=1, bottom=0)
return get_image_from_plot(fig)
@st.cache_data
def draw_pulse_sequence(current_step, total_steps):
fig, axes = plt.subplots(4, 1, figsize=(5, 6), sharex=True, gridspec_kw={'height_ratios': [1, 1.5, 1, 1]})
t = np.linspace(0, 10, 1000)
# 1. RF
ax = axes[0]
rf90 = np.sinc(t - 1.5) * (t > 0.5) * (t < 2.5)
rf180 = np.sinc((t - 4.5)*2) * (t > 3.5) * (t < 5.5) * 1.5
ax.plot(t, rf90 + rf180, color='black', lw=2)
ax.text(1.5, 1.2, '90°', ha='center', fontsize=12, fontweight='bold')
ax.text(4.5, 1.7, '180°', ha='center', fontsize=12, fontweight='bold')
ax.text(0, 0.5, 'RF', fontsize=14, fontweight='bold', va='center', ha='right', transform=ax.get_yaxis_transform())
ax.axis('off')
# 2. Gy
ax = axes[1]
gy_vals = np.linspace(1, -1, total_steps)
for i, gy_amp in enumerate(gy_vals):
gy = np.zeros_like(t)
gy[(t > 2.5) & (t < 3.5)] = gy_amp
if i == current_step:
ax.plot(t, gy, color='red', lw=3, zorder=10)
else:
ax.plot(t, gy, color='black', lw=1, alpha=0.3)
ax.text(0, 0, 'Gy', fontsize=14, fontweight='bold', va='center', ha='right', transform=ax.get_yaxis_transform())
ax.axis('off')
# 3. Gx
ax = axes[2]
gx = np.zeros_like(t)
gx[(t > 2.5) & (t < 3.5)] = -0.5
gx[(t > 6) & (t < 9)] = 0.5
ax.plot(t, gx, color='black', lw=2)
ax.fill_between(t, gx, 0, alpha=0.3, color='gray')
ax.text(0, 0, 'Gx', fontsize=14, fontweight='bold', va='center', ha='right', transform=ax.get_yaxis_transform())
ax.axis('off')
# 4. Echo
ax = axes[3]
echo_env = np.exp(-((t - 7.5)**2) / 0.5) * (t > 6) * (t < 9)
ax.plot(t, echo_env, color='black', lw=2)
ax.fill_between(t, echo_env, 0, alpha=0.3, color='gray')
ax.text(0, 0.5, 'Signal\n(Echo)', fontsize=14, fontweight='bold', va='center', ha='right', transform=ax.get_yaxis_transform())
ax.axis('off')
# ล็อกระยะกรอบตายตัวเพื่อหยุดอาการสั่น
fig.subplots_adjust(left=0.2, right=0.95, top=0.95, bottom=0.05, hspace=0.2)
return get_image_from_plot(fig)
@st.cache_data
def draw_kspace_filling(current_step, total_steps):
fig, ax = plt.subplots(figsize=(5, 6))
ax.set_facecolor('black')
ax.axhline(0, color='gray', lw=1, ls='--')
ax.axvline(0, color='gray', lw=1, ls='--')
y_vals = np.linspace(90, -90, total_steps)
for i in range(current_step + 1):
y = y_vals[i]
if i == current_step:
ax.annotate('', xy=(100, y), xytext=(-100, y), arrowprops=dict(arrowstyle='->', color='red', lw=3))
else:
ax.annotate('', xy=(100, y), xytext=(-100, y), arrowprops=dict(arrowstyle='->', color='white', lw=1.5))
for i in range(current_step):
ax.plot([100, -100], [y_vals[i], y_vals[i+1]], color='gray', ls=':', lw=1)
ax.set_xlim(-112, 112)
ax.set_ylim(-112, 112)
ax.set_title("K-Space Trajectory (Simulated)", fontweight='bold', color='white')
fig.patch.set_facecolor('black')
ax.axis('off')
# ล็อกระยะกรอบตายตัวเพื่อหยุดอาการสั่น
fig.subplots_adjust(left=0.05, right=0.95, top=0.9, bottom=0.05)
return get_image_from_plot(fig)
# ==========================================================
# --- 3. ส่วนแสดงผลเว็บ ---
# ==========================================================
_, col_main, _ = st.columns([1, 6, 1])
with col_main:
st.markdown('K-space to MRI image
', unsafe_allow_html=True)
st.markdown("""
**K-space คืออะไร?** เมื่อเรานำผู้ป่วยเข้าเครื่อง MRI และส่งคลื่น RF เข้าไปกระตุ้น สัญญาณที่ได้มานั้นจะยังไม่ได้ออกมาเป็นภาพอวัยวะทันที แต่จะถูกนำไปเก็บรวบรวมไว้ในพื้นที่ที่เรียกว่า **"K-space"** ซึ่งเป็น **พื้นที่เก็บข้อมูลดิบแบบสองมิติ** ก่อนจะนำไปผ่านกระบวนการทางคณิตศาสตร์ที่เรียกว่า **Fourier Transform** เพื่อให้ได้มาซึ่งภาพ MRI
ข้อมูลใน K-Space ถูกเก็บในรูปแบบ **ความถี่เชิงพื้นที่ (Spatial Frequency)** โดยพิกัดในตารางของ K-space ถูกสร้างขึ้นจากการทำงานของสนามแม่เหล็กเกรเดียนท์ 2 แกน ได้แก่ **Gx (ทำหน้าที่เข้ารหัสแนวความถี่)** และ **Gy (ทำหน้าที่เข้ารหัสแนวเฟส)** ซึ่งจะกำหนดว่าสัญญาณที่มีความถี่ต่างกันต้องถูกจัดเก็บไว้ตรงจุดไหนในพิกัด
""")
st.markdown("## 🧩 องค์ประกอบของ K-Space")
st.markdown("""
ข้อมูลใน k-space มักจะถูกนำมาแสดงผลในรูปแบบตารางสี่เหลี่ยม (Grid) โดยมีแกนหลักคือ **kx (แนวนอน - Frequency)** และ **ky (แนวตั้ง - Phase)** จุดสำคัญคือ **แกน kx และ ky เหล่านี้ ไม่ได้บอกตำแหน่งพิกัดในภาพอวัยวะ** แต่มันคือแกนที่บอกถึงลักษณะของ **"ความถี่เชิงพื้นที่"** ที่เป็นคลื่น (Sinusoidal wave) ด้วยเหตุนี้ **จุดแต่ละจุดบน K-space จึงไม่ได้จับคู่แบบ 1 ต่อ 1 กับพิกเซลบนภาพ MRI** (เช่น จุดมุมซ้ายบนของ K-space ไม่ได้สร้างภาพมุมซ้ายบนของอวัยวะ)
""")
_, col_img_center, _ = st.columns([2.5, 3, 2.5])
with col_img_center:
st_image(draw_kspace_diagram())
# ---------------------------------------------------------
# K-Space Trajectories
# ---------------------------------------------------------
st.markdown("## 🛤️ K-Space Trajectories")
st.markdown("""
**การบันทึกข้อมูลลงใน K-space สามารถทำได้หลายรูปแบบ** โดยจะยกตัวอย่างให้การเก็บข้อมูลจะดำเนินไปทีละบรรทัด โดยสัญญาณจะเกิดขึ้นหลังจากกระตุ้นด้วย RF Pulse โดยมีลำดับดังนี้
1. กระบวนการเริ่มต้นขึ้นเมื่อสนามแม่เหล็กเกรเดียนท์ **Gx** และ **Gy** เริ่มทำงานพร้อมกันเพื่อสร้างพิกัดอ้างอิง
2. ระบบจะเริ่มบันทึกข้อมูลจุดแรกของเฟสในแถวที่ 1 โดยจะเริ่มต้นที่ค่าแอมปลิจูดของ **Gy ทางฝั่งบวกก่อน (พิกัดด้านบนของ K-Space)**
3. สัญญาณจะถูกบันทึกไล่ไปตามแกน **Gx (จากซ้าย -kx ไป ขวา +kx)** จนได้ข้อมูลครบถ้วนเต็ม 1 แถว
4. เมื่อจบแถวแรก ระบบจะเริ่มกระบวนการใหม่เพื่อบันทึกข้อมูลในเฟสแถวที่ 2, 3, 4 ต่อไปเรื่อยๆ
5. การวนรอบนี้จะดำเนินต่อไปพร้อมกับการเปลี่ยนค่า **Gy ให้ลดลงจนไปถึงค่าทางฝั่งลบ (พิกัดด้านล่าง)** เมื่อบันทึกครบทุกแถวตามจำนวนความละเอียดที่ตั้งไว้ จะถือว่าสิ้นสุดกระบวนการเก็บข้อมูล MRI แบบสองมิติลงบน K-space
""")
total_anim_steps = 15
if 'fill_step' not in st.session_state:
st.session_state.fill_step = 0
def step_forward():
if st.session_state.fill_step < total_anim_steps - 1:
st.session_state.fill_step += 1
def step_backward():
if st.session_state.fill_step > 0:
st.session_state.fill_step -= 1
def run_all():
st.session_state.fill_step = total_anim_steps - 1
def reset_anim():
st.session_state.fill_step = 0
_, col_ctrl, _ = st.columns([1, 4, 1])
with col_ctrl:
st.write("ควบคุมการเติมบรรทัด (Gy):")
c1, c2, c3, c4 = st.columns(4)
c1.button("⏮ Reset", on_click=reset_anim)
c2.button("◀ ก่อนหน้า", on_click=step_backward)
c3.button("ถัดไป ▶", on_click=step_forward)
c4.button("⏭ รันจนจบ", on_click=run_all)
st.progress((st.session_state.fill_step + 1) / total_anim_steps)
# ถอด st.empty ออกให้หมดเพื่อให้ Streamlit จัดการ Layout ปกติ ภาพจะไม่กระโดดยุบตัว
col_anim1, col_anim2 = st.columns([1, 1])
with col_anim1:
st_image(draw_pulse_sequence(st.session_state.fill_step, total_anim_steps))
with col_anim2:
st_image(draw_kspace_filling(st.session_state.fill_step, total_anim_steps))
st.markdown("""
---
**📌 หมายเหตุ:** หากพูดถึงจำนวนครั้ง Phase Encoding ที่มากขึ้น หรือจำนวนบรรทัดการเก็บข้อมูลมากขึ้น ภาพก็จะมีความละเอียด (Resolution) ที่มากขึ้น เช่น เพิ่มขึ้นเป็น 128 หรือ 256 บรรทัด เป็นต้น
""")
st.markdown("---")
st.markdown("## 📍 1 จุดบน k-space")
# Center Text
st.markdown("""
k-space 1 จุด = ข้อมูลของภาพทั้งภาพ และ ภาพ 1 พิกเซล = ผลรวมของ k-space ทุกจุด
1 จุดใน k-space = แผ่นลวดลายคลื่น 1 แผ่น (2D Sinusoidal Wave)
""", unsafe_allow_html=True)
st.markdown("""
1. **ตำแหน่งของจุด (พิกัด kx, ky) บอก "ความถี่" และ "ทิศทาง"**
- **ระยะห่างจากศูนย์กลาง (ความถี่):** ยิ่งจุดนี้อยู่ไกลจากจุดศูนย์กลาง k-space มากเท่าไหร่ แผ่นลวดลายคลื่นก็จะยิ่ง **"ถี่"** (High frequency) ความยาวคลื่นจะสั้นลง มีความละเอียด เส้นจะห่างกันน้อยลง ทำให้ภาพมีดีเทลที่ชัดเจนขึ้น ในขณะที่ส่วนที่ใกล้จุดศูนย์กลาง คลื่นจะมีความถี่น้อย ความยาวคลื่นมาก มีลักษณะเป็นก้อนใหญ่ ๆ (สร้างรูปร่างโดยรวมของภาพ)
- **มุมของจุด (ทิศทาง):** ตำแหน่งของจุดเมื่อเทียบกับจุดศูนย์กลาง จะเป็นตัวบอกว่าแผ่นลวดลายคลื่นนี้จะ **"เอียง"** ไปในทิศทางไหน
- **จุดศูนย์กลางเป๊ะ (Origin):** จะเป็นคลื่นที่ไม่มีความถี่ ค่าความถี่ของคลื่นเป็นศูนย์ ดังนั้นตรงกลางจะไม่เห็นลักษณะรูปคลื่น
2. **ความสว่างของจุด (Amplitude / Magnitude) บอก "ปริมาณ/น้ำหนัก (Weight)"**
- ความสว่างของจุดไม่ได้แปลว่าภาพ MRI ตรงนั้นจะสว่าง แต่มันบอกถึง **"ปริมาณ/น้ำหนัก (Weight)"**
- **จุดสว่างมาก:** ภาพ MRI มีแผ่นลวดลายชนิดนี้เป็นส่วนประกอบอยู่ **เยอะมาก (มีความสำคัญต่อภาพสูง)**
- **จุดมืดหรือจาง:** ภาพ MRI แทบจะไม่มีลวดลายชนิดนี้ประกอบอยู่เลย
""")
st.markdown("### 🎛️ ลองปรับตำแหน่งของจุด K-Space เพื่อดูคลื่นความถี่")
_, col_slide1, col_slide2, _ = st.columns([0.5, 2.5, 2.5, 0.5])
with col_slide1:
kx_val = st.slider("พิกัด kx (แนวนอน)", -112, 111, 15)
with col_slide2:
ky_val = st.slider("พิกัด ky (แนวตั้ง)", -112, 111, 20)
_, col_img1, col_img2, _ = st.columns([0.5, 2.5, 2.5, 0.5])
with col_img1:
st_image(draw_kspace_point(kx_val, ky_val, kspace_bg_image), caption="พิกัด K-Space (มีเส้นบอกทิศทาง)")
with col_img2:
st_image(draw_wave(kx_val, ky_val), caption="แผ่นลวดลายคลื่น (2D Sinusoidal Wave)")
with st.expander("🔍 สังเกตลักษณะคลื่น (คลิกเพื่อดูคำอธิบายเพิ่มเติม)"):
st.markdown("""
**ลองปรับเลื่อนพิกัดเพื่อสังเกตความเปลี่ยนแปลง:**
- **ยิ่งจุดอยู่ใกล้ศูนย์กลาง:** คลื่นจะมีความถี่น้อย ความยาวคลื่นมาก มีลักษณะเป็นก้อนใหญ่ ๆ (แสดงถึงรูปร่างโดยรวม)
- **ยิ่งจุดอยู่ไกลศูนย์กลาง:** คลื่นจะมีความถี่สูง ความยาวคลื่นสั้นลง เส้นห่างกันน้อยลง มีความละเอียดสูง (แสดงส่วนที่เป็นดีเทลหรือเส้นขอบ)
- **เมื่อจุดอยู่ตรงกลางเป๊ะ (kx=0, ky=0):** จะไม่มีรูปคลื่นปรากฏให้เห็นเลย เพราะค่าความถี่ของคลื่นเป็นศูนย์
""")
st.markdown("## 🔄 Inverse Fourier Transform")
st.markdown("""
เมื่อเก็บข้อมูลจนเต็มพื้นที่ k-space เราจะใช้กระบวนการทางคณิตศาสตร์ **2D Inverse Fourier Transform (2D-iFT)** เปลี่ยนข้อมูลความถี่กลับไปเป็นข้อมูลภาพ (Spatial Domain)
ภาพ MRI เกิดจากการนำ **คลื่นความถี่จากทุกจุดใน k-space มาซ้อนทับกัน**
- บริเวณไหนที่เป็นเนื้อเยื่อจริง คลื่นที่มีเฟสตรงกันจะรวมตัวกันแบบเสริมฤทธิ์ ทำให้เกิด **จุดสว่าง**
- บริเวณไหนที่เป็นช่องว่าง คลื่นที่มีเฟสตรงข้ามจะหักล้างกัน ทำให้เกิด **จุดมืด**
""")
st.markdown("## 📊 ความถี่เชิงพื้นที่ (Spatial Frequency) คือ")
st.markdown("""
ในทางสัญญาณภาพ ความถี่ไม่ได้หมายถึงความเร็วของเวลา แต่หมายถึง **"อัตราการเปลี่ยนแปลงความเข้มของแสงในพื้นที่หนึ่งๆ"**
1. **ความถี่เชิงพื้นที่ต่ำ (Low Spatial Frequency)**
- **คืออะไร:** พื้นที่ที่ความสว่าง **ค่อยๆ เปลี่ยน** หรือ **เหมือนเดิมเป็นบริเวณกว้าง**
- **ตัวอย่างในภาพ MRI:** บริเวณเนื้อเยื่อก้อนใหญ่ๆ เช่น เนื้อตับ หรือเนื้อสมอง ที่มีสีโทนเดียวกันกินพื้นที่กว้าง
- **ตำแหน่งใน k-space:** ข้อมูลเหล่านี้จะรวมตัวกันอยู่บริเวณ **"ตรงกลาง"**
- **หน้าที่หลัก:** สร้าง **"รูปร่างรวมๆ และคอนทราสต์ (Contrast)"** ให้เรารู้ว่านี่คือก้อนอวัยวะอะไร
2. **ความถี่เชิงพื้นที่สูง (High Spatial Frequency)**
- **คืออะไร:** พื้นที่ที่ความสว่าง **เปลี่ยนแบบฉับพลันและรวดเร็ว** ภายในระยะทางสั้นๆ
- **ตัวอย่างในภาพ MRI:** ขอบของอวัยวะ (Edges) หรือรายละเอียดเส้นเลือดเส้นเล็กๆ
- **ตำแหน่ง in k-space:** ข้อมูลเหล่านี้จะกระจายตัวอยู่บริเวณ **"ขอบนอก"**
- **หน้าที่หลัก:** สร้าง **"ความคมชัด (Resolution) และรายละเอียดเล็กๆ"** ทำให้ภาพไม่เบลอ
""")
st.markdown("### 🎛️ ลองเลือกช่วงความถี่ (Interactive)")
_, col_radio_center, _ = st.columns([2, 3, 2])
with col_radio_center:
mode = st.radio("เลือกช่วงความถี่:", ["Low frequency", "High frequency"], horizontal=True)
_, col_fslide, _ = st.columns([1.5, 5, 1.5])
with col_fslide:
if mode == "Low frequency":
radius = st.slider("ปรับระดับความถี่ที่ยอมให้ผ่าน (รัศมีจากตรงกลาง)", 1, 160, 160)
else:
radius = st.slider("ปรับระดับการตัดข้อมูลส่วนกลาง (รัศมีจากตรงกลาง)", 0, 160, 0)
filtered_k, mri_result = apply_filter(kspace_raw, mode, radius)
_, col_fimg1, col_fimg2, _ = st.columns([0.5, 2.5, 2.5, 0.5])
with col_fimg1:
st_image(draw_filtered_kspace(filtered_k), caption="ภาพ K-Space ที่ถูกเลือก")
with col_fimg2:
st_image(draw_mri(mri_result), caption="ภาพ MRI ผลลัพธ์")
# Spacer: keeps page taller than viewport at all zoom levels so the
# scrollbar never disappears and never causes a layout-shift jiggle.
st.markdown('', unsafe_allow_html=True)