generated from streamlit/blank-app-template
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathstreamlit_app.py
316 lines (270 loc) · 13.4 KB
/
streamlit_app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
import json
import streamlit as st
import numpy as np
import math
from collections import Counter
import pandas as pd
import os
import glob
def GEtensor_app_help_page():
st.title("GE Diffusion tensor App (beta)")
st.markdown('''
## View/Convert GE tensor to bval/bvec
by Jaemin Shin, v1.0.20241213
The **GEtensor-app** is a web-based tool created with Python and Streamlit to facilitate the viewing and conversion of GE diffusion tensor files in a user-friendly format. The app provides an intuitive summary of b-values. It also presents b-vectors in a structured table format, and allows users to convert the data to FSL's bval/bvec format for download.
GE's diffusion gradient directions, including FSL bvec format, are rotation invariant. This means that no matter the scanning orientation—whether it's double-oblique, straight axial, head-first, or feet-first—the b-vectors stay the same, as long as the frequency encoding direction doesn’t change.
You may find this app particularly useful if you need:
- An intuitive display of b-values from a tensor file.
- Conversion of GE tensor data to FSL bval/bvec format.
- A case when only a tensor file is available, without access to valid DICOM files.
''')
with st.expander("HOW-TO get the GE tensor file & required scan parameters"):
st.write("GEHC Diffusion Scan Parameter screen:")
st.image("GEHC_UI.png", use_container_width=True)
st.markdown('''
Locations of GE diffusion tensor files on scanner:
**- Prior to MR30.0:**
- All tensor file are saved in `/usr/g/bin`
**- MR30.0 or later:**
- GE-preloaded tensor files are located in `/srv/nfs/psd/etc`
- User-provided tensor files should be placed in `/srv/nfs/psd/usr/etc`
- Precedence is given to the GE directory (`/srv/nfs/psd/etc`) when files with the same name exist in both locations
**JSON sidecar from dcm2niix (v1.0.20220915+):**
```json
{
...
"NumberOfDiffusionDirectionGE": 102,
"NumberOfDiffusionT2GE": 2,
"TensorFileNumberGE": 4321,
"PhaseEncodingDirection": "j",
...
"ConversionSoftware": "dcm2niix",
"ConversionSoftwareVersion": "v1.0.20230315"
}
''')
def read_JSON_info(json_file):
# Load JSON data
json_data = json.load(json_file)
# Default values, which will be overridden if present in JSON
num_dirs = json_data.get("NumberOfDiffusionDirectionGE", None)
num_t2 = json_data.get("NumberOfDiffusionT2GE", None)
phase_encoding_direction = json_data.get("PhaseEncodingDirection", None)
# Set frequency based on PhaseEncodingDirection or leave as None if not provided
if phase_encoding_direction in ["j", "j-"]:
freq = "RL"
elif phase_encoding_direction in ["i", "i-"]:
freq = "AP"
else:
freq = None # Frequency not provided in JSON
return num_dirs, num_t2, freq
def read_tensor_file_initial(file_content):
# Read tensor file content and extract information
lines = file_content.splitlines()
num_dirs_list = []
i = 0
while i < len(lines):
line = lines[i].strip()
# Ignore comments and empty lines
if line and line[0] != '#':
try:
# Check if line has a single integer number
num_dirs = int(line)
num_dirs_list.append(num_dirs)
# Skip the number of lines equal to the integer found, plus this line itself
i += num_dirs + 1
except ValueError:
# Return None or an empty list if format is incorrect
print(f"Invalid format in tensor file at line {i + 1}")
return None
else:
i += 1
return num_dirs_list
def read_directions_from_file(file_content, num_dirs, num_t2):
# num_dirs = num_dirs if num_dirs is not None else 6
# num_t2 = num_t2 if num_t2 is not None else 1
lines = file_content.splitlines()
b_vector = np.zeros((num_dirs + num_t2, 4))
internal_count = 0
header_lines = []
raw_lines = []
for line in lines:
tokens = line.split()
if not tokens:
continue
if tokens[0] == str(num_dirs):
internal_count = 0
raw_lines = []
continue
if line.startswith('#'):
header_lines.append(line)
if len(tokens) > 1 and tokens[0] != "#" and internal_count < num_dirs:
raw_lines.append(line)
internal_count += 1
b_vecx, b_vecy, b_vecz = map(float, tokens[:3])
b_vector[num_t2 + internal_count - 1, 1:] = [b_vecx, b_vecy, b_vecz]
return b_vector, header_lines, raw_lines
def convert_to_b_vector(b_vector, num_dirs, num_t2, b_val, freq):
for idx in range(num_dirs + num_t2):
b_vecx, b_vecy, b_vecz = b_vector[idx, 1:]
b_scale = b_vecx ** 2 + b_vecy ** 2 + b_vecz ** 2
b_val_new = int((b_val * b_scale + 2.5) / 5) * 5
b_vector[idx, 0] = b_val_new
b_vec_scale = math.sqrt(b_val / b_val_new) if b_val_new != 0 else 0
b_vector[idx, 1:] *= b_vec_scale
if freq == 'RL':
b_vector[:, 1] = -b_vector[:, 1]
elif freq == 'AP':
b_vector[:, [1, 2]] = -b_vector[:, [2, 1]]
def display_and_save_b_vector(b_vector, num_dirs, num_t2):
bval_output = [
('%.6f' % b_vector[idx, 0]).replace('-0.000000', '0').replace('0.000000', '0')
for idx in range(num_dirs + num_t2)
]
bvec_output = [
" ".join([
('%.6f' % b_vector[idx, col]).replace('-0.000000', '0').replace('0.000000', '0')
for idx in range(num_dirs + num_t2)
])
for col in range(1, 4)
]
return bval_output, bvec_output
def apply_custom_css():
custom_css = """
<style>
.tight-line-spacing {
line-height: 1.3;
font-size: 16px;
margin-bottom: 2px;
font-family: monospace;
}
.header-info {
font-size: 12px;
font-family: monospace;
}
.raw-lines {
font-size: 12px;
font-family: monospace;
}
.small-font {
line-height: 1.0;
font-size: 13px;
font-family: monospace;
}
</style>
"""
st.markdown(custom_css, unsafe_allow_html=True)
def main():
st.set_page_config(page_title="GEtensor-app")
GEtensor_app_help_page()
apply_custom_css()
# Collect tensor files in the ./tensor directory
tensor_list = sorted([os.path.basename(file) for file in glob.glob('./tensor/tensor*.dat')])
default_index = tensor_list.index('tensor.dat') if 'tensor.dat' in tensor_list else 0
# Layout: left for file selection, right for file upload
col1, col2 = st.columns(2)
with col1:
selected_tensor_file = st.selectbox("Select tensor file", options=tensor_list, index=default_index)
with col2:
uploaded_tensor_file = st.file_uploader("Upload tensor file", type=["txt", "dat"])
# Determine the file to use and extract details
if uploaded_tensor_file is not None:
file_content = uploaded_tensor_file.read().decode("utf-8")
file_name_prefix = uploaded_tensor_file.name.split('.')[0]
file_name = uploaded_tensor_file.name
st.success(f"File uploaded: {file_name}")
elif selected_tensor_file:
file_path = os.path.join('./tensor', selected_tensor_file)
try:
with open(file_path, 'r', encoding='utf-8') as f:
file_content = f.read()
file_name_prefix = os.path.splitext(selected_tensor_file)[0]
file_name = selected_tensor_file
st.success(f"File selected: {file_name}")
except Exception as e:
st.error(f"Error reading the selected file: {e}")
file_content, file_name_prefix, file_name = None, None, None
else:
st.error("Please upload a valid tensor file or select one from the list.")
file_content, file_name_prefix, file_name = None, None, None
uploaded_json = st.file_uploader("(optional) Upload JSON file from dcm2niix to automatically extract the necessary information", type=["json"])
# Proceed only if file content is available
if file_content:
# Read directories list from the file
num_dirs_list = read_tensor_file_initial(file_content)
if not num_dirs_list:
st.error("Failed to extract diffusion directions from the file.")
else:
# Conditionally set the initial value of num_dirs based on num_dirs_list
num_dirs = num_dirs_list[0]
# Layout changes to put Number of Diffusion Directions and Number of T2 in one line
col1, col2 = st.columns(2)
if uploaded_json is not None:
num_dirs_json, num_t2, freq = read_JSON_info(uploaded_json)
if num_dirs_json is not None:
num_dirs = num_dirs_json
with col1:
if num_dirs is not None:
st.write(f"Number of Diffusion Directions: <b>{num_dirs}</b> from JSON", unsafe_allow_html=True)
else:
num_dirs = st.number_input("Number of Diffusion Directions", min_value=6, step=1, value=6, key='num_dirs', disabled=uploaded_tensor_file is None)
with col2:
if num_t2 is not None:
st.write(f"Number of T2: <b>{num_t2}</b> from JSON", unsafe_allow_html=True)
else:
num_t2 = st.number_input("Number of T2", min_value=1, step=1, value=1, key='num_t2')
if freq is not None:
st.write(f"Frequency: <b>{freq}</b> from JSON", unsafe_allow_html=True)
else:
freq = st.radio("Frequency", ["RL", "AP"], key='freq')
else:
# Allow users to edit the extracted values or manually input if JSON is not uploaded
with col1:
num_dirs = st.selectbox("Number of Diffusion Directions", options=num_dirs_list, index=0)
with col2:
num_t2 = st.number_input("Number of T2", min_value=1, step=1, value=1, key='num_t2')
freq = st.radio("Frequency", ["RL", "AP"], index=["RL", "AP"].index("RL"), key='freq')
# Continue with the same logic for processing b-vectors and displaying outputs
b_val = st.number_input("b-Value", min_value=0, step=1, value=1000, key='b_val')
b_vector, header_lines, raw_lines = read_directions_from_file(file_content, num_dirs, num_t2)
convert_to_b_vector(b_vector, num_dirs, num_t2, b_val, freq)
bval_output, bvec_output = display_and_save_b_vector(b_vector, num_dirs, num_t2)
st.write("### Summary of b-values:")
b_values = [int(float(b)) for b in bval_output]
b_counter = Counter(b_values)
b_summary_html = ""
for b_value in sorted(b_counter):
b_summary_html += f"<div class='tight-line-spacing'>b={b_value: >5} x {b_counter[b_value]}</div>"
st.markdown(b_summary_html, unsafe_allow_html=True)
# Display headers, tables, and raw data
with st.expander("Display Header from " + file_name):
for header_line in header_lines:
st.markdown(f"<div class='header-info'>{header_line}</div>", unsafe_allow_html=True)
with st.expander("Display index/bval/bvec as a table"):
table_data = {
"b-value": [int(row[0]) for row in b_vector],
"bvec_x": [float(('%.6f' % row[1]).replace('-0.000000', '0')) for row in b_vector],
"bvec_y": [float(('%.6f' % row[2]).replace('-0.000000', '0')) for row in b_vector],
"bvec_z": [float(('%.6f' % row[3]).replace('-0.000000', '0')) for row in b_vector]
}
df = pd.DataFrame(table_data, index=range(1, len(b_vector) + 1))
table_html = df.to_html(classes='small-font', escape=False)
st.markdown(table_html, unsafe_allow_html=True)
with st.expander("Raw Lines from File " + file_name):
st.write(num_dirs)
for raw_line in raw_lines:
st.markdown(f"<div class='raw-lines'>{raw_line}</div>", unsafe_allow_html=True)
# Download buttons for bval and bvec files
download_bval_name = f"{file_name_prefix}_{num_t2}t2_{num_dirs}dir_b{b_val}.bval"
st.download_button("Download bval file", " ".join(bval_output), download_bval_name, key='download_bval')
download_bvec_name = f"{file_name_prefix}_{num_t2}t2_{num_dirs}dir_b{b_val}.bvec"
st.download_button("Download bvec file", "\n".join(bvec_output), download_bvec_name, key='download_bvec')
with st.expander("Display bval/bvec files"):
st.write("bval Output:")
st.write(" ".join(bval_output))
st.write("bvec Output:")
for line in bvec_output:
st.write(line)
else:
st.error("No valid tensor file to process.")
if __name__ == "__main__":
main()