Update app.py
Browse files
app.py
CHANGED
|
@@ -1,61 +1,86 @@
|
|
| 1 |
import streamlit as st
|
| 2 |
import numpy as np
|
| 3 |
import scipy.io
|
| 4 |
-
from PIL import Image, ImageDraw
|
| 5 |
-
import matplotlib.pyplot as plt
|
| 6 |
from streamlit_image_coordinates import streamlit_image_coordinates
|
| 7 |
|
| 8 |
-
# 1. ตั้งค่าหน้าเว็บ
|
| 9 |
-
st.set_page_config(layout="centered", page_title="K-Space
|
| 10 |
|
| 11 |
-
# ปรับ CSS เพื่อความสวยงาม (ฟอนต์ใหญ่, หัวข้อชัดเจน)
|
| 12 |
st.markdown("""
|
| 13 |
<style>
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
</style>
|
| 21 |
""", unsafe_allow_html=True)
|
| 22 |
|
| 23 |
-
# 2.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
@st.cache_data
|
| 25 |
-
def
|
| 26 |
try:
|
| 27 |
mat = scipy.io.loadmat('kspace.mat')
|
| 28 |
-
|
| 29 |
-
key = 'IM' if 'IM' in mat else ([k for k in mat.keys() if not k.startswith('__')][0])
|
| 30 |
data = mat[key]
|
| 31 |
-
# บังคับขนาด 224x224
|
| 32 |
if data.shape != (224, 224):
|
| 33 |
-
|
| 34 |
-
if data.ndim > 2: data = data[..., 0]
|
| 35 |
-
h, w = data.shape
|
| 36 |
data = data[(h-224)//2:(h+224)//2, (w-224)//2:(w+224)//2]
|
| 37 |
return data
|
| 38 |
except:
|
| 39 |
-
# สร้างข้อมูลจำลอง
|
| 40 |
y, x = np.ogrid[-112:112, -112:112]
|
| 41 |
img = np.zeros((224, 224))
|
| 42 |
-
img[(x/
|
| 43 |
-
img[(x/
|
|
|
|
| 44 |
return np.fft.fftshift(np.fft.fft2(img))
|
| 45 |
|
| 46 |
-
k_data =
|
| 47 |
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
arr = np.log(1 + np.abs(arr))
|
| 52 |
else:
|
| 53 |
-
|
| 54 |
-
|
|
|
|
| 55 |
return Image.fromarray(norm.astype(np.uint8)).convert("RGB")
|
| 56 |
|
| 57 |
# ==========================================
|
| 58 |
-
#
|
| 59 |
# ==========================================
|
| 60 |
|
| 61 |
st.title("K-space to MRI image")
|
|
@@ -64,110 +89,99 @@ st.header("K-space คือ")
|
|
| 64 |
st.write("""เมื่อเรานำผู้ป่วยเข้าเครื่อง MRI และส่งคลื่น RF เข้าไปกระตุ้น เกิดเป็นสัญญาณ MR Signal ที่ได้มานั้นจะยังไม่ได้ออกมาเป็นภาพอวัยวะ แต่จะถูกนำไปเก็บรวบรวมไว้ในพื้นที่ที่เรียกว่า "K-space" ซึ่งเป็นพื้นที่ที่จะเก็บข้อมูลดิบแบบสองมิติ ก่อนจะนำไปผ่านกระบวนการทางคฺณิตศาสตร์ที่เรียกว่า Fourier Transform ให้ได้มาซึ่งภาพ MRI ซึ่งข้อมูลใน K-Sapce ถูกเก็บในรูปแบบ ความถี่เชิงพื้นที่ (Spatial Frequency) พิกัดในตารางของ K-space ถูกสร้างขึ้นจากการทำงานของสนามแม่เหล็กเกรเดียนท์ 2 แกน ได้แก่ Gx (ทำหน้าที่เข้ารหัสในแนวความถี่ Frequency encoding) และ Gy (ทำหน้าที่เข้ารหัสในแนวเฟส Phase encoding) ซึ่งเกรเดียนท์ทั้งสองตัวนี้กำหนดว่า สัญญาณจากโปรตอนที่มีความถี่และเฟสจำเพาะเจาะจงที่ต่างกันนั้น จะต้องถูกนำไปจัดเก็บไว้ตรงจุดไหนในพิกัดของ K-space""")
|
| 65 |
|
| 66 |
st.header("องค์ประกอบของ K-Space")
|
| 67 |
-
|
| 68 |
-
try:
|
| 69 |
-
# สมมติว่าไฟล์รูปองค์ประกอบคือ Notes_260516_022203_1.jpg
|
| 70 |
-
st.image("Notes_260516_022203_1.jpg", caption="องค์ประกอบของ K-Space", use_container_width=True)
|
| 71 |
-
except:
|
| 72 |
-
st.info("💡 [พื้นที่สำหรับรูปองค์ประกอบของ K-Space]")
|
| 73 |
|
| 74 |
st.write("""ข้อมูลใน k-space มักจะถูกนำมาแสดงผลในรูปแบบตารางสี่เหลี่ยม (Grid) โดยมีแกนหลักคือ kx (แนวนอน - Frequency) และ ky (แนวตั้ง - Phase) แต่จุดสำคัญคือ แกน kx และ ky เหล่านี้ ไม่ได้บอกตำแหน่งพิกัด ในภาพ แต่มันคือแกนที่บอกถึงลักษณะของ "ความถี่เชิงพื้นที่ (Spatial Frequencies)" ซึ่งเป็นคลื่นความถี่ Sinusoidal wave ด้วยเหตุนี้ จุดแต่ละจุดบนพิกัด (kx, ky) ใน k-space จึง ไม่ได้จับคู่แบบ 1 ต่อ 1 กับพิกเซล (x, y) บนภาพ MRI (ไม่ได้แปลว่าจุดมุมซ้ายบนใน k-space จะสร้างภาพมุมซ้ายบนของภาพอวัยวะ)""")
|
| 75 |
|
| 76 |
-
# ------
|
| 77 |
-
# ส่วนกดจุด (Interactive 1)
|
| 78 |
-
# ------------------------------------------
|
| 79 |
st.subheader("1 จุดบน k-space")
|
| 80 |
|
| 81 |
-
|
| 82 |
-
|
|
|
|
|
|
|
| 83 |
st.rerun()
|
| 84 |
|
| 85 |
-
|
| 86 |
|
| 87 |
-
with
|
| 88 |
-
st.markdown("**เลือก
|
| 89 |
-
|
| 90 |
|
| 91 |
-
#
|
| 92 |
-
|
| 93 |
|
| 94 |
-
if
|
| 95 |
-
|
| 96 |
-
px, py = coords['x'], coords['y']
|
| 97 |
-
draw = ImageDraw.Draw(base_img)
|
| 98 |
-
# วาดเส้นทิศทางสีแดงจากศูนย์กลาง (112, 112)
|
| 99 |
-
draw.line((112, 112, px, py), fill="#FF0000", width=2)
|
| 100 |
-
# วาดจุดแดง
|
| 101 |
-
r = 4
|
| 102 |
-
draw.ellipse((px-r, py-r, px+r, py+r), fill="#FF0000", outline="white")
|
| 103 |
kx, ky = px - 112, py - 112
|
| 104 |
-
#
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
| 106 |
else:
|
| 107 |
kx, ky = 0, 0
|
| 108 |
-
st.image(
|
| 109 |
|
| 110 |
-
with
|
| 111 |
-
st.markdown(f"**Sinusoidal Wave (kx:{kx}, ky:{ky})
|
| 112 |
Y, X = np.mgrid[-112:112, -112:112]
|
|
|
|
| 113 |
wave = np.cos(2 * np.pi * (kx * X / 224 + ky * Y / 224))
|
| 114 |
-
|
| 115 |
-
st.image(Image.fromarray(
|
| 116 |
|
| 117 |
-
st.write("""k-space 1 จุด = ข้อมูลของภาพทั้งภาพ และ ภาพ 1 พิกเซล = ผลรวมของ k-space ทุกจุด
|
| 118 |
-
1 จุด
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
-
|
| 123 |
-
- **จุดมืดหรือจาง:** แปลว่าภาพ MRI ภาพนี้ แทบจะไม่มีลวดลายชนิดนี้ประกอบอยู่เลย""")
|
| 124 |
|
| 125 |
st.header("Inverse Fourier Transform")
|
| 126 |
st.write("""เมื่อเก็บข้อมูลจนเต็มพื้นที่ k-space เราจะใช้กระบวนการทางคณิตศาสตร์ 2D Inverse Fourier Transform (2D-iFT) ในการเปลี่ยนข้อมูลความถี่กลับไปเป็นข้อมูลในเชิงพื้นที่ (Spatial Domain) ภาพ MRI เกิดจากการนำ "คลื่นความถี่ (Sinusoidal spatial waves)" จากทุกจุดใน k-space มาซ้อนทับกัน คลื่นที่มีเฟสตรงกันจะรวมตัวกันแบบเสริมฤทธิ์ (Constructive interference) สร้างเป็นพิกัดที่สว่าง และคลื่นที่มีเฟสตรงข้ามจะหักล้างกัน (Destructive interference) กลายเป็นพื้นที่สีดำ โดยต้องอาศัยข้อมูลจากหลายจุดมาซ้อนทับกัน และเกิดการแทรกสอดตามคุณสมบัติของคลื่น บริเวณไหนที่เป็นเนื้อเยื่อจริง คลื่นจะเสริมกันทำให้เกิด จุดสว่าง บริเวณไหนที่เป็นช่องว่าง คลื่นจะหักล้างกันทำให้เกิด จุดมืด""")
|
| 127 |
|
| 128 |
st.header("ความถี่เชิงพื้นที่ (Spatial Frequency) คือ")
|
| 129 |
st.write("""ในทางสัญญาณภาพ (Spatial Frequency) ความถี่ไม่ได้หมายถึงความเร็วของเวลา แต่หมายถึง "อัตราการเปลี่ยนแปลงความเข้มของแสงในพื้นที่หนึ่งๆ"
|
| 130 |
-
|
| 131 |
ตัวอย่างในภาพ MRI: บริเวณเนื้อเยื่อก้อนใหญ่ๆ เช่น เนื้อตับ หรือเนื้อสมอง ที่มีสีเทาโทนเดียวกันกินพื้นที่กว้าง
|
| 132 |
ตำแหน่งใน k-space: ข้อมูลเหล่านี้จะรวมตัวกันอยู่บริเวณ "ตรงกลาง"
|
| 133 |
หน้าที่หลัก: สร้าง "รูปร่างรวมๆ และคอนทราสต์ (Contrast)" ให้เรารู้ว่านี่คือก้อนอวัยวะอะไร
|
| 134 |
|
| 135 |
-
|
| 136 |
ตัวอย่างในภาพ MRI: ขอบของอวัยวะ (Edges), รอยต่อระหว่างกระดูกกับไขสันหลัง, หรือรายละเอียดเส้นเลือดเส้นเล็กๆ
|
| 137 |
ตำแหน่งใน k-space: ข้อมูลเหล่านี้จะกระจายตัวอยู่บริเวณ "ขอบนอก"
|
| 138 |
หน้าที่หลัก: สร้าง "ความคมชัด (Resolution) และรายละเอียดเล็กๆ" ทำให้ภาพไม่เบลอ""")
|
| 139 |
|
| 140 |
-
# ------
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
st.write("""เมื่อเราจำแนกข้อมูลใน k-space ออกเป็นความถี่ต่ำ (ตรงกลาง) และความถี่สูง (ขอบนอก) ได้แล้ว เราสามารถเลือก "หยิบ" หรือ "ทิ้ง" ข้อมูลบางส่วนเพื่อดูผลลัพธ์ได้ เรียกว่าการใช้ตัวกรอง (Filter)""")
|
| 144 |
|
| 145 |
-
f_type = st.
|
| 146 |
|
| 147 |
-
|
| 148 |
-
|
| 149 |
|
| 150 |
if f_type == "Low-pass Filter":
|
| 151 |
-
st.
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
|
|
|
| 155 |
else:
|
| 156 |
-
st.
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
|
|
|
| 160 |
|
| 161 |
-
# คำนวณภาพผลลัพธ์
|
| 162 |
filtered_k = k_data * mask
|
| 163 |
-
mri_result = np.abs(np.fft.ifft2(np.fft.ifftshift(filtered_k)))
|
| 164 |
-
|
| 165 |
col_f1, col_f2 = st.columns(2)
|
|
|
|
| 166 |
with col_f1:
|
| 167 |
-
st.image(
|
| 168 |
with col_f2:
|
| 169 |
-
|
| 170 |
-
res_pil = (mri_result - mri_result.min()) / (mri_result.max() - mri_result.min() + 1e-8) * 255
|
| 171 |
-
st.image(Image.fromarray(res_pil.astype(np.uint8)), caption="ภาพ MRI ผลลัพธ์", width=300)
|
| 172 |
|
| 173 |
-
st.success(
|
|
|
|
| 1 |
import streamlit as st
|
| 2 |
import numpy as np
|
| 3 |
import scipy.io
|
| 4 |
+
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
| 5 |
from streamlit_image_coordinates import streamlit_image_coordinates
|
| 6 |
|
| 7 |
+
# 1. ตั้งค่าเลย์เอาต์หน้าเว็บและการออกแบบ (CSS)
|
| 8 |
+
st.set_page_config(layout="centered", page_title="MRI K-Space Learning Tool")
|
| 9 |
|
|
|
|
| 10 |
st.markdown("""
|
| 11 |
<style>
|
| 12 |
+
/* ปรับขนาดฟอนต์เนื้อหา */
|
| 13 |
+
.main .block-container { max-width: 900px; }
|
| 14 |
+
p, li { font-size: 19px !important; line-height: 1.7 !important; color: #333; }
|
| 15 |
+
|
| 16 |
+
/* ปรับหัวข้อ */
|
| 17 |
+
h1 { color: #0047AB; font-size: 45px !important; font-weight: 800 !important; border-bottom: 3px solid #0047AB; padding-bottom: 10px; margin-bottom: 30px !important; }
|
| 18 |
+
h2 { color: #0056b3; font-size: 32px !important; font-weight: 700 !important; margin-top: 40px !important; border-left: 8px solid #0056b3; padding-left: 15px; }
|
| 19 |
+
h3 { color: #007bff; font-size: 26px !important; font-weight: 600 !important; margin-top: 30px !important; }
|
| 20 |
+
|
| 21 |
+
/* ปรับปุ่มและ Slider */
|
| 22 |
+
.stButton>button { background-color: #f8f9fa; border: 1px solid #ddd; border-radius: 8px; font-weight: bold; }
|
| 23 |
+
.stSlider { padding-top: 20px; }
|
| 24 |
</style>
|
| 25 |
""", unsafe_allow_html=True)
|
| 26 |
|
| 27 |
+
# 2. ฟังก์ชันสร้างรูปองค์ประกอบ K-Space ขึ้นมาใหม่ (แทนรูปเดิม)
|
| 28 |
+
def create_kspace_diagram():
|
| 29 |
+
img = Image.new('RGB', (600, 400), color=(255, 255, 255))
|
| 30 |
+
draw = ImageDraw.Draw(img)
|
| 31 |
+
center = (300, 200)
|
| 32 |
+
|
| 33 |
+
# วาด Grid พื้นหลัง
|
| 34 |
+
for i in range(0, 601, 40): draw.line([(i, 0), (i, 400)], fill=(240, 240, 240))
|
| 35 |
+
for i in range(0, 401, 40): draw.line([(0, i), (600, i)], fill=(240, 240, 240))
|
| 36 |
+
|
| 37 |
+
# วาดแกน
|
| 38 |
+
draw.line([(50, 200), (550, 200)], fill=(0, 0, 0), width=3) # แกน kx
|
| 39 |
+
draw.line([(300, 50), (300, 350)], fill=(0, 0, 0), width=3) # แกน ky
|
| 40 |
+
|
| 41 |
+
# วาดพื้นที่ Low Frequency (กลาง) และ High Frequency (ขอบ)
|
| 42 |
+
draw.ellipse([240, 140, 360, 260], outline=(0, 100, 255), width=2) # วงใน
|
| 43 |
+
draw.text((310, 160), "Low Frequency\n(Contrast)", fill=(0, 80, 200))
|
| 44 |
+
|
| 45 |
+
draw.text((450, 210), "kx (Frequency)", fill=(0, 0, 0))
|
| 46 |
+
draw.text((310, 60), "ky (Phase)", fill=(0, 0, 0))
|
| 47 |
+
draw.text((420, 100), "High Frequency\n(Edges/Details)", fill=(150, 0, 0))
|
| 48 |
+
|
| 49 |
+
return img
|
| 50 |
+
|
| 51 |
+
# 3. จัดการข้อมูล K-Space
|
| 52 |
@st.cache_data
|
| 53 |
+
def load_data():
|
| 54 |
try:
|
| 55 |
mat = scipy.io.loadmat('kspace.mat')
|
| 56 |
+
key = 'kspace' if 'kspace' in mat else ([k for k in mat.keys() if not k.startswith('__')][0])
|
|
|
|
| 57 |
data = mat[key]
|
|
|
|
| 58 |
if data.shape != (224, 224):
|
| 59 |
+
h, w = data.shape[:2]
|
|
|
|
|
|
|
| 60 |
data = data[(h-224)//2:(h+224)//2, (w-224)//2:(w+224)//2]
|
| 61 |
return data
|
| 62 |
except:
|
| 63 |
+
# สร้างข้อมูลสมองจำลอง (Synthetic MRI)
|
| 64 |
y, x = np.ogrid[-112:112, -112:112]
|
| 65 |
img = np.zeros((224, 224))
|
| 66 |
+
img[(x/80)**2 + (y/100)**2 <= 1] = 0.5
|
| 67 |
+
img[(x/50)**2 + (y/70)**2 <= 1] = 0.8
|
| 68 |
+
img[x**2 + (y-30)**2 <= 15**2] = 1.0
|
| 69 |
return np.fft.fftshift(np.fft.fft2(img))
|
| 70 |
|
| 71 |
+
k_data = load_data()
|
| 72 |
|
| 73 |
+
def process_image(arr, is_kspace=True):
|
| 74 |
+
if is_kspace:
|
| 75 |
+
res = np.log(1 + np.abs(arr))
|
|
|
|
| 76 |
else:
|
| 77 |
+
# ป้องกันภาพซ้อน 4: ใช้ ifftshift ก่อน ifft2
|
| 78 |
+
res = np.abs(np.fft.ifft2(np.fft.ifftshift(arr)))
|
| 79 |
+
norm = (res - res.min()) / (res.max() - res.min() + 1e-8) * 255
|
| 80 |
return Image.fromarray(norm.astype(np.uint8)).convert("RGB")
|
| 81 |
|
| 82 |
# ==========================================
|
| 83 |
+
# เริ่มต้นหน้าเว็บ (Main Content)
|
| 84 |
# ==========================================
|
| 85 |
|
| 86 |
st.title("K-space to MRI image")
|
|
|
|
| 89 |
st.write("""เมื่อเรานำผู้ป่วยเข้าเครื่อง MRI และส่งคลื่น RF เข้าไปกระตุ้น เกิดเป็นสัญญาณ MR Signal ที่ได้มานั้นจะยังไม่ได้ออกมาเป็นภาพอวัยวะ แต่จะถูกนำไปเก็บรวบรวมไว้ในพื้นที่ที่เรียกว่า "K-space" ซึ่งเป็นพื้นที่ที่จะเก็บข้อมูลดิบแบบสองมิติ ก่อนจะนำไปผ่านกระบวนการทางคฺณิตศาสตร์ที่เรียกว่า Fourier Transform ให้ได้มาซึ่งภาพ MRI ซึ่งข้อมูลใน K-Sapce ถูกเก็บในรูปแบบ ความถี่เชิงพื้นที่ (Spatial Frequency) พิกัดในตารางของ K-space ถูกสร้างขึ้นจากการทำงานของสนามแม่เหล็กเกรเดียนท์ 2 แกน ได้แก่ Gx (ทำหน้าที่เข้ารหัสในแนวความถี่ Frequency encoding) และ Gy (ทำหน้าที่เข้ารหัสในแนวเฟส Phase encoding) ซึ่งเกรเดียนท์ทั้งสองตัวนี้กำหนดว่า สัญญาณจากโปรตอนที่มีความถี่และเฟสจำเพาะเจาะจงที่ต่างกันนั้น จะต้องถูกนำไปจัดเก็บไว้ตรงจุดไหนในพิกัดของ K-space""")
|
| 90 |
|
| 91 |
st.header("องค์ประกอบของ K-Space")
|
| 92 |
+
st.image(create_kspace_diagram(), caption="แผนผังแสดงองค์ประกอบของ K-Space (แกนความถี่และเฟส)", width=600)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
st.write("""ข้อมูลใน k-space มักจะถูกนำมาแสดงผลในรูปแบบตารางสี่เหลี่ยม (Grid) โดยมีแกนหลักคือ kx (แนวนอน - Frequency) และ ky (แนวตั้ง - Phase) แต่จุดสำคัญคือ แกน kx และ ky เหล่านี้ ไม่ได้บอกตำแหน่งพิกัด ในภาพ แต่มันคือแกนที่บอกถึงลักษณะของ "ความถี่เชิงพื้นที่ (Spatial Frequencies)" ซึ่งเป็นคลื่นความถี่ Sinusoidal wave ด้วยเหตุนี้ จุดแต่ละจุดบนพิกัด (kx, ky) ใน k-space จึง ไม่ได้จับคู่แบบ 1 ต่อ 1 กับพิกเซล (x, y) บนภาพ MRI (ไม่ได้แปลว่าจุดมุมซ้ายบนใน k-space จะสร้างภาพมุมซ้ายบนของภาพอวัยวะ)""")
|
| 95 |
|
| 96 |
+
# --- Interactive 1 ---
|
|
|
|
|
|
|
| 97 |
st.subheader("1 จุดบน k-space")
|
| 98 |
|
| 99 |
+
# ปุ่ม Reset
|
| 100 |
+
if st.button("🔄 Reset พิกัด"):
|
| 101 |
+
st.session_state.kx = 112
|
| 102 |
+
st.session_state.ky = 112
|
| 103 |
st.rerun()
|
| 104 |
|
| 105 |
+
col_pick, col_wave = st.columns([1, 1])
|
| 106 |
|
| 107 |
+
with col_pick:
|
| 108 |
+
st.markdown("**คลิกเลือกจุดบน K-Space:**")
|
| 109 |
+
k_img = process_image(k_data)
|
| 110 |
|
| 111 |
+
# ดักจับการคลิก
|
| 112 |
+
pos = streamlit_image_coordinates(k_img, key="coords")
|
| 113 |
|
| 114 |
+
if pos:
|
| 115 |
+
px, py = pos['x'], pos['y']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
kx, ky = px - 112, py - 112
|
| 117 |
+
# วาดเส้นทิศทางและจุดแดง
|
| 118 |
+
draw = ImageDraw.Draw(k_img)
|
| 119 |
+
draw.line((112, 112, px, py), fill="red", width=2)
|
| 120 |
+
draw.ellipse((px-4, py-4, px+4, py+4), fill="red", outline="white")
|
| 121 |
+
st.image(k_img, width=300)
|
| 122 |
else:
|
| 123 |
kx, ky = 0, 0
|
| 124 |
+
st.image(k_img, width=300)
|
| 125 |
|
| 126 |
+
with col_wave:
|
| 127 |
+
st.markdown(f"**Sinusoidal Wave (kx: {kx}, ky: {ky})**")
|
| 128 |
Y, X = np.mgrid[-112:112, -112:112]
|
| 129 |
+
# คำนวณคลื่น
|
| 130 |
wave = np.cos(2 * np.pi * (kx * X / 224 + ky * Y / 224))
|
| 131 |
+
wave_norm = ((wave + 1) * 127.5).astype(np.uint8)
|
| 132 |
+
st.image(Image.fromarray(wave_norm), width=300)
|
| 133 |
|
| 134 |
+
st.write("""**k-space 1 จุด = ข้อมูลของภาพทั้งภาพ และ ภาพ 1 พิกเซล = ผลรวมของ k-space ทุกจุด** 1 จุดใน k-space = แผ่นลวดลายคลื่น 1 แผ่น (2D Sinusoidal Wave)
|
| 135 |
+
1. **ตำแหน่งของจุด (พิกัด kx, ky) บอก "ความถี่" และ "ทิศทาง"** - ระยะห่างจากศูนย์กลาง (ความถี่): ยิ่งจุดนี้อยู่ไกลจากจุดศูนย์กลาง k-space มากเท่าไหร่ แผ่นลวดลายคลื่นก็จะยิ่ง "ถี่" หรือมีเส้นที่แคบมากขึ้นเท��านั้น (High frequency)
|
| 136 |
+
- มุมของจุด (ทิศทาง): ตำแหน่งของจุดเมื่อเทียบกับจุดศูนย์กลาง จะเป็นตัวบอกว่าแผ่นลวดลายคลื่นนี้จะ "เอียง" ไปในทิศทางไหน (ตั้ง นอน หรือเฉียงกี่องศา)
|
| 137 |
+
2. **ความสว่างของจุด (Amplitude / Magnitude) บอก "น้ำหนัก"** ความสว่างของจุดใน k-space ไม่ได้แปลว่าภาพ MRI ตรงนั้นจะสว่าง แต่มันคือการบอก "ปริมาณ (Weight)"
|
| 138 |
+
- จุดสว่างมาก: แปลว่าภาพ MRI ภาพนี้ มีแผ่นลวดลายชนิดนี้เป็นส่วนประกอบอยู่ เยอะมาก (มีความสำคัญต่อภาพสูง)
|
| 139 |
+
- จุดมืดหรือจาง: แปลว่าภาพ MRI ภาพนี้ แทบจะไม่มีลวดลายชนิดนี้ประกอบอยู่เลย""")
|
|
|
|
| 140 |
|
| 141 |
st.header("Inverse Fourier Transform")
|
| 142 |
st.write("""เมื่อเก็บข้อมูลจนเต็มพื้นที่ k-space เราจะใช้กระบวนการทางคณิตศาสตร์ 2D Inverse Fourier Transform (2D-iFT) ในการเปลี่ยนข้อมูลความถี่กลับไปเป็นข้อมูลในเชิงพื้นที่ (Spatial Domain) ภาพ MRI เกิดจากการนำ "คลื่นความถี่ (Sinusoidal spatial waves)" จากทุกจุดใน k-space มาซ้อนทับกัน คลื่นที่มีเฟสตรงกันจะรวมตัวกันแบบเสริมฤทธิ์ (Constructive interference) สร้างเป็นพิกัดที่สว่าง และคลื่นที่มีเฟสตรงข้ามจะหักล้างกัน (Destructive interference) กลายเป็นพื้นที่สีดำ โดยต้องอาศัยข้อมูลจากหลายจุดมาซ้อนทับกัน และเกิดการแทรกสอดตามคุณสมบัติของคลื่น บริเวณไหนที่เป็นเนื้อเยื่อจริง คลื่นจะเสริมกันทำให้เกิด จุดสว่าง บริเวณไหนที่เป็นช่องว่าง คลื่นจะหักล้างกันทำให้เกิด จุดมืด""")
|
| 143 |
|
| 144 |
st.header("ความถี่เชิงพื้นที่ (Spatial Frequency) คือ")
|
| 145 |
st.write("""ในทางสัญญาณภาพ (Spatial Frequency) ความถี่ไม่ได้หมายถึงความเร็วของเวลา แต่หมายถึง "อัตราการเปลี่ยนแปลงความเข้มของแสงในพื้นที่หนึ่งๆ"
|
| 146 |
+
1. **ความถี่เชิงพื้นที่ต่ำ (Low Spatial Frequency)** คืออะไร: พื้นที่ที่สีหรือความสว่าง "ค่อยๆ เปลี่ยน" หรือ "เหมือนเดิมเป็นบริเวณกว้าง" (เหมือนคลื่นลูกใหญ่ๆ ที่ขยับช้าๆ)
|
| 147 |
ตัวอย่างในภาพ MRI: บริเวณเนื้อเยื่อก้อนใหญ่ๆ เช่น เนื้อตับ หรือเนื้อสมอง ที่มีสีเทาโทนเดียวกันกินพื้นที่กว้าง
|
| 148 |
ตำแหน่งใน k-space: ข้อมูลเหล่านี้จะรวมตัวกันอยู่บริเวณ "ตรงกลาง"
|
| 149 |
หน้าที่หลัก: สร้าง "รูปร่างรวมๆ และคอนทราสต์ (Contrast)" ให้เรารู้ว่านี่คือก้อนอวัยวะอะไร
|
| 150 |
|
| 151 |
+
2. **ความถี่เชิงพื้นที่สูง (High Spatial Frequency)** คืออะไร: พื้นที่ที่ความสว่างเปลี่ยนแบบฉับพลันและรวดเร็วภายในระยะทางสั้นๆ เช่น จากขาวตัดเป็นดำสนิททันที (เหมือนลายทางแคบๆ ที่สลับสีถี่ๆ)
|
| 152 |
ตัวอย่างในภาพ MRI: ขอบของอวัยวะ (Edges), รอยต่อระหว่างกระดูกกับไขสันหลัง, หรือรายละเอียดเส้นเลือดเส้นเล็กๆ
|
| 153 |
ตำแหน่งใน k-space: ข้อมูลเหล่านี้จะกระจายตัวอยู่บริเวณ "ขอบนอก"
|
| 154 |
หน้าที่หลัก: สร้าง "ความคมชัด (Resolution) และรายละเอียดเล็กๆ" ทำให้ภาพไม่เบลอ""")
|
| 155 |
|
| 156 |
+
# --- Interactive 2 ---
|
| 157 |
+
st.divider()
|
| 158 |
+
st.write("""ซึ่งจะให้ผู้เรียนได้ลองปรับหน้าตาของภาพ K-Space แล้วเปรียบเทียบความแตกต่างด้วยการปรับ High-pass filter และ Low-pass filter เพื่อดูลักษณะและความสำคัญของข้อมูลบริเวณกลางและขอบนอกของ K-space เมื่อเราจำแนกข้อมูลใน k-space ออกเป็นความถี่ต่ำ (ตรงกลาง) และความถี่สูง (ขอบนอก) ได้แล้ว เราสามารถเลือก "หยิบ" หรือ "ทิ้ง" ข้อมูลบางส่วนเพื่อดูผลลัพธ์ได้ เรียกว่าการใช้ตัวกรอง (Filter)""")
|
|
|
|
| 159 |
|
| 160 |
+
f_type = st.radio("เลือกชนิดตัวกรอง:", ["Low-pass Filter", "High-pass Filter"], horizontal=True)
|
| 161 |
|
| 162 |
+
Y_dist, X_dist = np.ogrid[-112:112, -112:112]
|
| 163 |
+
d = np.sqrt(X_dist**2 + Y_dist**2)
|
| 164 |
|
| 165 |
if f_type == "Low-pass Filter":
|
| 166 |
+
st.markdown("**Low-pass Filter (ตัวกรองปล่อยความถี่ต่ำผ่าน):**")
|
| 167 |
+
st.write('ทำงานอย่างไร: "อนุญาตให้เฉพาะข้อมูลตรงกลาง (ความถี่ต่ำ) ผ่านไปสร้างภาพได้ ส่วนข้อมูลขอบนอก (ความถี่สูง) ให้ทิ้งไป"')
|
| 168 |
+
rad = st.slider("รัศมีวงกลม (Radius):", 0, 112, 112)
|
| 169 |
+
mask = d <= rad
|
| 170 |
+
msg = 'เราจะได้ภาพที่มี "คอนทราสต์" ดูออกว่าเป็นอวัยวะอะไร แต่ภาพจะ "เบลอ" (Blurry) เพราะข้อมูลเส้นขอบถูกทิ้งไปแล้ว'
|
| 171 |
else:
|
| 172 |
+
st.markdown("**High-pass Filter (ตัวกรองปล่อยความถี่สูงผ่าน):**")
|
| 173 |
+
st.write("ทำงานอย่างไร: อนุญาตให้เฉพาะข้อมูลขอบนอก (ความถี่สูง) ผ่านไปได้ ส่วนข้อมูลตรงกลางทิ้ง")
|
| 174 |
+
rad = st.slider("รัศมีวงกลม (Radius):", 0, 112, 0)
|
| 175 |
+
mask = d >= rad
|
| 176 |
+
msg = 'ภาพจะสูญเสียคอนทราสต์ไปจนเกือบมืดสนิท แต่จะปรากฏ "เส้นขอบร่าง" (Outline) ของอวัยวะขึ้นมาอย่างคมชัด'
|
| 177 |
|
| 178 |
+
# คำนวณภาพผลลัพธ์
|
| 179 |
filtered_k = k_data * mask
|
|
|
|
|
|
|
| 180 |
col_f1, col_f2 = st.columns(2)
|
| 181 |
+
|
| 182 |
with col_f1:
|
| 183 |
+
st.image(process_image(filtered_k), caption="K-Space Filtered", width=300)
|
| 184 |
with col_f2:
|
| 185 |
+
st.image(process_image(filtered_k, is_kspace=False), caption="MRI Result (Single Slice)", width=300)
|
|
|
|
|
|
|
| 186 |
|
| 187 |
+
st.success(f"**ผลลัพธ์ที่ได้:** {msg}")
|