Test / app.py
Nicha1234's picture
Update app.py
27c5cb9 verified
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("""
<style>
html, body, .stApp {
overflow-x: hidden !important;
}
.stApp {
overflow-y: scroll !important;
}
img {
max-width: 100% !important;
height: auto !important;
}
html, body, [class*="st-"] {
font-size: 18px;
}
.main-title {
text-align: center;
font-size: 45px !important;
font-weight: bold;
color: #1E88E5;
margin-bottom: 20px;
}
h2 {
color: #0D47A1;
border-bottom: 2px solid #1E88E5;
padding-bottom: 10px;
margin-top: 40px;
}
h3 {
color: #1565C0;
margin-top: 20px;
}
div.stButton > button:first-child {
background-color: #f0f2f6;
color: #0D47A1;
border-radius: 8px;
border: 1px solid #1E88E5;
font-weight: bold;
}
div.stButton > button:first-child:hover {
background-color: #1E88E5;
color: white;
}
/* Info box — force dark text in both light and dark mode */
.kspace-info-box {
text-align: center;
background-color: #dbeafe !important;
padding: 15px;
border-radius: 10px;
margin-bottom: 20px;
color: #1e3a5f !important;
}
.kspace-info-box b, .kspace-info-box strong {
color: #1e3a5f !important;
}
.reference-text {
font-size: 14px;
color: #666;
text-align: center;
margin-top: 5px;
}
</style>
""", 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'<p style="text-align:center;font-size:14px;color:gray;margin-top:4px">'
+ caption + '</p>') if caption else ''
st.markdown(
f'<div style="width:100%">'
f'<img src="data:image/png;base64,{b64}" style="width:100%;height:auto;display:block">'
f'{cap_html}</div>',
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('<p class="main-title">K-space to MRI image</p>', 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("""
<div class="kspace-info-box">
<b>k-space 1 จุด = ข้อมูลของภาพทั้งภาพ และ ภาพ 1 พิกเซล = ผลรวมของ k-space ทุกจุด</b><br><br>
<b>1 จุดใน k-space = แผ่นลวดลายคลื่น 1 แผ่น (2D Sinusoidal Wave)</b>
</div>
""", 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('<div style="height: 300px;"></div>', unsafe_allow_html=True)