| 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 |
|
|
| |
| 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) |
|
|
| |
| @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() |
|
|
| 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)) |
| |
| |
| ax.text(1.1, 0, '+kx', fontsize=20, fontweight='bold', va='center') |
| |
| |
| 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') |
| |
| |
| 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) |
| |
| |
| 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') |
| |
| |
| 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') |
|
|
| |
| 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') |
| |
| |
| 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) |
|
|
| |
| |
| |
|
|
| _, 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()) |
|
|
| |
| |
| |
| 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) |
|
|
| |
| 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") |
|
|
| |
| 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 ผลลัพธ์") |
|
|
| |
| |
| st.markdown('<div style="height: 300px;"></div>', unsafe_allow_html=True) |