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)