Nicha1234 commited on
Commit
b0b1f91
·
verified ·
1 Parent(s): 20057d6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +230 -1
app.py CHANGED
@@ -81,4 +81,233 @@ ax.set_title('Slice Selection Simulation')
81
  ax.legend()
82
  ax.grid(True)
83
 
84
- st.pyplot(fig)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  ax.legend()
82
  ax.grid(True)
83
 
84
+ st.pyplot(fig)
85
+
86
+ import streamlit as st
87
+ import numpy as np
88
+ import scipy.io
89
+ import gdown
90
+ import matplotlib.pyplot as plt
91
+ import os
92
+
93
+ # ตั้งค่าหน้าเว็บ (ต้องอยู่บนสุดเสมอ)
94
+ st.set_page_config(page_title="MRI Physics & Simulator", layout="wide")
95
+
96
+ # ==========================================
97
+ # ส่วนฟังก์ชันโหลดข้อมูลของอาจารย์
98
+ # ==========================================
99
+ @st.cache_resource
100
+ def load_vobj():
101
+ output = 'BrainHighResolution.mat'
102
+ if not os.path.exists(output):
103
+ file_id = '1vdV5xDNfAiHvmKuH5DPi1YJ5Ptl927nk'
104
+ gdown.download(f'https://drive.google.com/uc?id={file_id}', output, quiet=True)
105
+ try:
106
+ data = scipy.io.loadmat(output)
107
+ v = data['VObj'][0,0]
108
+ mid = v['T2'].shape[2] // 2
109
+ return {
110
+ 'rho': v['Rho'][:,:,mid],
111
+ 't1': v['T1'][:,:,mid] * 1000, # s -> ms
112
+ 't2': v['T2'][:,:,mid] * 1000, # s -> ms
113
+ }
114
+ except:
115
+ return None
116
+
117
+ # ==========================================
118
+ # เริ่มต้นหน้าเว็บ (แบ่ง Tabs)
119
+ # ==========================================
120
+ st.title("🧠 MRI Physics & Image Formation")
121
+ st.markdown("---")
122
+
123
+ tab1, tab2, tab3 = st.tabs([
124
+ "📍 3. Image Formation & K-space",
125
+ "🌊 4. Basic Pulse Sequences",
126
+ "🕹️ Simulator ภาพสมอง (TR/TE/TI)"
127
+ ])
128
+
129
+ # ==========================================
130
+ # TAB 1: Image Formation & K-space
131
+ # ==========================================
132
+ with tab1:
133
+ st.header("**3. การเข้ารหัสในแนวความถี่และแนวเฟส (Frequency and Phase encoding)**")
134
+ st.write("หลังจากที่เลือกตำแหน่งสไลซ์ในแนวแกน Z โดยการใช้สนามแม่เหล็กเกรเดียนท์ Gz เรียบร้อยแล้ว สปินในแนวระนาบ Y และ X ยังคงมีความถี่และมีเฟสเหมือนกัน[cite: 6]")
135
+ st.write("ในการสร้างภาพเอ็มอาร์ไอแบบสองมิติ จำเป็นต้องใช้สนามแม่เหล็กเกรเดียนท์ Gx และ Gy และให้ความถี่และเฟสของสปินมีค่าแตกต่างกันในตำแหน่งต่างๆ[cite: 6]")
136
+
137
+ st.markdown('<p style="color:red;"><b>Interactive Part:</b></p>', unsafe_allow_html=True)
138
+ st.markdown('<p style="color:red;">- ระนาบ X เป็นแนวความถี่ Frequency</p>', unsafe_allow_html=True)
139
+ st.markdown('<p style="color:red;">- ระนาบ Y เป็นแนวเฟส Phase (เกรเดียนท์ Gy จะเปลี่ยนขนาดแอมปลิจูดหลายค่า เริ่มจากลบไปบวก หรือบวกลงลบ)</p>', unsafe_allow_html=True)
140
+
141
+ st.markdown("---")
142
+
143
+ st.header("**4. พิกัด k-space แบบสองมิติ (2 dimension coordinates of k-space)**")
144
+ st.write("การใช้ Gx และ Gy ในการเข้ารหัสสัญญาณ เสมือนเป็นการสร้างพิกัดขึ้นมา เพื่อให้สปินที่มีความถี่และเฟสจำเพาะเข้าไปเติมในพิกัดนั้นๆ ใน k-space โดยมีลำดับดังนี้:[cite: 6]")
145
+ st.write("• **ตำแหน่ง A:** ช่วงเวลาที่ Gx และ Gy เริ่มทำงาน (สัมพันธ์กับจุดกึ่งกลางของ kx และ ky)[cite: 6]")
146
+ st.write("• **ตำแหน่ง B:** เริ่มบันทึกข้อมูลจุดแรก ของเฟสแถวที่ 1[cite: 6]")
147
+ st.write("• **ตำแหน่ง C:** บันทึกข้อมูลได้ครึ่งหนึ่ง ของเฟสแถวที่ 1[cite: 6]")
148
+ st.write("• **ตำแหน่ง D:** บันทึกข้อมูลได้ครบถ้วน ของเฟสแถวที่ 1[cite: 6]")
149
+ st.write("หลังจากนั้นเริ่มต้นที่ตำแหน่ง A B C D อีกครั้ง แต่เป็นเฟสแถวที่ 2, 3, 4 ไปเรื่อยๆ จนถึงเฟสแถวที่ M ถือเป็นการจบกระบวนการเก็บข้อมูลสองมิติ[cite: 6]")
150
+
151
+ st.markdown('<p style="color:red;"><b>Interactive Part: K-space to MRI Image</b></p>', unsafe_allow_html=True)
152
+ st.markdown('<p style="color:red;">[จำลอง] แบ่งหน้าจอเป็น 2 ฝั่ง ซ้ายคือ "K-space" ขวาคือ "Image" (รูปสมองค่อยๆ ชัดขึ้นเมื่อเติมเส้น K-space)</p>', unsafe_allow_html=True)
153
+
154
+ # ==========================================
155
+ # TAB 2: Basic Pulse Sequences
156
+ # ==========================================
157
+ with tab2:
158
+ st.header("**1. Spin Echo**")
159
+
160
+ # a. Conventional Spin Echo
161
+ st.subheader("**a. Spin Echo**")
162
+ st.write("ทำการกระตุ้นสปินด้วย 90° RF pulse ณ ตำแหน่งสไลซ์ที่เลือก[cite: 6]")
163
+ st.write("เมื่อเวลาผ่านไป $TE/2$ จะทำการกลับเฟสของสปินด้วย 180° RF pulse (Refocusing pulse) จะเกิดการรวมเฟสของสปินที่เวลา TE ซึ่งเป็นเวลาที่สัญญาณมีค่าสูงสุด (Echo signal)[cite: 6]")
164
+ st.write("เวลาระหว่าง 90° RF pulse สองอัน เรียกว่า TR[cite: 6]")
165
+
166
+ # b. Fast Spin Echo
167
+ st.subheader("**b. Fast Spin Echo**")
168
+ st.write("เพิ่มคลื่น 180° (Refocusing pulse) หลายๆ ครั้งภายใน 1 TR ทำให้สามารถเก็บข้อมูล Echo ได้หลายเส้นในครั้งเดียว เรียกว่า Echo Train Length (ETL) ซึ่งช่วยลดเวลาสแกน[cite: 6]")
169
+
170
+ # Interactive FSE
171
+ st.markdown('<div style="background-color:#ffebee; padding:15px; border-radius:10px;">', unsafe_allow_html=True)
172
+ st.markdown('<h4 style="color:#c62828; margin-top:0;">Interactive Scan Time Calculator</h4>', unsafe_allow_html=True)
173
+ st.write("ทดลองคำนวณเวลาการสแกนแบบ Fast Spin Echo")
174
+ col1, col2, col3 = st.columns(3)
175
+ with col1:
176
+ tr_val = st.number_input("TR (ms)", value=500, step=100)
177
+ with col2:
178
+ np_val = st.number_input("N-phase (จำนวน Phase)", value=256, step=64)
179
+ with col3:
180
+ etl_val = st.select_slider("Echo Train Length (ETL)", options=[1, 4, 8, 16], value=4)
181
+
182
+ scan_time_base = (tr_val * np_val) / 1000 # แปลงเป็นวินาที
183
+ scan_time_fse = scan_time_base / etl_val
184
+ st.markdown(f'<p style="color:#c62828;"><b>เวลาสแกนลดลง {etl_val} เท่า!</b> (จาก {scan_time_base:.1f} วินาที ➡️ เหลือเพียง <b>{scan_time_fse:.1f} วินาที</b>)</p>', unsafe_allow_html=True)
185
+ st.markdown('</div><br>', unsafe_allow_html=True)
186
+
187
+ # d. Inversion Recovery
188
+ st.subheader("**d. Inversion Recovery (IR)**")
189
+ st.write("ลำดับพัลส์พิเศษที่เริ่มต้นด้วยการยิงคลื่น 180° เพื่อกลับทิศทางของแกนแม่เหล็กให้ติดลบ จากนั้นเครื่องจะรอเป็นเวลาที่เรียกว่า TI (Inversion Time) ก่อนยิงคลื่น 90° เพื่อกดสัญญาณเนื้อเยื่อบางชนิด[cite: 6]")
190
+
191
+ # Interactive IR (STIR vs FLAIR)
192
+ st.markdown('<div style="background-color:#e8eaf6; padding:15px; border-radius:10px;">', unsafe_allow_html=True)
193
+ st.markdown('<h4 style="color:#283593; margin-top:0;">Interactive TI Slider (โหมดจำลองการกดสัญญาณ)</h4>', unsafe_allow_html=True)
194
+ ti_sim = st.slider("เลื่อนปรับค่า Inversion Time (TI) [ms]", 100, 3000, 150, step=50)
195
+
196
+ if 100 <= ti_sim <= 200:
197
+ st.success("**โหมด STIR (Short Tau Inversion Recovery):** เป็นเทคนิคการกดสัญญาณไขมัน (Fat suppression) ทำให้ไขมันในภาพกลายเป็นสีดำ[cite: 6]")
198
+ elif 2000 <= ti_sim <= 2600:
199
+ st.warning("**โหมด FLAIR (Fluid Attenuated Inversion Recovery):** เป็นเทคนิคการกดสัญญาณน้ำไขสันหลัง (CSF) ทำให้ภาพสมองเห็นรอยโรคชัดเจนขึ้นโดยไม่มีสีขาวของน้ำมาบัง[cite: 6]")
200
+ else:
201
+ st.info("กำลังอยู่ในช่วงเวลาพัก (TI ทั่วไป) - เนื้อเยื่อกำลังทยอยฟื้นตัวกลับสู่แกนตั้ง")
202
+ st.markdown('</div><br>', unsafe_allow_html=True)
203
+
204
+ st.markdown("---")
205
+
206
+ st.header("**2. Gradient Echo**")
207
+ st.subheader("**a. Conventional Gradient Echo**")
208
+ st.write("กระตุ้นสปินด้วย 90° (หรือน้อยกว่า) จากนั้นเร่งให้สปินแตกเฟสโดยใช้ GFE ค่าลบ และตามด้วย GFE ค่าบวก เมื่อพื้นที่ลบและบวกรวมกันเป็นศูนย์จะเกิดการรวมเฟส (Echo signal) โดยไม่ต้องใช้คลื่น 180°[cite: 6]")
209
+
210
+ st.subheader("**b. Fast Gradient Echo & SSFP**")
211
+ st.write("เมื่อ TR สั้นมากจนสั้นกว่า T2 ($TR \ll T2$) หางของสัญญาณ FID และ Echo จะรวมกัน (Merge) กลายเป็นสัญญาณที่ต่อเนื่องกัน เรียกว่าสภาวะ Steady-State Free Precession (SSFP)[cite: 6]")
212
+
213
+
214
+ # ==========================================
215
+ # TAB 3: MRI Simulator (Professor's Code)
216
+ # ==========================================
217
+ with tab3:
218
+ st.markdown('<h2 style="font-size:30px; text-align:center;">🧠 โปรแกรมจำลองภาพ MRI (T1, T2, FLAIR)</h2>', unsafe_allow_html=True)
219
+
220
+ v = load_vobj()
221
+
222
+ if v is not None:
223
+ rho = np.nan_to_num(v['rho'])
224
+ t1 = np.nan_to_num(v['t1'])
225
+ t2 = np.nan_to_num(v['t2'])
226
+
227
+ T1_CSF = 2569.0
228
+
229
+ t1_safe = np.where(t1 <= 0, 1e10, t1)
230
+ t2_safe = np.where(t2 <= 0, 1e-10, t2)
231
+
232
+ # จำลอง Sidebar มาไว้ในคอลัมน์ซ้ายเพื่อให้เข้ากับ Tab UI
233
+ col_ctrl, col_img = st.columns([1, 2])
234
+
235
+ with col_ctrl:
236
+ st.markdown("### 🕹️ Parameters Settings")
237
+ sequence = st.selectbox("เลือก Sequence", ["T1-Weighted", "T2-Weighted", "FLAIR"])
238
+
239
+ tr_ms = st.slider("TR (ms)", 100, 10000, 600 if sequence == "T1-Weighted" else 9000, step=100)
240
+ te_ms = st.slider("TE (ms)", 10, 200, 15 if sequence == "T1-Weighted" else 80, step=5)
241
+
242
+ ti_ms = None
243
+ if sequence == "FLAIR":
244
+ ti_null = int(T1_CSF * np.log(2.0 / (1.0 + np.exp(-tr_ms / T1_CSF))))
245
+ ti_ms = st.slider(f"TI (ms) [CSF null = {ti_null} ms]", 100, 4000, ti_null, step=25)
246
+
247
+ with col_img:
248
+ # คำนวณสัญญาณ
249
+ if sequence == "T1-Weighted":
250
+ signal = rho * (1 - np.exp(-tr_ms / t1_safe)) * np.exp(-te_ms / t2_safe)
251
+ caption = f"T1-Weighted | TR = {tr_ms} ms, TE = {te_ms} ms"
252
+ elif sequence == "T2-Weighted":
253
+ signal = rho * (1 - np.exp(-tr_ms / t1_safe)) * np.exp(-te_ms / t2_safe)
254
+ caption = f"T2-Weighted | TR = {tr_ms} ms, TE = {te_ms} ms"
255
+ else:
256
+ raw = rho * (1 - 2*np.exp(-ti_ms / t1_safe) + np.exp(-tr_ms / t1_safe)) * np.exp(-te_ms / t2_safe)
257
+ signal = np.clip(raw, 0, None)
258
+ caption = f"FLAIR | TR = {tr_ms} ms, TE = {te_ms} ms, TI = {ti_ms} ms"
259
+
260
+ st.markdown(f'<p style="font-size:18px; font-weight:bold; text-align:center;">{caption}</p>', unsafe_allow_html=True)
261
+
262
+ fig, ax = plt.subplots(figsize=(6, 6))
263
+ ax.imshow(signal, cmap='gray')
264
+ ax.axis('off')
265
+ fig.tight_layout()
266
+ st.pyplot(fig)
267
+ plt.close(fig)
268
+
269
+ st.divider()
270
+ st.markdown('<h2 style="font-size:22px;">📖 คำอธิบายทางทฤษฎี & กราฟ</h2>', unsafe_allow_html=True)
271
+
272
+ TISSUES = {
273
+ 'CSF': {'T1': 2569, 'T2': 329, 'color': '#2196F3'},
274
+ 'Gray Matter': {'T1': 833, 'T2': 83, 'color': '#9C27B0'},
275
+ 'White Matter': {'T1': 500, 'T2': 70, 'color': '#FF9800'},
276
+ }
277
+
278
+ col_g1, col_g2 = st.columns(2)
279
+
280
+ with col_g1:
281
+ st.markdown('<b style="font-size:16px;">T1 Recovery Curve</b>', unsafe_allow_html=True)
282
+ t_rec = np.linspace(0, 10000, 1000)
283
+ fig1, ax1 = plt.subplots(figsize=(5, 3))
284
+ for name, p in TISSUES.items():
285
+ ax1.plot(t_rec, 1 - np.exp(-t_rec / p['T1']), color=p['color'], lw=2, label=f"{name}")
286
+ ax1.axvline(tr_ms, color='red', lw=1.5, ls='--', label=f'TR = {tr_ms}')
287
+ ax1.set_xlabel('TR (ms)')
288
+ ax1.set_ylabel('Mz / M0')
289
+ ax1.legend(fontsize=8)
290
+ ax1.grid(alpha=0.3)
291
+ ax1.set_ylim(0, 1.05)
292
+ ax1.set_xlim(0, 10000)
293
+ st.pyplot(fig1)
294
+ plt.close(fig1)
295
+
296
+ with col_g2:
297
+ st.markdown('<b style="font-size:16px;">T2 Decay Curve</b>', unsafe_allow_html=True)
298
+ t_dec = np.linspace(0, 1200, 1000)
299
+ fig2, ax2 = plt.subplots(figsize=(5, 3))
300
+ for name, p in TISSUES.items():
301
+ ax2.plot(t_dec, np.exp(-t_dec / p['T2']), color=p['color'], lw=2, label=f"{name}")
302
+ ax2.axvline(te_ms, color='red', lw=1.5, ls='--', label=f'TE = {te_ms}')
303
+ ax2.set_xlabel('TE (ms)')
304
+ ax2.set_ylabel('Mxy / M0')
305
+ ax2.legend(fontsize=8)
306
+ ax2.grid(alpha=0.3)
307
+ ax2.set_ylim(0, 1.05)
308
+ ax2.set_xlim(0, 1200)
309
+ st.pyplot(fig2)
310
+ plt.close(fig2)
311
+
312
+ else:
313
+ st.error("Error loading VObj data. (ตรวจดูการเชื่อมต่ออินเทอร์เน็ตสำหรับการโหลดไฟล์ครั้งแรกครับ)")