Update app.py
Browse files
app.py
CHANGED
|
@@ -1,187 +1,269 @@
|
|
| 1 |
import streamlit as st
|
| 2 |
import numpy as np
|
| 3 |
import scipy.io
|
| 4 |
-
|
| 5 |
-
|
|
|
|
| 6 |
|
| 7 |
-
#
|
| 8 |
-
st.set_page_config(layout="
|
| 9 |
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
-
|
| 22 |
-
.
|
| 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 |
-
|
| 34 |
-
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
-
# วาด
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
| 40 |
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
draw.text((420, 100), "High Frequency\n(Edges/Details)", fill=(150, 0, 0))
|
| 48 |
|
| 49 |
-
|
|
|
|
|
|
|
| 50 |
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 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-
|
| 87 |
|
| 88 |
-
st.
|
| 89 |
-
|
|
|
|
|
|
|
| 90 |
|
| 91 |
st.header("องค์ประกอบของ K-Space")
|
| 92 |
-
st.image(create_kspace_diagram(), caption="แผนผังแสดงองค์ประกอบของ K-Space (แกนความถี่และเฟส)", width=600)
|
| 93 |
|
| 94 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
|
| 96 |
-
|
| 97 |
-
st.subheader("1 จุดบน k-space")
|
| 98 |
|
| 99 |
-
|
| 100 |
-
if st.button("🔄 Reset พิกัด"):
|
| 101 |
-
st.session_state.kx = 112
|
| 102 |
-
st.session_state.ky = 112
|
| 103 |
-
st.rerun()
|
| 104 |
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
|
| 144 |
st.header("ความถี่เชิงพื้นที่ (Spatial Frequency) คือ")
|
| 145 |
-
st.
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
with col_f2:
|
| 185 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
|
| 187 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import streamlit as st
|
| 2 |
import numpy as np
|
| 3 |
import scipy.io
|
| 4 |
+
import matplotlib.pyplot as plt
|
| 5 |
+
import zipfile
|
| 6 |
+
import os
|
| 7 |
|
| 8 |
+
# --- ตั้งค่าหน้าเว็บ Streamlit ---
|
| 9 |
+
st.set_page_config(layout="wide", page_title="K-Space to MRI")
|
| 10 |
|
| 11 |
+
# --- ฟังก์ชันช่วยเหลือ (Helper Functions) ---
|
| 12 |
+
@st.cache_data
|
| 13 |
+
def load_kspace_data():
|
| 14 |
+
try:
|
| 15 |
+
# ตรวจสอบว่ามีไฟล์ mat หรือ zip ใน directory หรือไม่
|
| 16 |
+
if os.path.exists('kspace.mat'):
|
| 17 |
+
mat_data = scipy.io.loadmat('kspace.mat')
|
| 18 |
+
else:
|
| 19 |
+
with zipfile.ZipFile("kspace.zip", 'r') as zip_ref:
|
| 20 |
+
zip_ref.extractall("temp_kspace")
|
| 21 |
+
mat_data = scipy.io.loadmat("temp_kspace/kspace.mat")
|
| 22 |
+
kspace = mat_data['kspace']
|
| 23 |
+
return kspace
|
| 24 |
+
except Exception as e:
|
| 25 |
+
st.error("ไม่พบไฟล์ข้อมูล kspace.mat หรือ kspace.zip กรุณาอัปโหลดไฟล์เข้าระบบ")
|
| 26 |
+
# สร้างข้อมูลจำลองกรณีไม่เจอไฟล์ เพื่อให้แอปทำงานต่อได้
|
| 27 |
+
x = np.linspace(-5, 5, 224)
|
| 28 |
+
y = np.linspace(-5, 5, 224)
|
| 29 |
+
X, Y = np.meshgrid(x, y)
|
| 30 |
+
dummy_image = np.sin(X**2 + Y**2) * np.exp(-(X**2 + Y**2)/10)
|
| 31 |
+
return np.fft.fftshift(np.fft.fft2(dummy_image))
|
| 32 |
+
|
| 33 |
+
kspace_data = load_kspace_data()
|
| 34 |
+
|
| 35 |
+
def kspace_to_image(kspace_freq):
|
| 36 |
+
# แปลง K-space กลับเป็นภาพ MRI ขนาด 224x224
|
| 37 |
+
img = np.fft.ifft2(np.fft.ifftshift(kspace_freq))
|
| 38 |
+
img_mag = np.abs(img)
|
| 39 |
+
# Normalize ให้ค่าอยู่ระหว่าง 0-1
|
| 40 |
+
img_norm = (img_mag - img_mag.min()) / (img_mag.max() - img_mag.min())
|
| 41 |
+
return img_norm
|
| 42 |
+
|
| 43 |
+
def generate_kspace_axis_image():
|
| 44 |
+
# จำลองรูปแกน K-Space ตามที่แนบมา
|
| 45 |
+
fig, ax = plt.subplots(figsize=(5, 5))
|
| 46 |
+
ax.axhline(0, color='black', linewidth=2)
|
| 47 |
+
ax.axvline(0, color='black', linewidth=2)
|
| 48 |
|
| 49 |
+
ax.text(0.95, 0.05, 'kx\n(Frequency)', transform=ax.transAxes, ha='right', va='bottom', fontsize=12, fontweight='bold', color='red')
|
| 50 |
+
ax.text(0.05, 0.95, 'ky\n(Phase)', transform=ax.transAxes, ha='left', va='top', fontsize=12, fontweight='bold', color='red')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
+
ax.grid(True, linestyle='--', alpha=0.5)
|
| 53 |
+
ax.set_xlim(-1, 1)
|
| 54 |
+
ax.set_ylim(-1, 1)
|
| 55 |
+
ax.set_xticks([])
|
| 56 |
+
ax.set_yticks([])
|
| 57 |
+
ax.set_title("K-Space", fontweight='bold')
|
| 58 |
|
| 59 |
+
# วาดคลื่นจำลองในกราฟให้คล้ายกับรูปที่ร่างไว้
|
| 60 |
+
x = np.linspace(-1, 1, 100)
|
| 61 |
+
y = 0.2 * np.sin(10 * x)
|
| 62 |
+
ax.plot(x, y, color='red', alpha=0.5)
|
| 63 |
+
ax.plot(y, x, color='blue', alpha=0.5)
|
| 64 |
|
| 65 |
+
return fig
|
| 66 |
+
|
| 67 |
+
def generate_2d_wave(kx, ky, size=224):
|
| 68 |
+
x = np.arange(size)
|
| 69 |
+
y = np.arange(size)
|
| 70 |
+
X, Y = np.meshgrid(x, y)
|
| 71 |
|
| 72 |
+
X = X - size // 2
|
| 73 |
+
Y = Y - size // 2
|
|
|
|
| 74 |
|
| 75 |
+
# สร้างคลื่น 2D Sinusoidal
|
| 76 |
+
wave = np.cos(2 * np.pi * (kx * X + ky * Y) / size)
|
| 77 |
+
return wave
|
| 78 |
|
| 79 |
+
def draw_kspace_point(kx, ky, size=224):
|
| 80 |
+
fig, ax = plt.subplots(figsize=(5, 5))
|
| 81 |
+
ax.imshow(np.zeros((size, size)), cmap='gray', extent=[-size//2, size//2, -size//2, size//2])
|
| 82 |
+
|
| 83 |
+
ax.plot(kx, ky, 'ro', markersize=8)
|
| 84 |
+
# วาดเส้นแสดงทิศทาง
|
| 85 |
+
ax.annotate('', xy=(kx, ky), xytext=(0, 0),
|
| 86 |
+
arrowprops=dict(arrowstyle='->', color='yellow', lw=2))
|
| 87 |
+
|
| 88 |
+
ax.axhline(0, color='white', linewidth=0.5, linestyle='--')
|
| 89 |
+
ax.axvline(0, color='white', linewidth=0.5, linestyle='--')
|
| 90 |
+
|
| 91 |
+
ax.set_xlim(-size//2, size//2)
|
| 92 |
+
ax.set_ylim(-size//2, size//2)
|
| 93 |
+
ax.set_title(f"พิกัด K-Space (kx={kx}, ky={ky})")
|
| 94 |
+
ax.axis('off')
|
| 95 |
+
return fig
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
+
# --- ส่วนเนื้อหาและ UI ของหน้าเว็บ ---
|
|
|
|
|
|
|
| 98 |
|
| 99 |
+
st.title("K-Space to MRI image")
|
| 100 |
|
| 101 |
+
st.markdown("""
|
| 102 |
+
**K-space คือ**
|
| 103 |
+
เมื่อเรานำผู้ป่วยเข้าเครื่อง MRI และส่งคลื่น RF เข้าไปกระตุ้น เกิดเป็นสัญญาณ MR Signal ที่ได้มาน��้นจะยังไม่ได้ออกมาเป็นภาพอวัยวะ แต่จะถูกนำไปเก็บรวบรวมไว้ในพื้นที่ที่เรียกว่า "K-space" ซึ่งเป็นพื้นที่ที่จะเก็บข้อมูลดิบแบบสองมิติ ก่อนจะนำไปผ่านกระบวนการทางคณิตศาสตร์ที่เรียกว่า Fourier Transform ให้ได้มาซึ่งภาพ MRI ซึ่งข้อมูลใน K-Sapce ถูกเก็บในรูปแบบ ความถี่เชิงพื้นที่ (Spatial Frequency) พิกัดในตารางของ K-space ถูกสร้างขึ้นจากการทำงานของสนามแม่เหล็กเกรเดียนท์ 2 แกน ได้แก่ Gx (ทำหน้าที่เข้ารหัสในแนวความถี่ Frequency encoding) และ Gy (ทำหน้าที่เข้ารหัสในแนวเฟส Phase encoding) ซึ่งเกรเดียนท์ทั้งสองตัวนี้กำหนดว่า สัญญาณจากโปรตอนที่มีความถี่และเฟสจำเพาะเจาะจงที่ต่างกันนั้น จะต้องถูกนำไปจัดเก็บไว้ตรงจุดไหนในพิกัดของ K-space
|
| 104 |
+
""")
|
| 105 |
|
| 106 |
st.header("องค์ประกอบของ K-Space")
|
|
|
|
| 107 |
|
| 108 |
+
col1, col2 = st.columns([1, 1.5])
|
| 109 |
+
with col1:
|
| 110 |
+
fig_axis = generate_kspace_axis_image()
|
| 111 |
+
st.pyplot(fig_axis)
|
| 112 |
+
with col2:
|
| 113 |
+
st.markdown("""
|
| 114 |
+
ข้อมูลใน k-space มักจะถูกนำมาแสดงผลในรูปแบบตารางสี่เหลี่ยม (Grid) โดยมีแกนหลักคือ kx (แนวนอน - Frequency) และ ky (แนวตั้ง - Phase) แต่จุดสำคัญคือ แกน kx และ ky เหล่านี้ ไม่ได้บอกตำแหน่งพิกัด ในภาพ แต่มันคือแกนที่บอกถึง ลักษณะของ "ความถี่เชิงพื้นที่ (Spatial Frequencies)" ซึ่งเป็นคลื่นความถี่ Sinusoidal wave ด้วยเหตุนี้ จุดแต่ละจุดบนพิกัด (kx, ky) ใน k-space จึง ไม่ได้จับคู่แบบ 1 ต่อ 1 กับพิกเซล (x, y) บนภาพ MRI (ไม่ได้แปลว่าจุดมุมซ้ายบนใน k-space จะสร้างภาพมุมซ้ายบนของ ภาพอวัยวะ)
|
| 115 |
+
""")
|
| 116 |
|
| 117 |
+
st.divider()
|
|
|
|
| 118 |
|
| 119 |
+
st.header("1 จุดบน k-space")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
+
# --- Interactive Part 1: การเลือกจุดบน K-Space ---
|
| 122 |
+
st.markdown("**(Interactive: ลองเลื่อน Slider เพื่อเลือกตำแหน่งจุด แล้วดูทิศทางและลักษณะของแผ่นลวดลายคลื่น)**")
|
| 123 |
+
|
| 124 |
+
if 'kx_val' not in st.session_state:
|
| 125 |
+
st.session_state.kx_val = 15
|
| 126 |
+
if 'ky_val' not in st.session_state:
|
| 127 |
+
st.session_state.ky_val = 20
|
| 128 |
+
|
| 129 |
+
def reset_point():
|
| 130 |
+
st.session_state.kx_val = 0
|
| 131 |
+
st.session_state.ky_val = 0
|
| 132 |
+
|
| 133 |
+
col_w1, col_w2, col_w3 = st.columns([1, 1, 1])
|
| 134 |
+
with col_w1:
|
| 135 |
+
st.slider("ตำแหน่งแกน kx (แนวนอน)", -112, 111, key='kx_val')
|
| 136 |
+
st.slider("ตำแหน่งแกน ky (แนวตั้ง)", -112, 111, key='ky_val')
|
| 137 |
+
st.button("Reset จุด", on_click=reset_point)
|
| 138 |
+
|
| 139 |
+
with col_w2:
|
| 140 |
+
fig_pt = draw_kspace_point(st.session_state.kx_val, st.session_state.ky_val)
|
| 141 |
+
st.pyplot(fig_pt)
|
| 142 |
+
|
| 143 |
+
with col_w3:
|
| 144 |
+
wave_img = generate_2d_wave(st.session_state.kx_val, st.session_state.ky_val)
|
| 145 |
+
fig_wave, ax_wave = plt.subplots(figsize=(5, 5))
|
| 146 |
+
ax_wave.imshow(wave_img, cmap='gray')
|
| 147 |
+
ax_wave.set_title("แผ่นลวดลายคลื่น (2D Sinusoidal Wave)")
|
| 148 |
+
ax_wave.axis('off')
|
| 149 |
+
st.pyplot(fig_wave)
|
| 150 |
|
| 151 |
+
|
| 152 |
+
st.markdown("""
|
| 153 |
+
**k-space 1 จุด = ข้อมูลของภาพทั้งภาพ และ ภาพ 1 พิกเซล = ผลรวมของ k-space ทุกจุด**
|
| 154 |
+
**1 จุดใน k-space = แผ่นลวดลายคลื่น 1 แผ่น (2D Sinusoidal Wave)**
|
| 155 |
+
|
| 156 |
+
1. **ตำแหน่งของจุด (พิกัด kx, ky) บอก "ความถี่" และ "ทิศทาง"**
|
| 157 |
+
- **ระยะห่างจากศูนย์กลาง (ความถี่):** ยิ่งจุดนี้อยู่ไกลจากจุดศูนย์กลาง k-space มากเท่าไหร่ แผ่นลวดลายคลื่นก็จะยิ่ง "ถี่" หรือมีเส้นที่แคบมากขึ้นเท่านั้น (High frequency)
|
| 158 |
+
- **มุมของจุด (ทิศทาง):** ตำแหน่งของจุดเมื่อเทียบกับจุดศูนย์กลาง จะเป็นตัวบอกว่าแผ่นลวดลา��คลื่นนี้จะ "เอียง" ไปในทิศทางไหน (ตั้ง นอน หรือเฉียงกี่องศา)
|
| 159 |
+
2. **ความสว่างของจุด (Amplitude / Magnitude) บอก "น้ำหนัก"**
|
| 160 |
+
- ความสว่างของจุดใน k-space ไม่ได้แปลว่าภาพ MRI ตรงนั้นจะสว่าง แต่มันคือการบอก "ปริมาณ (Weight)"
|
| 161 |
+
- **จุดสว่างมาก:** แปลว่าภาพ MRI ภาพนี้ มีแผ่นลวดลายชนิดนี้เป็นส่วนประกอบอยู่ เยอะมาก (มีความสำคัญต่อภาพสูง)
|
| 162 |
+
- **จุดมืดหรือจาง:** แปลว่าภาพ MRI ภาพนี้ แทบจะไม่มีลวดลายชนิดนี้ประกอบอยู่เลย
|
| 163 |
+
""")
|
| 164 |
+
|
| 165 |
+
st.divider()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
|
| 167 |
st.header("Inverse Fourier Transform")
|
| 168 |
+
st.markdown("""
|
| 169 |
+
เมื่อเก็บข้อมูลจนเต็มพื้นที่ k-space เราจะใช้กระบวนการทางคณิตศาสตร์ 2D Inverse Fourier Transform (2D-iFT) ในการเปลี่ยนข้อมูลความถี่กลับไปเป็นข้อมูลในเชิงพื้นที่ (Spatial Domain) ภาพ MRI เกิดจากการนำ "คลื่นความถี่ (Sinusoidal spatial waves)" จากทุกจุดใน k-space มาซ้อนทับกัน คลื่นที่มีเฟสตรงกันจะรวมตัวกันแบบเสริมฤทธิ์ (Constructive interference) สร้างเป็นพิกัดที่สว่าง และคลื่นที่มีเฟสตรงข้ามจะหักล้างกัน (Destructive interference) กลายเป็นพื้นที่สีดำ
|
| 170 |
+
โดยต้องอาศัยข้อมูลจากหลายจุดมาซ้อนทับกัน และเกิดการแทรกสอดตามคุณสมบัติของคลื่น
|
| 171 |
+
o บริเวณไหนที่เป็นเนื้อเยื่อจริง คลื่นจะเสริมกันทำให้เกิด จุดสว่าง
|
| 172 |
+
o บริเวณไหนที่เป็นช่องว่าง คลื่นจะหักล้างกันทำให้เกิด จุดมืด
|
| 173 |
+
""")
|
| 174 |
|
| 175 |
st.header("ความถี่เชิงพื้นที่ (Spatial Frequency) คือ")
|
| 176 |
+
st.markdown("""
|
| 177 |
+
ในทางสัญญาณภาพ (Spatial Frequency) ความถี่ไม่ได้หมายถึงความเร็วของเวลา แต่หมายถึง "อัตราการเปลี่ยนแปลงความเข้มของแสงในพื้นที่หนึ่งๆ"
|
| 178 |
+
|
| 179 |
+
1. **ความถี่เชิงพื้นที่ต่ำ (Low Spatial Frequency)**
|
| 180 |
+
- **คืออะไร:** พื้นที่ที่สีหรือความสว่าง "ค่อยๆ เปลี่ยน" หรือ "เหมือนเดิมเป็นบริเวณกว้าง" (เหมือนคลื่นลูกใหญ่ๆ ที่ขยับช้าๆ)
|
| 181 |
+
- **ตัวอย่างในภาพ MRI:** บริเวณเนื้อเยื่อก้อนใหญ่ๆ เช่น เนื้อตับ หรือเนื้อสมอง ที่มีสีเทาโทนเดียวกันกินพื้นที่กว้าง
|
| 182 |
+
- **ตำแหน่งใน k-space:** ข้อมูลเหล่านี้จะรวมตัวกันอยู่บริเวณ "ตรงกลาง"
|
| 183 |
+
- **หน้าที่หลัก:** สร้าง "รูปร่างรวมๆ และคอนทราสต์ (Contrast)" ให้เรารู้ว่านี่คือก้อนอวัยวะอะไร
|
| 184 |
+
|
| 185 |
+
2. **ความถี่เชิงพื้นที่สูง (High Spatial Frequency)**
|
| 186 |
+
- **คืออะไร:** พื้นที่ที่ความสว่าง เปลี่ยนแบบฉับพลันและรวดเร็ว ภายในระยะทางสั้นๆ เช่น จากขาว ตัดเป็นดำสนิททันที (เหมือนลายทางแคบๆ ที่สลับสีถี่ๆ)
|
| 187 |
+
- **ตัวอย่างในภาพ MRI:** ขอบของอวัยวะ (Edges), รอยต่อระหว่างกระดูกกับไขสันหลัง, หรือรายละเอียดเส้นเลือดเส้นเล็กๆ
|
| 188 |
+
- **ตำแหน่งใน k-space:** ข้อมูลเหล่านี้จะกระจายตัวอยู่บริเวณ "ขอบนอก"
|
| 189 |
+
- **หน้าที่หลัก:** สร้าง "ความคมชัด (Resolution) และรายละเอียดเล็กๆ" ทำให้ภาพไม่เบลอ
|
| 190 |
+
|
| 191 |
+
ซึ่งจะให้ผู้เรียนได้ลองปรับหน้าตาของภาพ K-Space แล้วเปรียบเทียบความแตกต่างด้วยการปรับ High-pass filter และ Low-pass filter เพื่อดูลักษณะและความสำคัญของข้อมูลบริเวณกลางและขอบนอกของ K-space
|
| 192 |
+
เมื่อเราจำแนกข้อมูลใน k-space ออกเป็นความถี่ต่ำ (ตรงกลาง) และความถี่สูง (ขอบนอก) ได้แล้ว เราสามารถเลือก "หยิบ" หรือ "ทิ้ง" ข้อมูลบางส่วนเพื่อดูผลลัพธ์ได้ เรียกว่าการใช้ตัวกรอง (Filter)
|
| 193 |
+
""")
|
| 194 |
+
|
| 195 |
st.divider()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
|
| 197 |
+
# --- Interactive Part 2: Slide bar แบบ High/Low Pass ---
|
| 198 |
+
st.subheader("ตัวกรอง K-Space (Interactive Filters)")
|
| 199 |
+
|
| 200 |
+
if 'filter_type' not in st.session_state:
|
| 201 |
+
st.session_state.filter_type = 'Low-pass Filter'
|
| 202 |
+
if 'filter_radius' not in st.session_state:
|
| 203 |
+
st.session_state.filter_radius = 112
|
| 204 |
+
|
| 205 |
+
def reset_filter():
|
| 206 |
+
st.session_state.filter_type = 'Low-pass Filter'
|
| 207 |
+
st.session_state.filter_radius = 112
|
| 208 |
+
|
| 209 |
+
col_f1, col_f2 = st.columns([1, 1.5])
|
| 210 |
with col_f1:
|
| 211 |
+
filter_choice = st.radio("เลือกรูปแบบ Filter", ['Low-pass Filter', 'High-pass Filter'], key='filter_type')
|
| 212 |
+
|
| 213 |
+
if filter_choice == 'Low-pass Filter':
|
| 214 |
+
st.slider("ปรับระดับการให้ข้อมูลผ่าน (จากเต็มไปหาแคบลง)", 0, 112, value=112, key='filter_radius', help="ยิ่งลดค่า ข้อมูลขอบนอกยิ่งหาย เหลือแต่ตรงกลาง")
|
| 215 |
+
else:
|
| 216 |
+
st.slider("ปรับระดับการตัดข้อมูลตรงกลาง (จาก 0 ไปหาสูงสุด)", 0, 112, value=0, key='filter_radius', help="ยิ่งเพิ่มค่า ข้อมูลตรงกลางยิ่งหาย เหลือแต่เส้นขอบ")
|
| 217 |
+
|
| 218 |
+
st.button("Reset ค่า Filter", on_click=reset_filter)
|
| 219 |
+
|
| 220 |
with col_f2:
|
| 221 |
+
if filter_choice == 'Low-pass Filter':
|
| 222 |
+
with st.expander("รายละเอียด Low-pass Filter", expanded=True):
|
| 223 |
+
st.markdown("""
|
| 224 |
+
**Low-pass Filter (ตัวกรองปล่อยความถี่ต่ำผ่าน):**
|
| 225 |
+
- **ทำงานอย่างไร:** "อนุญาตให้เฉพาะข้อมูลตรงกลาง (ความถี่ต่ำ) ผ่านไปสร้างภาพได้ ส่วนข้อมูลขอบนอก (ความถี่สูง) ให้ทิ้งไป"
|
| 226 |
+
- **ผลลัพธ์ที่ได้:** เราจะได้ภาพที่มี "คอนทราสต์" ดูออกว่าเป็นอวัยวะอะไร แต่ภาพจะ "เบลอ" (Blurry) เพราะข้อมูลเส้นขอบถูกทิ้งไปแล้ว
|
| 227 |
+
""")
|
| 228 |
+
else:
|
| 229 |
+
with st.expander("รายละเอียด High-pass Filter", expanded=True):
|
| 230 |
+
st.markdown("""
|
| 231 |
+
**High-pass Filter (ตัวกรองปล่อยความถี่สูงผ่าน):**
|
| 232 |
+
- **ทำงานอย่างไร:** อนุญาตให้เฉพาะข้อมูลขอบนอก (ความถี่สูง) ผ่านไปได้ ส่วนข้อมูลตรงกลางทิ้ง
|
| 233 |
+
- **ผลลัพธ์ที่ได้:** ภาพจะสูญเสียคอนทราสต์ไปจนเกือบมืดสนิท แต่จะปรากฏ "เส้นขอบร่าง" (Outline) ของอวัยวะขึ้นมาอย่างคมชัด
|
| 234 |
+
""")
|
| 235 |
+
|
| 236 |
+
# คำนวณผลลัพธ์ Filter
|
| 237 |
+
size = 224
|
| 238 |
+
center = size // 2
|
| 239 |
+
Y, X = np.ogrid[:size, :size]
|
| 240 |
+
dist_from_center = np.sqrt((X - center)**2 + (Y - center)**2)
|
| 241 |
+
|
| 242 |
+
mask = np.ones((size, size))
|
| 243 |
+
radius = st.session_state.filter_radius
|
| 244 |
+
|
| 245 |
+
if filter_choice == 'Low-pass Filter':
|
| 246 |
+
mask[dist_from_center > radius] = 0
|
| 247 |
+
elif filter_choice == 'High-pass Filter':
|
| 248 |
+
mask[dist_from_center < radius] = 0
|
| 249 |
+
|
| 250 |
+
filtered_kspace = kspace_data * mask
|
| 251 |
+
|
| 252 |
+
# แสดงผลภาพซ้ายและขวา
|
| 253 |
+
col_img1, col_img2 = st.columns(2)
|
| 254 |
+
|
| 255 |
+
with col_img1:
|
| 256 |
+
fig_k, ax_k = plt.subplots(figsize=(5, 5))
|
| 257 |
+
filt_k_mag = np.log(np.abs(filtered_kspace) + 1)
|
| 258 |
+
ax_k.imshow(filt_k_mag, cmap='gray')
|
| 259 |
+
ax_k.set_title(f"ภาพ K-Space ที่ถูกกรอง", fontweight='bold')
|
| 260 |
+
ax_k.axis('off')
|
| 261 |
+
st.pyplot(fig_k)
|
| 262 |
|
| 263 |
+
with col_img2:
|
| 264 |
+
mri_img = kspace_to_image(filtered_kspace)
|
| 265 |
+
fig_m, ax_m = plt.subplots(figsize=(5, 5))
|
| 266 |
+
ax_m.imshow(mri_img, cmap='gray')
|
| 267 |
+
ax_m.set_title("ภาพ MRI ผลลัพธ์ (ขนาด 224x224)", fontweight='bold')
|
| 268 |
+
ax_m.axis('off')
|
| 269 |
+
st.pyplot(fig_m)
|