Nicha1234 commited on
Commit
b981624
·
verified ·
1 Parent(s): 98baea6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +176 -539
app.py CHANGED
@@ -1,550 +1,187 @@
1
  import streamlit as st
2
  import numpy as np
 
3
  import matplotlib.pyplot as plt
4
- import matplotlib.patches as patches
5
- from mpl_toolkits.mplot3d import Axes3D
6
 
7
- # ==========================================
8
- # ⚙️ ตั้งค่าหน้าเว็บ (ต้องเป็นคำสั่งแรกเสมอ)
9
- # ==========================================
10
- st.set_page_config(page_title="MRI Physics Simulator", layout="wide", initial_sidebar_state="expanded")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  # ==========================================
13
- # 🎨 ฟังก์ชันวาดกราฟ 3D & 2D แบ Interactive
14
  # ==========================================
15
- def draw_3d_sphere(ax):
16
- u = np.linspace(0, 2 * np.pi, 50)
17
- v = np.linspace(0, np.pi, 50)
18
- x = np.outer(np.cos(u), np.sin(v))
19
- y = np.outer(np.sin(u), np.sin(v))
20
- z = np.outer(np.ones(np.size(u)), np.cos(v))
21
- ax.plot_surface(x, y, z, color='royalblue', alpha=0.1, edgecolor='none')
22
- ax.plot([-1.2, 1.2], [0, 0], [0, 0], color='gray', alpha=0.5, ls='--', lw=1)
23
- ax.plot([0, 0], [-1.2, 1.2], [0, 0], color='gray', alpha=0.5, ls='--', lw=1)
24
- ax.plot([0, 0], [0, 0], [-1.2, 1.2], color='white', alpha=0.5, ls='-', lw=1.5)
25
- ax.text(1.3, 0, 0, 'x', color='w', fontsize=10)
26
- ax.text(0, 1.3, 0, 'y', color='w', fontsize=10)
27
- ax.text(0, 0, 1.3, 'z (B0)', color='w', fontsize=10, fontweight='bold')
28
- ax.set_axis_off()
29
- ax.set_xlim([-1.2, 1.2]); ax.set_ylim([-1.2, 1.2]); ax.set_zlim([-1.2, 1.2])
30
- ax.view_init(elev=20, azim=45)
31
-
32
- def draw_2d_circle(ax):
33
- circle = patches.Circle((0, 0), 1, fill=False, color='gray', alpha=0.5)
34
- ax.add_patch(circle)
35
- ax.plot([-1.2, 1.2], [0, 0], 'gray', ls='--', alpha=0.5)
36
- ax.plot([0, 0], [-1.2, 1.2], 'gray', ls='--', alpha=0.5)
37
- ax.text(1.2, 0, 'x', color='w', fontsize=10)
38
- ax.text(0, 1.2, 'y', color='w', fontsize=10)
39
- ax.set_xlim([-1.3, 1.3]); ax.set_ylim([-1.3, 1.3])
40
- ax.set_axis_off()
41
- ax.set_aspect('equal')
42
-
43
- def draw_vector(ax, origin, vec, color, lw=2, alpha=1.0, is_3d=True):
44
- if is_3d:
45
- ax.quiver(origin[0], origin[1], origin[2], vec[0], vec[1], vec[2], color=color, linewidth=lw, alpha=alpha)
46
- else:
47
- ax.annotate('', xy=(vec[0], vec[1]), xytext=(origin[0], origin[1]),
48
- arrowprops=dict(arrowstyle="->", color=color, lw=lw, alpha=alpha))
49
-
50
- def plot_interactive_sequence(seq_type, step):
51
- plt.style.use('dark_background')
52
- fig = plt.figure(figsize=(10, 5))
53
- fig.patch.set_facecolor('#1e1e2f')
54
- fig.subplots_adjust(left=0.05, right=0.95, top=0.95, bottom=0.05)
55
-
56
- ax_3d = fig.add_axes([0.05, 0.55, 0.4, 0.45], projection='3d')
57
- ax_2d = fig.add_axes([0.55, 0.55, 0.4, 0.45])
58
- ax_t = fig.add_axes([0.05, 0.05, 0.9, 0.4])
59
-
60
- ax_3d.set_facecolor('#1e1e2f')
61
- ax_2d.set_facecolor('#1e1e2f')
62
- ax_t.set_facecolor('#1e1e2f')
63
-
64
- draw_3d_sphere(ax_3d)
65
- ax_3d.set_title("3D Vector (Mz)", color='w', fontsize=12, pad=-10, loc='left')
66
- draw_2d_circle(ax_2d)
67
- ax_2d.set_title("Transverse Plane (Mxy)", color='w', fontsize=12, pad=-10, loc='left')
68
-
69
- ax_t.set_title("Detailed Pulse Sequence Timeline", color='w', fontsize=12, pad=10, loc='left')
70
- ax_t.axis('off')
71
- ax_t.set_xlim(0, 10); ax_t.set_ylim(-0.5, 4.5)
72
-
73
- ax_t.text(-0.2, 4.0, "RF", color='cyan', va='center', ha='right', fontweight='bold')
74
- ax_t.text(-0.2, 3.0, "Gz (Slice)", color='orange', va='center', ha='right', fontweight='bold')
75
- ax_t.text(-0.2, 2.0, "Gy (Phase)", color='yellow', va='center', ha='right', fontweight='bold')
76
- ax_t.text(-0.2, 1.0, "Gx (Freq)", color='orange', va='center', ha='right', fontweight='bold')
77
- ax_t.text(-0.2, 0.0, "Signal", color='w', va='center', ha='right', fontweight='bold')
78
-
79
- for i in range(5):
80
- ax_t.plot([0, 10], [i, i], color='gray', alpha=0.3)
81
-
82
- if seq_type == 'SE':
83
- ax_t.plot([4, 4], [-0.5, 4.5], 'gray', ls='--', alpha=0.3)
84
- ax_t.plot([7, 7], [-0.5, 4.5], 'gray', ls='--', alpha=0.3)
85
- ax_t.text(4, -0.4, "TE/2", color='gray', ha='center', fontsize=9)
86
- ax_t.text(7, -0.4, "TE (Echo)", color='gray', ha='center', fontsize=9)
87
-
88
- ax_t.plot([1.0, 1.0], [4.0, 4.5], color='cyan', lw=3); ax_t.text(1.0, 4.6, "90°", color='cyan', ha='center')
89
- ax_t.plot([4.0, 4.0], [4.0, 4.7], color='cyan', lw=4); ax_t.text(4.0, 4.8, "180°", color='cyan', ha='center')
90
-
91
- ax_t.fill_between([0.8, 1.2], 3.0, 3.4, color='orange', alpha=0.8)
92
- ax_t.fill_between([1.2, 1.6], 3.0, 2.7, color='orange', alpha=0.8)
93
- ax_t.fill_between([3.7, 4.3], 3.0, 3.4, color='orange', alpha=0.8)
94
-
95
- for offset in np.linspace(-0.3, 0.3, 5):
96
- ax_t.plot([2.0, 2.5], [2+offset, 2+offset], 'yellow', lw=2)
97
- ax_t.fill_between([2.0, 2.5], 1.7, 2.3, color='yellow', alpha=0.2)
98
-
99
- ax_t.fill_between([2.6, 3.1], 1.0, 1.4, color='orange', alpha=0.8)
100
- ax_t.fill_between([6.0, 8.0], 1.0, 1.4, color='orange', alpha=0.8)
101
-
102
- x_sig = np.linspace(6.2, 7.8, 50)
103
- y_sig = 0.0 + 0.8 * np.exp(-((x_sig-7)**2)/0.1) * np.sin(30*x_sig)**2
104
- ax_t.plot(x_sig, y_sig, color='red', lw=2)
105
-
106
- if step == 1:
107
- draw_vector(ax_3d, (0,0,0), (0,0,1), 'royalblue')
108
- ax_2d.text(0, -0.3, "No Signal", color='gray', ha='center')
109
- elif step == 2:
110
- draw_vector(ax_3d, (0,0,0), (1,0,0), 'crimson')
111
- draw_vector(ax_2d, (0,0), (1,0), 'crimson', is_3d=False)
112
- ax_t.axvline(1.0, color='yellow', ls=':', alpha=0.5)
113
- elif step == 3:
114
- draw_vector(ax_3d, (0,0,0), (np.cos(0.4),np.sin(0.4),0), 'crimson', alpha=0.6)
115
- draw_vector(ax_3d, (0,0,0), (1,0,0), 'crimson', alpha=0.6)
116
- draw_vector(ax_3d, (0,0,0), (np.cos(-0.4),np.sin(-0.4),0), 'crimson', alpha=0.6)
117
- draw_vector(ax_2d, (0,0), (np.cos(0.4),np.sin(0.4)), 'crimson', alpha=0.6, is_3d=False)
118
- draw_vector(ax_2d, (0,0), (1,0), 'crimson', alpha=0.6, is_3d=False)
119
- draw_vector(ax_2d, (0,0), (np.cos(-0.4),np.sin(-0.4)), 'crimson', alpha=0.6, is_3d=False)
120
- ax_t.axvline(2.5, color='yellow', ls=':', alpha=0.5)
121
- elif step == 4:
122
- draw_vector(ax_3d, (0,0,0), (-np.cos(0.4),np.sin(0.4),0), 'violet', alpha=0.8)
123
- draw_vector(ax_3d, (0,0,0), (-1,0,0), 'violet', alpha=0.8)
124
- draw_vector(ax_3d, (0,0,0), (-np.cos(-0.4),np.sin(-0.4),0), 'violet', alpha=0.8)
125
- draw_vector(ax_2d, (0,0), (-np.cos(0.4),np.sin(0.4)), 'violet', alpha=0.8, is_3d=False)
126
- draw_vector(ax_2d, (0,0), (-1,0), 'violet', alpha=0.8, is_3d=False)
127
- draw_vector(ax_2d, (0,0), (-np.cos(-0.4),np.sin(-0.4)), 'violet', alpha=0.8, is_3d=False)
128
- ax_t.axvline(4.0, color='yellow', ls=':', alpha=0.5)
129
- elif step == 5:
130
- draw_vector(ax_3d, (0,0,0), (-np.cos(0.2),np.sin(0.2),0), 'forestgreen', alpha=0.8)
131
- draw_vector(ax_3d, (0,0,0), (-1,0,0), 'forestgreen', alpha=0.8)
132
- draw_vector(ax_3d, (0,0,0), (-np.cos(-0.2),np.sin(-0.2),0), 'forestgreen', alpha=0.8)
133
- draw_vector(ax_2d, (0,0), (-np.cos(0.2),np.sin(0.2)), 'forestgreen', alpha=0.8, is_3d=False)
134
- draw_vector(ax_2d, (0,0), (-1,0), 'forestgreen', alpha=0.8, is_3d=False)
135
- draw_vector(ax_2d, (0,0), (-np.cos(-0.2),np.sin(-0.2)), 'forestgreen', alpha=0.8, is_3d=False)
136
- ax_t.axvline(5.5, color='yellow', ls=':', alpha=0.5)
137
- elif step == 6:
138
- draw_vector(ax_3d, (0,0,0), (-1,0,0), 'red', lw=3)
139
- draw_vector(ax_2d, (0,0), (-1,0), 'red', lw=3, is_3d=False)
140
- ax_t.axvline(7.0, color='yellow', ls=':', alpha=0.5)
141
-
142
- elif seq_type == 'GRE':
143
- ax_t.plot([5, 5], [-0.5, 4.5], 'gray', ls='--', alpha=0.3)
144
- ax_t.text(5, -0.4, "TE (Echo)", color='gray', ha='center', fontsize=9)
145
-
146
- ax_t.plot([1.0, 1.0], [4.0, 4.4], color='cyan', lw=3); ax_t.text(1.0, 4.5, "α°", color='cyan', ha='center')
147
- ax_t.fill_between([0.8, 1.2], 3.0, 3.4, color='orange', alpha=0.8)
148
- ax_t.fill_between([1.2, 1.6], 3.0, 2.7, color='orange', alpha=0.8)
149
-
150
- for offset in np.linspace(-0.3, 0.3, 5):
151
- ax_t.plot([2.0, 2.5], [2+offset, 2+offset], 'yellow', lw=2)
152
- ax_t.fill_between([2.0, 2.5], 1.7, 2.3, color='yellow', alpha=0.2)
153
-
154
- ax_t.fill_between([2.6, 3.1], 1.0, 0.6, color='orange', alpha=0.8)
155
- ax_t.fill_between([4.0, 6.0], 1.0, 1.4, color='orange', alpha=0.8)
156
-
157
- x_sig = np.linspace(4.2, 5.8, 50)
158
- y_sig = 0.0 + 0.8 * np.exp(-((x_sig-5)**2)/0.1) * np.sin(30*x_sig)**2
159
- ax_t.plot(x_sig, y_sig, color='red', lw=2)
160
-
161
- if step == 1:
162
- draw_vector(ax_3d, (0,0,0), (0,0,1), 'royalblue')
163
- ax_2d.text(0, -0.3, "No Signal", color='gray', ha='center')
164
- elif step == 2:
165
- draw_vector(ax_3d, (0,0,0), (0.7,0,0.7), 'cyan')
166
- draw_vector(ax_2d, (0,0), (0.7,0), 'cyan', is_3d=False)
167
- ax_t.axvline(1.0, color='yellow', ls=':', alpha=0.5)
168
- elif step == 3:
169
- draw_vector(ax_3d, (0,0,0), (0.7*np.cos(0.5),0.7*np.sin(0.5),0.7), 'orange', alpha=0.6)
170
- draw_vector(ax_3d, (0,0,0), (0.7,0,0.7), 'orange', alpha=0.6)
171
- draw_vector(ax_3d, (0,0,0), (0.7*np.cos(-0.5),0.7*np.sin(-0.5),0.7), 'orange', alpha=0.6)
172
- draw_vector(ax_2d, (0,0), (0.7*np.cos(0.5),0.7*np.sin(0.5)), 'orange', alpha=0.6, is_3d=False)
173
- draw_vector(ax_2d, (0,0), (0.7,0), 'orange', alpha=0.6, is_3d=False)
174
- draw_vector(ax_2d, (0,0), (0.7*np.cos(-0.5),0.7*np.sin(-0.5)), 'orange', alpha=0.6, is_3d=False)
175
- ax_t.axvline(2.8, color='yellow', ls=':', alpha=0.5)
176
- elif step == 4:
177
- draw_vector(ax_3d, (0,0,0), (0.7*np.cos(0.2),0.7*np.sin(0.2),0.7), 'yellowgreen', alpha=0.8)
178
- draw_vector(ax_3d, (0,0,0), (0.7,0,0.7), 'yellowgreen', alpha=0.8)
179
- draw_vector(ax_3d, (0,0,0), (0.7*np.cos(-0.2),0.7*np.sin(-0.2),0.7), 'yellowgreen', alpha=0.8)
180
- draw_vector(ax_2d, (0,0), (0.7*np.cos(0.2),0.7*np.sin(0.2)), 'yellowgreen', alpha=0.8, is_3d=False)
181
- draw_vector(ax_2d, (0,0), (0.7,0), 'yellowgreen', alpha=0.8, is_3d=False)
182
- draw_vector(ax_2d, (0,0), (0.7*np.cos(-0.2),0.7*np.sin(-0.2)), 'yellowgreen', alpha=0.8, is_3d=False)
183
- ax_t.axvline(4.0, color='yellow', ls=':', alpha=0.5)
184
- elif step == 5:
185
- draw_vector(ax_3d, (0,0,0), (0.7,0,0.7), 'red', lw=3)
186
- draw_vector(ax_2d, (0,0), (0.7,0), 'red', lw=3, is_3d=False)
187
- ax_t.axvline(5.0, color='yellow', ls=':', alpha=0.5)
188
-
189
- st.pyplot(fig)
190
- plt.close(fig)
191
 
192
- # ==========================================
193
- # 🧭 เมนูด้านซ้าย (Sidebar)
194
- # ==========================================
195
- st.sidebar.title("เมนูหลัก (Navigation)")
196
- page = st.sidebar.radio("เลือกหน้าเน้อหา:", [
197
- "🧠 1. Image Formation (Intro - K-Space)",
198
- "🌊 2. Pulse Sequences & Simulator"
199
- ])
200
- st.sidebar.markdown("---")
201
-
202
- # ==============================================================================
203
- # หน้าที่ 1: IMAGE FORMATION
204
- # ==============================================================================
205
- if page == "🧠 1. Image Formation (Intro - K-Space)":
206
- st.title("🧠 บทที่ 3: รูปแบบภาพและลำดับพัลส์พื้นฐานของเอ็มอาร์ไอ")
207
-
208
- st.header("การกำหนดระบบพิกัด (MRI Coordinate System)")
209
- st.write("**แกน Z (Longitudinal):** แนวขนานกับตัวผู้ป่วย (จากหัวไปเท้า)")
210
- st.write("**แกน Y (Vertical):** แนวตั้งฉากกับพื้น (จากหน้าไปหลัง/ท้องไปหลัง)")
211
- st.write("**แกน X (Horizontal):** แนวนอน (จากซ้ายไปขวา)")
212
- st.markdown("---")
213
-
214
- st.header("จากสปินสู่สัญญาณ NMR (Signal Generation)")
215
- st.write("1. เมื่อสปินอยู่ในสนามแม่เหล็กหลัก (B0) จะเกิดแรงแม่เหล็กสุทธิ (Net Magnetization)")
216
- st.write("2. เมื่อส่งคลื่น RF Pulse ที่ความถี่ Larmor Frequency เข้าไป สปินจะถูกผลักให้เบนออกจากแนวเดิม")
217
- st.write("3. เมื่อหยุดส่ง RF สปินจะคายพลังงานกลับคืนสู่สภาวะสมดุล เกิดสัญญาณ NMR Signal (FID)")
218
- st.info("สัญญาณ NMR (อนาล็อ) ➡️ ADC (ิจิทั) ➡️ ทึกลงใน K-space ➡️ Fourier Transform ➡️ ภาพ MRI")
219
- st.markdown("---")
220
-
221
- st.title("Image Formation")
222
-
223
- # ---------------------------------------------------------
224
- st.subheader("1. การระบุตำแหน่งของสปินโดยใช้สนามแม่เหล็กเกรเดียนท์ (Spin localization)")
225
- st.write("- **โหมดไม่เปิด Gradient:** สัญญาณ NMR จะมีเพียงความถี่เดียว")
226
- st.write("- **โหมดเปิด Gradient (Gx):** จะทำให้สปินหมุนควงด้วยความถี่ที่ต่า��กัน จึงสามารถระบุตำแหน่งสัญญาณได้")
227
- st.write("- **ความถี่ของสปินจะเพิ่มขึ้นเมื่อสนามแม่เหล็กเกรเดียนท์ (Gx) มีความแรงเพิ่มขึ้น**")
228
-
229
- use_gradient = st.toggle("เปิด/ปิดการทำงานของ Gradient (Gx)", value=True)
230
-
231
- fig1, ax1 = plt.subplots(figsize=(8, 3))
232
- fig1.subplots_adjust(left=0.1, right=0.95, top=0.85, bottom=0.2)
233
- x_pos = np.linspace(-10, 10, 10)
234
-
235
- if use_gradient:
236
- freq = 63.85 + (0.01 * x_pos)
237
- title = "Gradient ON: Frequencies vary by position"
238
- color = 'r'
239
- else:
240
- freq = np.full_like(x_pos, 63.85)
241
- title = "Gradient OFF: All spins have the same frequency"
242
- color = 'blue'
243
-
244
- ax1.plot(x_pos, freq, marker='o', color=color, lw=2)
245
- ax1.vlines(x_pos, 63.7, freq, colors=color, alpha=0.5)
246
- ax1.set_xlabel("X Position (cm)")
247
- ax1.set_ylabel("Larmor Freq (MHz)")
248
- ax1.set_title(title)
249
- ax1.grid(True, alpha=0.3)
250
- ax1.set_ylim(63.7, 63.98)
251
  st.pyplot(fig1)
252
- st.markdown("---")
253
-
254
- # ---------------------------------------------------------
255
- st.subheader("2. การเลือกสไลซ์ (Slice Selection)")
256
- st.write("การเลือกตำแหนงสไลซ์ทำได้โดยการใช้ RF pulse ที่มีความถี่เท่ากับ Larmor Frequency ที่จำเพาะกับระยะทางในแนวแกน Z ที่เราสนใจทำการสแกน โดยใช้สนามแม่เหล็กเกรเดียนท์ Gz")
257
- st.latex(r"Slice\ Thickness = \frac{2\pi \cdot BW}{\gamma \cdot G_z}")
258
-
259
- st.markdown('<div style="background-color:#ffebee; padding:15px; border-radius:10px;">', unsafe_allow_html=True)
260
- col_s1, col_s2 = st.columns(2)
261
- with col_s1:
262
- gz_slope = st.slider("ปรับความชันเกรเดียนท์ Gz (mT/m)", 10, 50, 25, step=1)
263
- with col_s2:
264
- bandwidth = st.slider("ปรับ Bandwidth คลื่นวิทยุ (Hz)", 500, 2000, 1000, step=50)
265
-
266
- slice_thickness = (bandwidth / gz_slope) * 0.1
267
- st.markdown(f"**ความหนาของสไลซ์ที่ได้:** <span style='color:red; font-size:18px;'>{slice_thickness:.2f} mm</span>", unsafe_allow_html=True)
268
-
269
- fig2, ax2 = plt.subplots(figsize=(8, 3))
270
- fig2.subplots_adjust(left=0.1, right=0.95, top=0.85, bottom=0.2)
271
- z_axis = np.linspace(-10, 10, 100)
272
- freq_line = gz_slope * z_axis
273
- ax2.plot(z_axis, freq_line, label=f'Gz = {gz_slope}', color='blue', lw=2)
274
- ax2.axhline(y=bandwidth/2, color='red', linestyle='--', label='+ BW/2', lw=2)
275
- ax2.axhline(y=-bandwidth/2, color='red', linestyle='--', label='- BW/2', lw=2)
276
- ax2.fill_betweenx([-bandwidth/2, bandwidth/2], -slice_thickness/2, slice_thickness/2, color='red', alpha=0.3, label='Slice Thickness')
277
- ax2.set_xlabel('Z Position')
278
- ax2.set_ylabel('Frequency')
279
- ax2.set_title('Slice Selection Simulation')
280
- ax2.set_ylim(-600, 600)
281
- ax2.legend(fontsize=10, loc='lower right')
282
  st.pyplot(fig2)
283
- st.markdown('</div>', unsafe_allow_html=True)
284
- st.markdown("---")
285
-
286
- # ---------------------------------------------------------
287
- st.subheader("3. การเข้ารัสใแนวความถี่และแนวเฟส (Frequency and Phase encoding)")
288
- st.write("ในกาสร้าง 2 ิติำเป็้องใช้ Gx และ Gy เพให้ควา่และ่ากัน:")
289
- st.write("- **ระX เป็นแนวควมถี(Frequency Encoding):** ทำห้ควมเร็วการมุนต่างกัน")
290
- st.write("- **ระนาบ Y เป็นแนเฟส (Phase Encoding):** ทำให้มุมเริ่มต้น (เฟส) การมุต่างก")
291
-
292
- fig3, (ax_f, ax_p) = plt.subplots(1, 2, figsize=(10, 3.5))
293
- fig3.subplots_adjust(left=0.05, right=0.95, top=0.85, bottom=0.1)
294
-
295
- t = np.linspace(0, 4*np.pi, 200)
296
- ax_f.plot(t, np.sin(3*t) + 2.5, 'yellow', lw=2)
297
- ax_f.plot(t, np.sin(t) - 2.5, 'yellow', lw=2)
298
- ax_f.text(12, 3.5, "High Freq", color='white')
299
- ax_f.text(12, -1.5, "Low Freq", color='white')
300
- ax_f.set_title("Different Frequencies", color='white')
301
- ax_f.set_facecolor('#000033')
302
- ax_f.axis('off')
303
-
304
- ax_p.plot(t, np.sin(t) + 2.5, 'yellow', lw=2)
305
- ax_p.plot(t, -np.cos(t) - 2.5, 'yellow', lw=2)
306
-
307
- circ1 = patches.Circle((15, 2.5), 1.8, fill=False, color='w')
308
- circ2 = patches.Circle((15, -2.5), 1.8, fill=False, color='w')
309
- ax_p.add_patch(circ1); ax_p.add_patch(circ2)
310
- ax_p.arrow(15, 2.5, 0, -1.2, head_width=0.4, color='yellow', lw=2)
311
- ax_p.arrow(15, -2.5, 1.2, 0, head_width=0.4, color='pink', lw=2)
312
- ax_p.text(16, 1.5, "270 Deg", color='pink')
313
- ax_p.text(16.5, -2.5, "0 Deg", color='pink')
314
- ax_p.set_xlim(0, 18)
315
- ax_p.set_title("Different Phases", color='white')
316
- ax_p.set_facecolor('#000033')
317
- ax_p.axis('off')
318
-
319
- fig3.patch.set_facecolor('#000033')
320
- st.pyplot(fig3)
321
- st.markdown("---")
322
-
323
- # ---------------------------------------------------------
324
- st.subheader("4. พิกัด k-space แบบสองมิ (2D K-space)")
325
- st.write("การใช้ Gx และ Gy เสมืนการสร้างพิกดเพื่อให้สปิเข้าไปเติมใ K-space โดยปกติการเก็บข้อมูลใ K-space แบบ 2D จะเก็บทีละเส้นในแนวแกน ky จากศูนย์กลางออกไป หรือจากล่างขึ้นบนจนเต็ม")
326
-
327
- k_step_radio = st.radio("เือกำดบเวลา (Timeline Step):", ["A. เร่มต้น", "B. เรก็บข้อมูล", "C. ึ่าง", "D. สิ้นสุดเส้น"], horizontal=True, label_visibility="collapsed")
328
- k_step = k_step_radio[0]
329
-
330
- if k_step == "A":
331
- st.info("📍 **ตแหน่ง A:** Gx และ Gy เพิ่งเริ่มทำงาน (สัพัธ์กับจุกึ่งกลางขอ kx และ ky)")
332
- time_x, k_x_pos = 0.5, 0.5
333
- elif k_step == "B":
334
- st.success("📍 **ตำแหน่ง B:** เริ่มบันทึกข้อมูลจุดแรก ของเฟสเส้นนั้น")
335
- time_x, k_x_pos = 2.0, 0.1
336
- elif k_step == "C":
337
- st.warning("📍 **ตำแหน่ง C:** บันทึกข้อมูลถึงกึ่งกลาง K-space ของเส้นนั้น")
338
- time_x, k_x_pos = 3.5, 0.5
339
- else:
340
- st.error("📍 **ตำแหน่ง D:** บันทึกข้อมูลเสร็จสิ้นสำหรับเส้นนั้น (เตรียมเปลี่ยนแถว Gy)")
341
- time_x, k_x_pos = 5.0, 0.9
342
-
343
- plt.style.use('dark_background')
344
- fig_ks, (ax_seq, ax_k) = plt.subplots(1, 2, figsize=(10, 4))
345
- fig_ks.subplots_adjust(left=0.05, right=0.95, top=0.85, bottom=0.1)
346
- fig_ks.patch.set_facecolor('#1e1e2f')
347
- ax_seq.set_facecolor('#1e1e2f'); ax_k.set_facecolor('#1e1e2f')
348
-
349
- ax_seq.axis('off')
350
- ax_seq.text(-0.5, 3, 'Gy', va='center', fontweight='bold', color='white')
351
- ax_seq.text(-0.5, 1.5, 'Gx', va='center', fontweight='bold', color='white')
352
- ax_seq.text(-0.5, 0, 'Echo', va='center', fontweight='bold', color='white')
353
- ax_seq.axhline(3, color='gray', lw=0.5); ax_seq.axhline(1.5, color='gray', lw=0.5); ax_seq.axhline(0, color='gray', lw=0.5)
354
-
355
- ax_seq.fill_between([0.5, 2.0], [3, 3], [3.8, 3.8], color='gray', alpha=0.5)
356
- ax_seq.fill_between([0.5, 2.0], [3, 3], [2.2, 2.2], facecolor='none', edgecolor='gray', hatch='--')
357
- ax_seq.fill_between([0.5, 2.0], [1.5, 1.5], [0.7, 0.7], color='gray', alpha=0.5)
358
- ax_seq.fill_between([2.0, 5.0], [1.5, 1.5], [2.3, 2.3], color='gray', alpha=0.5)
359
-
360
- x_echo = np.linspace(2.0, 5.0, 100)
361
- y_echo = 0.8 * np.exp(-((x_echo-3.5)**2)/0.2) * np.sin(30*x_echo)
362
- ax_seq.plot(x_echo, y_echo, 'white')
363
-
364
- ax_seq.axvline(time_x, color='red', linestyle='--', lw=2)
365
- ax_seq.text(time_x, -0.5, f" {k_step} ", color='red', ha='center', bbox=dict(facecolor='white', edgecolor='red', boxstyle='circle'))
366
- ax_seq.set_title("Sequence Timeline", fontweight='bold', color='white')
367
- ax_seq.set_ylim(-1, 4.5)
368
-
369
- ax_k.axis('off')
370
- ax_k.axhline(0.5, color='gray', lw=0.5); ax_k.axvline(0.5, color='gray', lw=0.5)
371
- ax_k.text(0.95, 0.52, 'kx', color='white'); ax_k.text(0.52, 0.95, 'ky', color='white')
372
-
373
- for i in np.linspace(0.1, 0.9, 9):
374
- ax_k.plot([0.1, 0.9], [i, i], color='gray', lw=1)
375
-
376
- ax_k.plot([0.1, 0.9], [0.5, 0.5], color='cyan', lw=3)
377
- ax_k.plot(k_x_pos, 0.5, 'ro', markersize=10, zorder=5)
378
-
379
- if k_step == "A":
380
- ax_k.plot(0.5, 0.5, 'wo', markersize=8)
381
- ax_k.annotate('', xy=(0.1, 0.5), xytext=(0.5, 0.5), arrowprops=dict(arrowstyle='->', ls='--', color='gray'))
382
- ax_k.text(0.52, 0.55, 'A', color='white')
383
-
384
- ax_k.set_title("K-Space Trajectory", fontweight='bold', color='white')
385
- ax_k.set_xlim(0, 1.1); ax_k.set_ylim(0, 1.1)
386
- st.pyplot(fig_ks)
387
-
388
- # --- จำลองการเก็บข้อมูล K-space ---
389
- st.markdown('<div style="background-color:#e8eaf6; padding:15px; border-radius:10px;">', unsafe_allow_html=True)
390
- st.markdown('**Interactive: การเติมข้อมูล K-space ทีละเฟส (Phase Encoding จากจุดกึ่งกลางขยายออก)**')
391
-
392
- lines_to_fill = st.slider("จำนวนเส้น Phase Encoding (Ky)", 2, 64, 16, step=2)
393
-
394
- base_img = np.zeros((64, 64))
395
- cv, rv = np.meshgrid(np.arange(64), np.arange(64))
396
- base_img[((cv-32)**2 / 15**2) + ((rv-32)**2 / 20**2) < 1] = 1
397
- base_img[((cv-25)**2 / 4**2) + ((rv-35)**2 / 6**2) < 1] = 0.5
398
-
399
- k_space = np.fft.fftshift(np.fft.fft2(base_img))
400
- mask = np.zeros((64, 64))
401
-
402
- # เก็บจากศูนย์กลางออกไปด้านนอก (Center-out)
403
- center = 32
404
- half_lines = lines_to_fill // 2
405
- mask[center-half_lines : center+half_lines, :] = 1
406
-
407
- k_space_masked = k_space * mask
408
- recon_img = np.abs(np.fft.ifft2(np.fft.ifftshift(k_space_masked)))
409
-
410
- plt.style.use('default')
411
- fig_recon, (ax_rk, ax_ri) = plt.subplots(1, 2, figsize=(8, 3.5))
412
- fig_recon.subplots_adjust(left=0.05, right=0.95, top=0.85, bottom=0.1)
413
- ax_rk.imshow(np.log(1 + np.abs(k_space_masked)), cmap='gray')
414
- ax_rk.set_title(f"K-Space (Filled {lines_to_fill} lines center-out)")
415
- ax_rk.axis('off')
416
-
417
- ax_ri.imshow(recon_img, cmap='gray')
418
- ax_ri.set_title("Reconstructed Image")
419
- ax_ri.axis('off')
420
- st.pyplot(fig_recon)
421
- st.markdown('</div>', unsafe_allow_html=True)
422
-
423
- # ==============================================================================
424
- # PAGE 2: PULSE SEQUENCES
425
- # ==============================================================================
426
  else:
427
- st.title("🌊 บทที3: Basic Pulse Sequences")
428
-
429
- t_se, t_fse, t_ir, t_gre = st.tabs([
430
- "🔵 Spin Echo (SE)",
431
- "⚡ Fast Spin Echo",
432
- "🔄 Inversion Recovery (IR)",
433
- "🟢 Gradient Echo (GRE)"
434
- ])
435
-
436
- # ---------------------------------------------------------
437
- # TAB 1: Spin Echo
438
- # ---------------------------------------------------------
439
- with t_se:
440
- st.header("1. ลำดับพัลส์สปินเอคโค (Spin Echo Sequence)")
441
- st.write("ทำการกระตุ้นสปินด้วย 90° RF pulse ณ ตำแหน่งสไลซ์ที่เลือกโดยใช้สนามแม่เหล็กเกรเดียนท์ Gss เมื่อเวลาผ่านไป TE/2 จะทำการกลับเฟสของสปินด้วย 180° RF pulse (Refocusing pulse) จะเกิดการรวมเฟสของสปินที่เวลา TE ซึ่งเป็นเวลาที่สัญญาณมีค่าสูงสุด (echo signal)")
442
-
443
- step_se_radio = st.radio("เลือกสเต็ป (SE):", ["1", "2", "3", "4", "5", "6"], horizontal=True, label_visibility="collapsed")
444
- step_se = int(step_se_radio)
445
-
446
- plot_interactive_sequence('SE', step_se)
447
-
448
- se_descriptions = {
449
- 1: "1. สภาวะสมดุล: เวกเตอร์แม่เหล็กสุทธิ (Net Magnetization) จัดเรียงตัวตามทิศทางของสนามแม่เหล็กหลัก B0 (แกน Z)",
450
- 2: "2. 90° RF: คลื่นวิทยุทำมุม 90 องศา ผลักเวกเตอร์ให��ลงมาอยู่ในระนาบตัดขวาง (Mxy) และมีการใช้ Gz เพื่อเลือก Slice",
451
- 3: "3. Dephase: สปินเริ่มหมุนด้วยความถี่ที่ต่างกันจากการใช้ Gx Pre-phase ทำให้กระจายเฟสออกจากกัน",
452
- 4: "4. 180° RF: ยิงคลื่นวิทยุ 180 องศา เพื่อพลิกกลับทิศทางของสปิน (Refocusing pulse)",
453
- 5: "5. Rephase: สปินที่กระจายออกไป เริ่มหมุนกลับมารวมเฟสกันอีกครั้งระหว่างที่เปิด Gx Readout",
454
- 6: "6. Echo: สปินรวมเฟสกันอย่างสมบูรณ์ เกิดสัญญาณสะท้อนกลับ (Echo signal) สูงสุดที่เวลา TE"
455
- }
456
- st.info(f"**คำอธิบาย:** {se_descriptions[step_se]}")
457
-
458
- # ---------------------------------------------------------
459
- # TAB 2: Fast Spin Echo
460
- # ---------------------------------------------------------
461
- with t_fse:
462
- st.header("2. ลำดับพัลส์สปินเอคโคแบบเร็ว (Fast Spin Echo)")
463
- st.write("การสแกนแบบ Spin Echo ปกติอาจใช้เวลานาน Fast Spin Echo จึงถูกพัฒนาขึ้นโดยการเพิ่มคลื่น 180° (Refocusing pulse) หลายๆ ครั้งภายใน 1 TR ทำให้สามารถเก็บข้อมูล Echo ได้หลายเส้นในครั้งเดียว (เรียกว่า Echo Train Length หรือ ETL) ซึ่งช่วยลดเวลาในการสร้างภาพ")
464
-
465
- fse_step_radio = st.radio("เลือกสเต็ป (FSE):", ["1", "2", "3", "4", "5", "6"], horizontal=True, label_visibility="collapsed")
466
- step_fse = int(fse_step_radio)
467
- plot_interactive_sequence('SE', step_fse)
468
-
469
- fse_descriptions = {
470
- 1: "1. สภาวะสมดุล: เวกเตอร์แม่เหล็กสุทธิจัดเรียงตัวตามแกน Z",
471
- 2: "2. 90° RF: พลิกสปินลงมาอยู่ในระนาบตัดขวาง เริ่มกระบวนการ Dephase",
472
- 3: "3. 180° RF (1): ยิงคลื่น Refocusing ลูกแรก เพื่อให้สปินกลับมารวมกัน",
473
- 4: "4. Echo 1: เกิดสัญญาณ Echo ลูกแรก (เก็บข้อมูลเส้นแรก)",
474
- 5: "5. 180° RF (2): ยิงคลื่น Refocusing ลูกที่สอง เพื่อดึงสปินให้กลับมารวมกันอีกครั้ง (ทำซ้ำจนครบ ETL)",
475
- 6: "6. Echo 2: เกิดสัญญาณ Echo ลูกที่สอง (เก็บข้อมูลเส้นถัดไป ทำให้สแกนได้เร็วขึ้นมาก)"
476
- }
477
- st.info(f"**คำอธิบาย:** {fse_descriptions[step_fse]}")
478
-
479
- st.markdown('#### ⏱️ Interactive Scan Time Calculator')
480
- st.latex(r"Scan\ Time\ = TR \times N_{phase} \times \frac{1}{ETL}")
481
-
482
- c1, c2, c3 = st.columns(3)
483
- tr = c1.number_input("TR (ms)", 100, 5000, 500, step=100)
484
- n_phase = c2.number_input("N-phase", 64, 512, 256, step=64)
485
- etl = c3.select_slider("ETL (ลดเวลาสแกน)", [1, 2, 4, 8, 16], value=4)
486
-
487
- base_time = (tr * n_phase) / 1000
488
- new_time = base_time / etl
489
- st.markdown(f"**เวลาสแกนแบบ SE:** {base_time:.1f} วินาที")
490
- st.markdown(f"**เวลาสแกนแบบ FSE ลดลง {etl} เท่า! เหลือเพียง:** <span style='color:red; font-size:20px;'>{new_time:.1f} วินาที</span>", unsafe_allow_html=True)
491
-
492
- # ---------------------------------------------------------
493
- # TAB 3: Inversion Recovery
494
- # ---------------------------------------------------------
495
- with t_ir:
496
- st.header("3. ลำดับพัลส์ Inversion Recovery (IR)")
497
- st.write("ลำดับพัลส์พิเศษที่เริ่มต้นด้วยการยิงคลื่น 180° เพื่อกลับทิศทางของแกนแม่เหล็กให้ติดลบ จากนั้นรอเวลา TI (Inversion Time) ก่อนที่จะยิงคลื่น 90° ตามปกติ เทคนิคนี้ใช้กดสัญญาณของเนื���อเยื่อที่เราไม่ต้องการเห็นในภาพ")
498
-
499
- step_ir_radio = st.radio("เลือกสเต็ป (IR):", ["1", "2", "3", "4", "5", "6"], horizontal=True, label_visibility="collapsed")
500
- step_ir = int(step_ir_radio)
501
- plot_interactive_sequence('SE', step_ir)
502
-
503
- ir_descriptions = {
504
- 1: "1. สภาวะสมดุล: เวกเตอร์แม่เหล็กสุทธิจัดเรียงตัวตามแกน Z",
505
- 2: "2. 180° Inversion: พลิกเวกเตอร์ 180 องศาลงไปติดลบ (Mz คว่ำหัวลง)",
506
- 3: "3. TI Delay: รอเวลาให้เนื้อเยื่อค่อยๆ ฟื้นตัวกลับมาทางบวก จุดนี้จะกะให้เนื้อเยื่อบางชนิดอยู่ตรง 0 พอดี (Null point)",
507
- 4: "4. 90° RF: ยิงคลื่น 90° เพื่อดึงเวกเตอร์ที่เหลือลงมาที่ระนาบ Mxy (เนื้อเยื่อที่อยู่ตรง 0 จะไม่เกิดสัญญาณในภาพ)",
508
- 5: "5. 180° Refocus: เข้าสู่กระบวนการ Spin echo ปกติ เพื่อดึงสปินกลับมารวมกัน",
509
- 6: "6. Echo: สปินรวมเฟสกัน เกิดเป็นสัญญาณภาพที่ถูกกด (Suppress) เนื้อเยื่อที่ไม่ต้องการไปแล้ว"
510
- }
511
- st.info(f"**คำอธิบาย:** {ir_descriptions[step_ir]}")
512
-
513
- st.markdown('### 🎛️ Simulator โหมดจำลองการกดสัญญาณ (Interactive TI)')
514
-
515
- ti_slider = st.slider("เลื่อนเปลี่ยนค่า Inversion Time (TI) [ms]", 100, 4000, 2500, step=50)
516
-
517
- if 100 <= ti_slider <= 200:
518
- msg = "💡 **โหมด STIR (Short Tau Inversion Recovery):** กดสัญญาณไขมัน (Fat suppression)"
519
- color = "#ffcdd2"
520
- elif 2000 <= ti_slider <= 2600:
521
- msg = "💧 **โหมด FLAIR (Fluid Attenuated Inversion Recovery):** กดสัญญาณน้ำไขสันหลัง (CSF suppression)"
522
- color = "#e3f2fd"
523
- else:
524
- msg = "⏳ **เนื้อเยื่อกำลังฟื้นตัว:** ยังไม่ถึงจุด Null point ของเนื้อเยื่อหลัก"
525
- color = "#fff9c4"
526
-
527
- st.markdown(f'<div style="background-color:{color}; padding:10px; border-radius:5px; height:45px;">{msg}</div><br>', unsafe_allow_html=True)
528
-
529
- # ---------------------------------------------------------
530
- # TAB 4: Gradient Echo
531
- # ---------------------------------------------------------
532
- with t_gre:
533
- st.header("4. ลำดับพัลส์เกรเดียนท์เอคโค (Gradient Echo)")
534
- st.write("ทำการกระตุ้นสปินด้วยมุม α° (มุมพลิกมักน้อยกว่า 90°) เมื่อเวลาผ่านไปจะเร่งให้สปินแตกเฟส โดยใช้สนามแม่เหล็กเกรเดียนท์ Gfe ฝั่งลบ และตามด้วย Gfe ฝั่งบวก เมื่อพื้นที่รวมกันเป็นศูนย์จะเกิดการรวมเฟส ทำให้เกิดสัญญาณสูงสุด (ต่างจาก spin echo ที่ใช้ 180° RF)")
535
-
536
- gre_step_radio = st.radio("เลือกสเต็ป (GRE):", ["1", "2", "3", "4", "5"], horizontal=True, label_visibility="collapsed")
537
- step_gre = int(gre_step_radio)
538
- plot_interactive_sequence('GRE', step_gre)
539
-
540
- gre_descriptions = {
541
- 1: "1. สภาวะสมดุล: เวกเตอร์แม่เหล็กสุทธิจัดเรียงตัวตามแกน B0 (แกน Z)",
542
- 2: "2. α° RF (Alpha Pulse): ยิงคลื่น RF ด้วยมุมพลิก (Flip Angle - α°) ที่น้อยกว่า 90 องศา ข้อดีคือทำให้ Mz ฟื้นตัวกลับมาได้เร็วขึ้น จึงใช้ TR ที่สั้นมากๆ ได้",
543
- 3: "3. Gfe Dephase: เปิด Gradient แนวความถี่ (Gfe) ฝั่งลบ เพื่อบังคับให้สปินแตกเฟส (กระจายตัว) อย่างรวดเร็ว",
544
- 4: "4. Gfe Rephase: สลับ Gradient (Gfe) มาฝั่งบวก (Gradient Reversal) สปินที่แตกเฟสออกไปจะค่อยๆ วิ่งกลับมารวมกัน",
545
- 5: "5. Echo: สปินกลับมารวมเฟสกันตรงกลาง K-space พอดี ทำให้เกิดสัญญาณ Gradient Echo สูงสุดอย่างรวดเร็ว โดยไม่ต้องพึ่งคลื่น 180°"
546
- }
547
- st.info(f"**คำอธิบาย:** {gre_descriptions[step_gre]}")
548
-
549
- st.write("**ปรากฏการณ์ SSFP (Steady-State Free Precession):**")
550
- st.write("ถ้ายิงคลื่น RF ถี่มากๆ จน TR สั้นกว่าเวลา T2 ของเนื้อเยื่อ หางของสัญญาณ FID และ Echo จะขยายมาชนและรวมกัน (Merge) กลายเป็นสัญญาณที่ต่อเนื่องกันไม่ขาดสาย (Continuous signal)")
 
1
  import streamlit as st
2
  import numpy as np
3
+ import scipy.io
4
  import matplotlib.pyplot as plt
 
 
5
 
6
+ # ตั้งค่าหน้าเว็บให้กว้าง
7
+ st.set_page_config(layout="wide", page_title="K-Space to MRI Image")
8
+
9
+ # โหลดข้อมูล K-space จากไฟล์ mat
10
+ @st.cache_data
11
+ def load_kspace_data():
12
+ try:
13
+ # สมมติว่าไฟล์ชื่อ kspace.mat และมีตัวแปรชื่อ kspace ด้านใน
14
+ mat = scipy.io.loadmat('kspace.mat')
15
+ # หากชื่อตัวแปรใน mat ไม่ใช่ 'kspace' สามารถเปลี่ยนให้ตรงกับที่มีได้
16
+ key = [k for k in mat.keys() if not k.startswith('__')][0]
17
+ return mat[key]
18
+ except Exception as e:
19
+ st.error(f"ไม่พบไฟล์ kspace.mat หรือไฟล์มีปัญหา: {e}")
20
+ # ข้อมูลจำลองกรณีไม่เจอไฟล์
21
+ img = np.zeros((256, 256))
22
+ img[100:156, 100:156] = 1
23
+ return np.fft.fftshift(np.fft.fft2(img))
24
+
25
+ kspace = load_kspace_data()
26
+ ny, nx = kspace.shape
27
+
28
+ # ฟังก์ชันแสดงความสว่างของ K-space
29
+ def get_magnitude(k):
30
+ return np.log(1 + np.abs(k))
31
+
32
+ # ฟังก์ชันแปลงกลับเป็นภาพ MRI (Inverse Fourier Transform)
33
+ def reconstruct_mri(k):
34
+ return np.abs(np.fft.ifft2(np.fft.ifftshift(k)))
35
 
36
  # ==========================================
37
+ # ส่นเนื้อหของเว็
38
  # ==========================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
+ st.title("K-space to MRI image")
41
+
42
+ st.header("K-space คือ")
43
+ st.write("""
44
+ เมื่อเรานำผู้ป่วยเข้าเครื่อง MRI และส่งคลื่น RF เข้าไปกระตุ้น เกิดเป็นสัญญาณ MR Signal ที่ได้มานั้นจะยังไม่ได้ออกมาเป็นภาพอวัยวะ แต่จะถูกนำไปเก็บรวบรวมไว้ในพื้นที่ที่เรียกว่า "K-space" ซึ่งป็นพื้นที่ที่จะเก็บข้อมูดิบแบบสองมิติ ก่อนจะนำไปผ่านกระบวนการทางคฺณิตศาสตร์ที่เรียกว่า Fourier Transform ให้ได้มาซึ่งภาพ MRI ซึ่งข้อมูลใน K-Sapce ถูกเก็บในรูปแบบ ความถี่เชิงพ้นที่ (Spatial Frequency) พิกัดในตารางขง K-space ถูสร้างขึ้นจากการทำงานของสนามแม่เล็กเกรเดียท์ 2 แกน ไดแก่ Gx (ทำหน้ที่ข้ารหัสใแนวความถี่ Frequency encoding) และ Gy (ทำหนาที่เข้ารหัสในแนวเฟส Phase encoding) ซึ่งเกรเดียนท์ทั้งสงตัวนี้กำนดว่สัญญาณจากโปรตอนที่มีความถี่และเฟสจำเพาะเจาะจงที่ต่างกันนั้น จะต้องถูกนำไปจัดเก็บไว้ตรงจุดไหนในพิกัดของ K-space
45
+ """)
46
+
47
+ st.header("องค์ประกอบของ K-Space")
48
+ st.write("""
49
+ ข้อมูลใน k-space มักจะถูกนำมาแสดงผลในรูปแบบตารางสี่เหลี่ยม (Grid) โดยมีแกนหลักคือ kx (แนวนอน - Frequency) และ ky (แนวตั้ง - Phase) แต่จุดสำคัญคือ แกน kx และ ky เหล่านี้ ไม่ได้บอกตำแหน่งพิกัด ในภาพ แต่มันคือแกนที่บอกถึงลักษณะของ "ความถี่เชิงพื้นที่ (Spatial Frequencies)" ซึ่งเป็นคลื่นความถี่ Sinusoidal wave ด้วยเหตุนี้ จุดแต่ละจุดบนพิกัด (kx, ky) ใน k-space จึง ไม่ได้จับคู่แบบ 1 ต่อ 1 กับพิกเซล (x, y) บนภาพ MRI (ไม่ได้แปลว่าจุดมุมซ้ายบนใน k-space จะสร้างภาพมุมซ้ายบนของภาพอวัยวะ)
50
+ """)
51
+
52
+ st.subheader("1 จุดบน k-space")
53
+
54
+ # --- Interactive 1 ---
55
+ st.markdown("<p style='color:#ff4b4b; font-weight:bold;'>ไอเดีย Interactive หัวข้อนี้ ให้ผู้เรียนกดแต่ละจุดของ K-space ละให้แสดงภาพคลื่นข้อมูลของจุดนั้นด้านซ้ายมือ และมีเส้นลากทิศทางของตำแหน่งของจุดเมื่อเทียบกับจุดศูนย์กลาง จะได้ให้เข้าใจว่ามันเอียงไปทางเดียวกับคลื่น พอผู้เรียนอยากเปลี่ยนก็กด reset ได้</p>", unsafe_allow_html=True)
56
+
57
+ if 'kx_point' not in st.session_state:
58
+ st.session_state['kx_point'] = 0
59
+ if 'ky_point' not in st.session_state:
60
+ st.session_state['ky_point'] = 0
61
+
62
+ if st.button("Reset จุด"):
63
+ st.session_state['kx_point'] = 0
64
+ st.session_state['ky_point'] = 0
65
+
66
+ # ใช้ Slider แทกดลบนภาพโดยตรเพื่อห้ใช้งาได้ใน Streamlit ทันที
67
+ col_slider1, col_slider2 = st.columns(2)
68
+ with col_slider1:
69
+ kx_val = st.slider("พิกัด kx (แนวนอน)", -nx//2, nx//2-1, st.session_state['kx_point'], key="kx_slider")
70
+ with col_slider2:
71
+ ky_val = st.slider("พิกัด ky (แนวตั้ง)", -ny//2, ny//2-1, st.session_state['ky_point'], key="ky_slider")
72
+
73
+ st.session_state['kx_point'] = kx_val
74
+ st.session_state['ky_point'] = ky_val
75
+
76
+ col_plot1, col_plot2 = st.columns(2)
77
+ with col_plot1:
78
+ fig1, ax1 = plt.subplots(figsize=(5, 5))
79
+ ax1.imshow(get_magnitude(kspace), cmap='gray', extent=[-nx//2, nx//2, -ny//2, ny//2])
80
+ ax1.plot(kx_val, ky_val, 'ro', markersize=8) # จุดที่เลือก
81
+ ax1.plot([0, kx_val], [0, ky_val], 'r--', linewidth=2) # เส้นลากจากจุดศูนย์กลาง
82
+ ax1.set_title("K-Space")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  st.pyplot(fig1)
84
+
85
+ with col_plot2:
86
+ fig2, ax2 = plt.subplots(figsize=(5, 5))
87
+ Y_grid, X_grid = np.mgrid[-ny//2:ny//2, -nx//2:nx//2]
88
+ # คลื่ความถี่ 2D
89
+ wave = np.cos(2 * np.pi * (kx_val * X_grid / nx + ky_val * Y_grid / ny))
90
+ ax2.imshow(wave, cmap='gray')
91
+ ax2.set_title("2D Sinusoidal Wave")
92
+ ax2.axis('off')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  st.pyplot(fig2)
94
+
95
+ st.write("""
96
+ k-space 1 จุด = ข้อมูลของภาพทั้งภาพ และ ภาพ 1 พิกเซล = ผลรวมของ k-space ทุกจุด
97
+ 1 จุดใน k-space = แผ่นลวดลายคลื่น 1 แผ่น (2D Sinusoidal Wave)
98
+ 1. ตำแหน่งของจุด (พิกัด kx, ky) บอก "ความถี่" และ "ทิศทาง"
99
+ ะยะห่างกศูนย์กลาง (ควาถี่): ยิ่งุด้อยู่ไกลจากจุดศูนย์กลาk-space มากเท่าไหร่ ผ่นวดลายคลื่นก็จยิ่ง "ถี่" หรือมีเส้นทีแคบมากขึ้นเท่าน (High frequency)
100
+ มุมของจุด (ทิศทาง): ตำแห่งของจุดเมื่อเทียกับจุดศูนย์กลาง จะเป็นตัวบอกว่าผ่ดลยคลืนนี้จะ "เอียง" ไปนทิศทงไหน (ตั้ง อน หรือเฉียงกี่องศา)
101
+ 2. ามว่างของจุด (Amplitude / Magnitude) บอ "น้ำหนั"
102
+ ความสว่างของจุดใน k-space ไม่ได้แปลว่าภาพ MRI ตรงนั้นจะสว่าง แต่มันคือการบอก "ปริมาณ (Weight)"
103
+ จุดสว่างมาก: แปลว่าภาพ MRI ภาพนี้ มีแผ่นลวดลายชนิดนี้เป็นส่วนประกอบอยู่ เยอะมาก (มีความสำคัญต่อภาพสูง)
104
+ จุดมืดหรือจาง: แปลว่าภาพ MRI ภาพนี้ แทบจะไม่มีลวดลายชนิดนี้ประกอบอยู่เลย
105
+ """)
106
+
107
+ st.header("Inverse Fourier Transform")
108
+ st.write("""
109
+ เมื่อเก็บข้อมูลจนเต็มพื้นที่ k-space เราจะใช้กระบวนการทางคณิตศาสตร์ 2D Inverse Fourier Transform (2D-iFT) ในการเปลี่ยนข้อมูลความถี่กลับไปเป็นข้อมูลในเชิงพื้นที่ (Spatial Domain) ภาพ MRI เกิดจากการนำ "คลื่นความถี่ (Sinusoidal spatial waves)" จากทุกจุดใน k-space มาซ้อนทับกัน คลื่นที่มีเฟสตรงกันจะรวมตัวกันแบบเสริมฤทธิ์ (Constructive interference) สร้างเป็นพิกัดที่สว่าง และคลื่นที่��ีเฟสตรงข้ามจะหักล้างกัน (Destructive interference) กลายเป็นพื้นที่สีดำ
110
+ โดยต้องอาศัยข้อมูลจากหลายจุดมาซ้อนทับกัน และเกิดการแทรกสอดตามคุณสมบัติของคลื่น
111
+ บริเวณไหนที่เป็นเนื้อเยื่อจริง คลื่นจะเสริมกันทำให้เกิด จุดสว่าง
112
+ บริเวณไหนที่เป็นช่องว่าง คลื่นจะหักล้างกันทำให้เกิด จุดมืด
113
+ """)
114
+
115
+ st.header("ความถี่เชิงพื้นที่ (Spatial Frequency) คือ")
116
+ st.write("""
117
+ ในทางสัญญาณภาพ (Spatial Frequency) ความถี่ไม่ได้หมายถึงความเร็วของเวลา แต่หมายถึง "อัตราการเปลี่ยนแปลงความเข้มของแสงในพื้นที่หนึ่งๆ"
118
+ 1. ความถี่เชิงพื้นที่ต่ำ (Low Spatial Frequency)
119
+ คืออะไร: พื้นที่ที่สีหรือความสว่าง "ค่อยๆ เปลี่ยน" หรือ "เหมือนเดิมเป็นบริเวณกว้าง" (เหมือนคลื่นลูกใหญ่ๆ ที่ขยับช้าๆ)
120
+ ตัวอย่างในภาพ MRI: บริเวณเนื้อเยื่อก้อนใหญ่ๆ เช่น เนื้อตับ หรือเนื้อสมอง ที่มีสีเทาโทนเดียวกันกินพื้นที่กว้าง
121
+ ตำแหน่งใน k-space: ข้อมูลเหล่านี้จะรวมตัวกันอยู่บริเวณ "ตรงกลาง"
122
+ หน้าที่หลัก: สร้าง "รูปร่างรวมๆ และคอนทราสต์ (Contrast)" ให้เรารู้ว่านี่คือก้อนอวัยวะอะไร
123
+
124
+ 2. ความถี่เชิงพื้นที่สูง (High Spatial Frequency)
125
+ คืออะไร: พื้นที่ที่ความสว่างเปลี่ยนแบบฉับพลันและรวดเร็วภายในระยะทางสั้นๆ เช่น จากขาวตัดเป็นดำสนิททันที (เหมือนลายทางแคบๆ ที่สลับสีถี่ๆ)
126
+ ตัวอย่างในภาพ MRI: ขอบของอวัยวะ (Edges), รอยต่อระหว่างกระดูกกับไขสันหลัง, หรือรายละเอียดเส้นเลือดเส้นเล็กๆ
127
+ ตำแหน่งใน k-space: ข้อมูลเหล่านี้จะกระจายตัวอยู่บริเวณ "ขอบนอก"
128
+ หน้าที่หลัก: สร้าง "ความคมชัด (Resolution) และรายละเอียดเล็กๆ" ทำให้ภาพไม่เบลอ
129
+
130
+ ซึ่งจะให้ผู้เรียนได้ลองปรับหน้าตาของภาพ K-Space แล้วเปรียบเทียบความแตกต่างด้วยการปรับ High-pass filter และ Low-pass filter เพื่อดูลักษณะและความสำคัญของข้อมูลบริเวณกลางและขอบนอกของ K-space
131
+ เมื่อเราจำแนกข้อมูลใน k-space ออกเป็นความถี่ต่ำ (ตรงกลาง) และความถี่สูง (ขอบนอก) ได้แล้ว เราสามารถเลือก "หยิบ" หรือ "ทิ้ง" ข้อมูลบางส่วนเพื่อดูผลลัพธ์ได้ เรียกว่าการใช้ตัวกรอง (Filter)
132
+ """)
133
+
134
+ # --- Toggles สำหรับเนื้อหาของ Filter ตามไฟล์ต้นฉบับ ---
135
+ with st.expander("Low-pass Filter (ตัวกรองปล่อยควาถี่่ำผ่าน):", expanded=False):
136
+ st.markdown("<p style='color:#ff4b4b;'>อันนซ่อนใน Toggle</p>", unsafe_allow_html=True)
137
+ st.write('ทำงานอย่างไร: "อนุญาตให้เฉพาะข้อมูลตรงกลาง (ความถี่ต่ำ) ผ่านไปสร้างภาพได้ ส่วนข้อมูลขอบนอก (ความถี่สู��) ให้ทิ้งไป"')
138
+ st.write('ผลลัพธ์ที่ได้: เราจะได้ภาพที่มี "คอทราสต์" ดูออกว่าป็นอวัยวะอะไ แตภาพจะ "เบลอ" (Blurry) เพราะข้อมูลเส้นขอบถูทิ้ไปแล้ว อันีซ่อใน Toggle')
139
+
140
+ with st.expander("High-pass Filter (ตัวกรองปล่อยความถี่สูงผ่าน):", expanded=False):
141
+ st.markdown("<p style='color:#ff4b4b;'>อันนี้ซ่อนใน Toggle</p>", unsafe_allow_html=True)
142
+ st.write("งาอยไร: อนุญาตให้พาะข้อมูลขอบอก (ควาถี่สูง) ผ่าไปไ้ สวนข้อมูลตรงกลางทิ้ง")
143
+ st.write('ผลลัพธ์ที่ได้: ภาพจะสูญเสียคอนทราสต์ไปจนเกือบมืดสนิท แต่จะปรากฏ "เส้นขอบร่าง" (Outline) ของอวัยวะขึ้นมาอย่างคมชัด อันนี้ซ่อนใน Toggle')
144
+
145
+ # --- Interactive 2 ---
146
+ st.markdown("<p style='color:#ff4b4b; font-weight:bold;'>ไอเดีย Interactive หัวข้อนี้ อยากได้เป็น Slide bar อะ แบบมีให้กด สองตุ่ม low กับ high ละให้เลื่อนได้ เช่น แบบ low pass ถ้า max เต็มหลอดคือผ่านหมด ละค่อยลดลงมาตามลำดับ ทำให้ภาพตรงกลางจะเล็กเมื่อเทียบกับตอนเต็มหลอด แต่ปรับได้เอง ส่วนแบบสูงก็ตรงข้ามกันจาก 0 คือข้อมูลเต็ม แต่พอยิ่งเลื่อนหลอดเพิ่ม ข้อมูลตรงกลางหาย เหลือแต่ขอบ จนเต็ม Max คือไม่มีเลย ทั้งสองอันนี้จะมีภาพ MRi ที่แปลงแล้วแสดงข้าง ๆ ด้วย ละมีปุ่มให้ reset ได้</p>", unsafe_allow_html=True)
147
+
148
+ if 'lp_radius' not in st.session_state:
149
+ st.session_state['lp_radius'] = nx // 2
150
+ if 'hp_radius' not in st.session_state:
151
+ st.session_state['hp_radius'] = 0
152
+
153
+ if st.button("Reset Filter"):
154
+ st.session_state['lp_radius'] = nx // 2
155
+ st.session_state['hp_radius'] = 0
156
+
157
+ filter_mode = st.radio("เลือกชนิด Filter:", ("Low-pass", "High-pass"))
158
+
159
+ Y_dist, X_dist = np.ogrid[-ny//2:ny//2, -nx//2:nx//2]
160
+ dist_from_center = np.sqrt(X_dist**2 + Y_dist**2)
161
+
162
+ if filter_mode == "Low-pass":
163
+ r = st.slider("Low-pass Radius (Max เต็มหลอดคือผ่านหมด)", 0, nx//2, st.session_state['lp_radius'], key='lp_slider')
164
+ st.session_state['lp_radius'] = r
165
+ mask = dist_from_center <= r
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  else:
167
+ r = st.slider("High-pass Radius (ยิงเลื่อนหลอดเพิ่ม ข้อมูลตรงกลางหาย)", 0, nx//2, st.session_state['hp_radius'], key='hp_slider')
168
+ st.session_state['hp_radius'] = r
169
+ mask = dist_from_center >= r
170
+
171
+ filtered_kspace = kspace * mask
172
+ reconstructed_img = reconstruct_mri(filtered_kspace)
173
+
174
+ col_f1, col_f2 = st.columns(2)
175
+ with col_f1:
176
+ fig_k, ax_k = plt.subplots(figsize=(6, 6))
177
+ ax_k.imshow(get_magnitude(filtered_kspace), cmap='gray')
178
+ ax_k.set_title("Filtered K-Space")
179
+ ax_k.axis('off')
180
+ st.pyplot(fig_k)
181
+
182
+ with col_f2:
183
+ fig_img, ax_img = plt.subplots(figsize=(6, 6))
184
+ ax_img.imshow(reconstructed_img, cmap='gray')
185
+ ax_img.set_title("Reconstructed MRI Image")
186
+ ax_img.axis('off')
187
+ st.pyplot(fig_img)