Nicha1234 commited on
Commit
5a1e208
·
verified ·
1 Parent(s): f06eed2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +238 -156
app.py CHANGED
@@ -1,187 +1,269 @@
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")
87
 
88
- st.header("K-space คือ")
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}")
 
 
 
 
 
 
 
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)