napari-nifti-viewer 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- napari_nifti_viewer/__init__.py +22 -0
- napari_nifti_viewer/_nifti_loader.py +371 -0
- napari_nifti_viewer/_version.py +1 -0
- napari_nifti_viewer/_widget.py +450 -0
- napari_nifti_viewer/napari.yaml +10 -0
- napari_nifti_viewer-0.1.0.dist-info/METADATA +248 -0
- napari_nifti_viewer-0.1.0.dist-info/RECORD +10 -0
- napari_nifti_viewer-0.1.0.dist-info/WHEEL +5 -0
- napari_nifti_viewer-0.1.0.dist-info/entry_points.txt +2 -0
- napari_nifti_viewer-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
import os
|
2
|
+
import sys
|
3
|
+
import logging
|
4
|
+
from pathlib import Path
|
5
|
+
|
6
|
+
|
7
|
+
if sys.platform == 'darwin':
|
8
|
+
os.environ['QT_MAC_WANTS_LAYER'] = '1'
|
9
|
+
|
10
|
+
class QtWarningFilter(logging.Filter):
|
11
|
+
def filter(self, record):
|
12
|
+
return not (record.getMessage().find("Layer-backing is always enabled") >= 0)
|
13
|
+
|
14
|
+
logging.getLogger().addFilter(QtWarningFilter())
|
15
|
+
|
16
|
+
import warnings
|
17
|
+
warnings.filterwarnings("ignore", message="Layer-backing is always enabled")
|
18
|
+
|
19
|
+
from napari_nifti_viewer._version import __version__
|
20
|
+
from napari_nifti_viewer._widget import NiftiViewerWidget
|
21
|
+
|
22
|
+
__all__ = ["NiftiViewerWidget", "__version__"]
|
@@ -0,0 +1,371 @@
|
|
1
|
+
import os
|
2
|
+
from pathlib import Path
|
3
|
+
from typing import Dict, List, Optional, Tuple, Union, Any
|
4
|
+
import numpy as np
|
5
|
+
import nibabel as nib
|
6
|
+
import json
|
7
|
+
|
8
|
+
class NiftiLoader:
|
9
|
+
"""Specialized loader for NIfTI files, extracting data, metadata, labels, etc."""
|
10
|
+
|
11
|
+
def __init__(self):
|
12
|
+
self.supported_formats = {'.nii', '.nii.gz'}
|
13
|
+
self.current_file = None
|
14
|
+
self.current_nii = None
|
15
|
+
|
16
|
+
def load_file(self, file_path: Union[str, Path]) -> Tuple[np.ndarray, Dict]:
|
17
|
+
"""Load NIfTI file and extract all information
|
18
|
+
|
19
|
+
Args:
|
20
|
+
file_path: Path to NIfTI file
|
21
|
+
|
22
|
+
Returns:
|
23
|
+
(image data, complete metadata dictionary)
|
24
|
+
"""
|
25
|
+
file_path = Path(file_path)
|
26
|
+
if not file_path.exists():
|
27
|
+
raise FileNotFoundError(f"File does not exist: {file_path}")
|
28
|
+
|
29
|
+
# Check file format
|
30
|
+
if not self._is_nifti_file(file_path):
|
31
|
+
raise ValueError(f"Unsupported file format, only .nii and .nii.gz are supported")
|
32
|
+
|
33
|
+
# Load NIfTI file
|
34
|
+
self.current_file = file_path
|
35
|
+
self.current_nii = nib.load(str(file_path))
|
36
|
+
|
37
|
+
# Extract data
|
38
|
+
data = self.current_nii.get_fdata()
|
39
|
+
|
40
|
+
# Extract all metadata
|
41
|
+
metadata = self._extract_all_metadata()
|
42
|
+
|
43
|
+
return data, metadata
|
44
|
+
|
45
|
+
def _is_nifti_file(self, file_path: Path) -> bool:
|
46
|
+
"""Check if file is a NIfTI file"""
|
47
|
+
suffixes = [s.lower() for s in file_path.suffixes]
|
48
|
+
|
49
|
+
# Check .nii.gz
|
50
|
+
if len(suffixes) >= 2 and suffixes[-2] == '.nii' and suffixes[-1] == '.gz':
|
51
|
+
return True
|
52
|
+
|
53
|
+
# Check .nii
|
54
|
+
if file_path.suffix.lower() == '.nii':
|
55
|
+
return True
|
56
|
+
|
57
|
+
return False
|
58
|
+
|
59
|
+
def _extract_all_metadata(self) -> Dict:
|
60
|
+
"""Extract all metadata information from NIfTI file"""
|
61
|
+
if self.current_nii is None:
|
62
|
+
return {}
|
63
|
+
|
64
|
+
metadata = {}
|
65
|
+
|
66
|
+
# Basic file information
|
67
|
+
metadata['file_info'] = self._extract_file_info()
|
68
|
+
|
69
|
+
# Header information
|
70
|
+
metadata['header'] = self._extract_header_info()
|
71
|
+
|
72
|
+
# Affine transformation matrix
|
73
|
+
metadata['affine'] = self._extract_affine_info()
|
74
|
+
|
75
|
+
# Data information
|
76
|
+
metadata['data_info'] = self._extract_data_info()
|
77
|
+
|
78
|
+
# Coordinate system information
|
79
|
+
metadata['coordinate_system'] = self._extract_coordinate_info()
|
80
|
+
|
81
|
+
# Label information (if exists)
|
82
|
+
metadata['labels'] = self._extract_label_info()
|
83
|
+
|
84
|
+
# Extension header information (if exists)
|
85
|
+
metadata['extensions'] = self._extract_extensions()
|
86
|
+
|
87
|
+
return metadata
|
88
|
+
|
89
|
+
def _extract_file_info(self) -> Dict:
|
90
|
+
"""Extract basic file information"""
|
91
|
+
return {
|
92
|
+
'file_path': str(self.current_file),
|
93
|
+
'file_name': self.current_file.name,
|
94
|
+
'file_size': self.current_file.stat().st_size,
|
95
|
+
'format': 'NIfTI',
|
96
|
+
'version': self.current_nii.header.get('sizeof_hdr', 348),
|
97
|
+
}
|
98
|
+
|
99
|
+
def _extract_header_info(self) -> Dict:
|
100
|
+
"""Extract detailed NIfTI header information"""
|
101
|
+
header = self.current_nii.header
|
102
|
+
|
103
|
+
header_info = {}
|
104
|
+
|
105
|
+
# Basic header fields
|
106
|
+
basic_fields = [
|
107
|
+
'sizeof_hdr', 'data_type', 'db_name', 'extents', 'session_error',
|
108
|
+
'regular', 'dim_info', 'dim', 'intent_p1', 'intent_p2', 'intent_p3',
|
109
|
+
'intent_code', 'datatype', 'bitpix', 'slice_start', 'pixdim',
|
110
|
+
'vox_offset', 'scl_slope', 'scl_inter', 'slice_end', 'slice_code',
|
111
|
+
'xyzt_units', 'cal_max', 'cal_min', 'slice_duration', 'toffset',
|
112
|
+
'glmax', 'glmin', 'descrip', 'aux_file', 'qform_code', 'sform_code',
|
113
|
+
'quatern_b', 'quatern_c', 'quatern_d', 'qoffset_x', 'qoffset_y',
|
114
|
+
'qoffset_z', 'srow_x', 'srow_y', 'srow_z', 'intent_name', 'magic'
|
115
|
+
]
|
116
|
+
|
117
|
+
for field in basic_fields:
|
118
|
+
try:
|
119
|
+
value = header.get(field)
|
120
|
+
if value is not None:
|
121
|
+
# Convert numpy types to Python native types for JSON serialization
|
122
|
+
if isinstance(value, np.ndarray):
|
123
|
+
header_info[field] = value.tolist()
|
124
|
+
elif isinstance(value, (np.integer, np.floating)):
|
125
|
+
header_info[field] = value.item()
|
126
|
+
elif isinstance(value, bytes):
|
127
|
+
header_info[field] = value.decode('utf-8', errors='ignore').strip('\x00')
|
128
|
+
else:
|
129
|
+
header_info[field] = value
|
130
|
+
except Exception as e:
|
131
|
+
header_info[field] = f"Extraction failed: {str(e)}"
|
132
|
+
|
133
|
+
# Add explanatory information
|
134
|
+
header_info['explanations'] = self._get_header_explanations(header_info)
|
135
|
+
|
136
|
+
return header_info
|
137
|
+
|
138
|
+
def _extract_affine_info(self) -> Dict:
|
139
|
+
"""Extract affine transformation matrix information"""
|
140
|
+
affine = self.current_nii.affine
|
141
|
+
|
142
|
+
return {
|
143
|
+
'affine_matrix': affine.tolist(),
|
144
|
+
'translation': affine[:3, 3].tolist(),
|
145
|
+
'rotation_scaling': affine[:3, :3].tolist(),
|
146
|
+
'determinant': float(np.linalg.det(affine[:3, :3])),
|
147
|
+
'is_orthogonal': bool(np.allclose(np.dot(affine[:3, :3], affine[:3, :3].T), np.eye(3))),
|
148
|
+
}
|
149
|
+
|
150
|
+
def _extract_data_info(self) -> Dict:
|
151
|
+
"""Extract data statistics information"""
|
152
|
+
data = self.current_nii.get_fdata()
|
153
|
+
|
154
|
+
return {
|
155
|
+
'shape': list(data.shape),
|
156
|
+
'ndim': data.ndim,
|
157
|
+
'dtype': str(data.dtype),
|
158
|
+
'min_value': float(np.min(data)),
|
159
|
+
'max_value': float(np.max(data)),
|
160
|
+
'mean_value': float(np.mean(data)),
|
161
|
+
'std_value': float(np.std(data)),
|
162
|
+
'median_value': float(np.median(data)),
|
163
|
+
'unique_values_count': int(len(np.unique(data))),
|
164
|
+
'non_zero_count': int(np.count_nonzero(data)),
|
165
|
+
'zero_count': int(np.sum(data == 0)),
|
166
|
+
'nan_count': int(np.sum(np.isnan(data))),
|
167
|
+
'inf_count': int(np.sum(np.isinf(data))),
|
168
|
+
}
|
169
|
+
|
170
|
+
def _extract_coordinate_info(self) -> Dict:
|
171
|
+
"""Extract coordinate system information"""
|
172
|
+
header = self.current_nii.header
|
173
|
+
|
174
|
+
# Get voxel size
|
175
|
+
pixdim = header.get_zooms()
|
176
|
+
|
177
|
+
# Get unit information
|
178
|
+
xyzt_units = header.get('xyzt_units', 0)
|
179
|
+
spatial_unit = xyzt_units & 0x07
|
180
|
+
temporal_unit = (xyzt_units & 0x38) >> 3
|
181
|
+
|
182
|
+
spatial_units_map = {
|
183
|
+
0: 'Unknown',
|
184
|
+
1: 'meters (m)',
|
185
|
+
2: 'millimeters (mm)',
|
186
|
+
3: 'micrometers (μm)',
|
187
|
+
}
|
188
|
+
|
189
|
+
temporal_units_map = {
|
190
|
+
0: 'Unknown',
|
191
|
+
8: 'seconds (s)',
|
192
|
+
16: 'milliseconds (ms)',
|
193
|
+
24: 'microseconds (μs)',
|
194
|
+
}
|
195
|
+
|
196
|
+
return {
|
197
|
+
'voxel_size': list(pixdim) if pixdim else [],
|
198
|
+
'spatial_unit': spatial_units_map.get(spatial_unit, f'Unknown unit ({spatial_unit})'),
|
199
|
+
'temporal_unit': temporal_units_map.get(temporal_unit, f'Unknown unit ({temporal_unit})'),
|
200
|
+
'qform_code': int(header.get('qform_code', 0)),
|
201
|
+
'sform_code': int(header.get('sform_code', 0)),
|
202
|
+
'slice_code': int(header.get('slice_code', 0)),
|
203
|
+
}
|
204
|
+
|
205
|
+
def _extract_label_info(self) -> Dict:
|
206
|
+
"""Extract label information (if it exists)"""
|
207
|
+
data = self.current_nii.get_fdata()
|
208
|
+
|
209
|
+
# Check if this might be a label image
|
210
|
+
unique_values = np.unique(data)
|
211
|
+
|
212
|
+
label_info = {
|
213
|
+
'unique_values': unique_values.tolist(),
|
214
|
+
'is_likely_labels': self._is_likely_label_image(data, unique_values),
|
215
|
+
'label_count': len(unique_values),
|
216
|
+
}
|
217
|
+
|
218
|
+
# If likely a label image, extract more information
|
219
|
+
if label_info['is_likely_labels']:
|
220
|
+
label_info['label_statistics'] = {}
|
221
|
+
for label_val in unique_values:
|
222
|
+
mask = data == label_val
|
223
|
+
label_info['label_statistics'][str(float(label_val))] = {
|
224
|
+
'voxel_count': int(np.sum(mask)),
|
225
|
+
'percentage': float(np.sum(mask) / data.size * 100),
|
226
|
+
}
|
227
|
+
|
228
|
+
return label_info
|
229
|
+
|
230
|
+
def _is_likely_label_image(self, data: np.ndarray, unique_values: np.ndarray) -> bool:
|
231
|
+
"""Determine if this is likely a label image"""
|
232
|
+
# Heuristic rules for label detection
|
233
|
+
|
234
|
+
# 1. If unique values are few and integers
|
235
|
+
if len(unique_values) <= 50 and np.allclose(unique_values, unique_values.astype(int)):
|
236
|
+
return True
|
237
|
+
|
238
|
+
# 2. If data type is integer
|
239
|
+
if np.issubdtype(data.dtype, np.integer):
|
240
|
+
return True
|
241
|
+
|
242
|
+
# 3. If all values are integers (even if data type is float)
|
243
|
+
if np.allclose(data, np.round(data)):
|
244
|
+
return True
|
245
|
+
|
246
|
+
return False
|
247
|
+
|
248
|
+
def _extract_extensions(self) -> List[Dict]:
|
249
|
+
"""Extract NIfTI extension information"""
|
250
|
+
extensions = []
|
251
|
+
|
252
|
+
if hasattr(self.current_nii, 'extra') and self.current_nii.extra:
|
253
|
+
for i, ext in enumerate(self.current_nii.extra):
|
254
|
+
ext_info = {
|
255
|
+
'index': i,
|
256
|
+
'esize': ext.get('esize', 0),
|
257
|
+
'ecode': ext.get('ecode', 0),
|
258
|
+
}
|
259
|
+
|
260
|
+
# Try to decode extension data
|
261
|
+
if 'edata' in ext:
|
262
|
+
try:
|
263
|
+
ext_info['edata'] = ext['edata'].decode('utf-8', errors='ignore')
|
264
|
+
except:
|
265
|
+
ext_info['edata'] = f"Binary data (length: {len(ext['edata'])})"
|
266
|
+
|
267
|
+
extensions.append(ext_info)
|
268
|
+
|
269
|
+
return extensions
|
270
|
+
|
271
|
+
def _get_header_explanations(self, header_info: Dict) -> Dict:
|
272
|
+
"""Provide explanations for header fields"""
|
273
|
+
explanations = {}
|
274
|
+
|
275
|
+
# Data type explanations
|
276
|
+
datatype_map = {
|
277
|
+
2: 'Unsigned char (uint8)',
|
278
|
+
4: 'Signed short (int16)',
|
279
|
+
8: 'Signed int (int32)',
|
280
|
+
16: 'Single precision float (float32)',
|
281
|
+
64: 'Double precision float (float64)',
|
282
|
+
256: 'Signed char (int8)',
|
283
|
+
512: 'Unsigned short (uint16)',
|
284
|
+
768: 'Unsigned int (uint32)',
|
285
|
+
}
|
286
|
+
|
287
|
+
if 'datatype' in header_info:
|
288
|
+
explanations['datatype'] = datatype_map.get(
|
289
|
+
header_info['datatype'],
|
290
|
+
f"Unknown datatype ({header_info['datatype']})"
|
291
|
+
)
|
292
|
+
|
293
|
+
# Intent code explanations
|
294
|
+
intent_map = {
|
295
|
+
0: 'No special intent',
|
296
|
+
2: 'Correlation coefficient',
|
297
|
+
3: 'T-statistic',
|
298
|
+
4: 'F-statistic',
|
299
|
+
5: 'Z-score',
|
300
|
+
}
|
301
|
+
|
302
|
+
if 'intent_code' in header_info:
|
303
|
+
explanations['intent_code'] = intent_map.get(
|
304
|
+
header_info['intent_code'],
|
305
|
+
f"Other intent ({header_info['intent_code']})"
|
306
|
+
)
|
307
|
+
|
308
|
+
return explanations
|
309
|
+
|
310
|
+
def get_metadata_summary(self, metadata: Dict) -> str:
|
311
|
+
"""Generate human-readable metadata summary"""
|
312
|
+
summary = []
|
313
|
+
|
314
|
+
# File information
|
315
|
+
if 'file_info' in metadata:
|
316
|
+
info = metadata['file_info']
|
317
|
+
summary.append(f"Filename: {info.get('file_name', 'Unknown')}")
|
318
|
+
summary.append(f"File size: {info.get('file_size', 0) / 1024 / 1024:.2f} MB")
|
319
|
+
|
320
|
+
# Data information
|
321
|
+
if 'data_info' in metadata:
|
322
|
+
data = metadata['data_info']
|
323
|
+
summary.append(f"Data shape: {data.get('shape', [])}")
|
324
|
+
summary.append(f"Data type: {data.get('dtype', 'Unknown')}")
|
325
|
+
summary.append(f"Value range: [{data.get('min_value', 0):.3f}, {data.get('max_value', 0):.3f}]")
|
326
|
+
summary.append(f"Mean value: {data.get('mean_value', 0):.3f}")
|
327
|
+
summary.append(f"Std deviation: {data.get('std_value', 0):.3f}")
|
328
|
+
|
329
|
+
# Coordinate system information
|
330
|
+
if 'coordinate_system' in metadata:
|
331
|
+
coord = metadata['coordinate_system']
|
332
|
+
if coord.get('voxel_size'):
|
333
|
+
summary.append(f"Voxel size: {[f'{v:.3f}' for v in coord['voxel_size']]} {coord.get('spatial_unit', '')}")
|
334
|
+
|
335
|
+
# Label information
|
336
|
+
if 'labels' in metadata and metadata['labels'].get('is_likely_labels'):
|
337
|
+
labels = metadata['labels']
|
338
|
+
summary.append(f"Label image: Yes")
|
339
|
+
summary.append(f"Label count: {labels.get('label_count', 0)}")
|
340
|
+
|
341
|
+
return "\n".join(summary)
|
342
|
+
|
343
|
+
def export_metadata_to_json(self, metadata: Dict, output_path: Union[str, Path]) -> None:
|
344
|
+
"""Export metadata to JSON file"""
|
345
|
+
output_path = Path(output_path)
|
346
|
+
|
347
|
+
# Ensure output directory exists
|
348
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
349
|
+
|
350
|
+
# Deep copy and convert numpy types to JSON serializable types
|
351
|
+
def convert_numpy_types(obj):
|
352
|
+
if isinstance(obj, dict):
|
353
|
+
return {key: convert_numpy_types(value) for key, value in obj.items()}
|
354
|
+
elif isinstance(obj, list):
|
355
|
+
return [convert_numpy_types(item) for item in obj]
|
356
|
+
elif isinstance(obj, np.ndarray):
|
357
|
+
return obj.tolist()
|
358
|
+
elif isinstance(obj, (np.integer, np.floating)):
|
359
|
+
return obj.item()
|
360
|
+
elif isinstance(obj, np.bool_):
|
361
|
+
return bool(obj)
|
362
|
+
elif isinstance(obj, bytes):
|
363
|
+
return obj.decode('utf-8', errors='ignore').strip('\x00')
|
364
|
+
else:
|
365
|
+
return obj
|
366
|
+
|
367
|
+
json_serializable_metadata = convert_numpy_types(metadata)
|
368
|
+
|
369
|
+
# Write JSON file
|
370
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
371
|
+
json.dump(json_serializable_metadata, f, ensure_ascii=False, indent=2)
|
@@ -0,0 +1 @@
|
|
1
|
+
__version__ = "0.1.0"
|
@@ -0,0 +1,450 @@
|
|
1
|
+
from typing import Dict, List, Optional, Tuple, Union
|
2
|
+
import os
|
3
|
+
import sys
|
4
|
+
from pathlib import Path
|
5
|
+
import numpy as np
|
6
|
+
import napari
|
7
|
+
from qtpy.QtWidgets import (
|
8
|
+
QWidget, QVBoxLayout, QHBoxLayout, QTabWidget,
|
9
|
+
QPushButton, QLabel, QLineEdit, QTextEdit, QGroupBox,
|
10
|
+
QFileDialog, QMessageBox, QScrollArea, QTreeWidget,
|
11
|
+
QTreeWidgetItem, QSplitter, QTableWidget, QTableWidgetItem
|
12
|
+
)
|
13
|
+
from qtpy.QtCore import Qt
|
14
|
+
|
15
|
+
# 抑制 macOS 上的 Qt layer-backing 警告
|
16
|
+
if sys.platform == 'darwin':
|
17
|
+
os.environ['QT_MAC_WANTS_LAYER'] = '1'
|
18
|
+
import warnings
|
19
|
+
warnings.filterwarnings("ignore", message="Layer-backing is always enabled")
|
20
|
+
|
21
|
+
from ._nifti_loader import NiftiLoader
|
22
|
+
|
23
|
+
class NiftiViewerWidget(QWidget):
|
24
|
+
"""Main interface for NIfTI file viewer"""
|
25
|
+
|
26
|
+
def __init__(self, napari_viewer):
|
27
|
+
super().__init__()
|
28
|
+
self.viewer = napari_viewer
|
29
|
+
self.nifti_loader = NiftiLoader()
|
30
|
+
self.current_data = None
|
31
|
+
self.current_metadata = None
|
32
|
+
|
33
|
+
# 设置界面
|
34
|
+
self.setup_ui()
|
35
|
+
|
36
|
+
def setup_ui(self):
|
37
|
+
"""设置用户界面"""
|
38
|
+
layout = QVBoxLayout()
|
39
|
+
|
40
|
+
# File loading area
|
41
|
+
load_group = QGroupBox("File Loading")
|
42
|
+
load_layout = QVBoxLayout()
|
43
|
+
|
44
|
+
# File selection
|
45
|
+
file_layout = QHBoxLayout()
|
46
|
+
self.file_path = QLineEdit()
|
47
|
+
self.file_path.setReadOnly(True)
|
48
|
+
self.file_path.setPlaceholderText("Select a .nii or .nii.gz file...")
|
49
|
+
|
50
|
+
browse_btn = QPushButton("Browse...")
|
51
|
+
browse_btn.clicked.connect(self.load_file)
|
52
|
+
|
53
|
+
load_btn = QPushButton("Load to Napari")
|
54
|
+
load_btn.clicked.connect(self.load_to_napari)
|
55
|
+
load_btn.setEnabled(False)
|
56
|
+
self.load_btn = load_btn
|
57
|
+
|
58
|
+
file_layout.addWidget(QLabel("File:"))
|
59
|
+
file_layout.addWidget(self.file_path, 1)
|
60
|
+
file_layout.addWidget(browse_btn)
|
61
|
+
file_layout.addWidget(load_btn)
|
62
|
+
|
63
|
+
load_layout.addLayout(file_layout)
|
64
|
+
load_group.setLayout(load_layout)
|
65
|
+
layout.addWidget(load_group)
|
66
|
+
|
67
|
+
# Create tabbed interface
|
68
|
+
self.tabs = QTabWidget()
|
69
|
+
|
70
|
+
# File overview tab (merge basic info + data statistics)
|
71
|
+
self.setup_overview_tab()
|
72
|
+
|
73
|
+
# Detailed info tab (merge header info + metadata)
|
74
|
+
self.setup_detailed_info_tab()
|
75
|
+
|
76
|
+
# Label analysis tab
|
77
|
+
self.setup_labels_tab()
|
78
|
+
|
79
|
+
layout.addWidget(self.tabs)
|
80
|
+
|
81
|
+
# 导出按钮
|
82
|
+
export_layout = QHBoxLayout()
|
83
|
+
export_btn = QPushButton("Export Metadata as JSON")
|
84
|
+
export_btn.clicked.connect(self.export_metadata)
|
85
|
+
export_btn.setEnabled(False)
|
86
|
+
self.export_btn = export_btn
|
87
|
+
|
88
|
+
export_layout.addStretch()
|
89
|
+
export_layout.addWidget(export_btn)
|
90
|
+
layout.addLayout(export_layout)
|
91
|
+
|
92
|
+
self.setLayout(layout)
|
93
|
+
|
94
|
+
def setup_overview_tab(self):
|
95
|
+
"""Set up file overview tab (merge basic info and data statistics)"""
|
96
|
+
overview_tab = QWidget()
|
97
|
+
layout = QVBoxLayout()
|
98
|
+
|
99
|
+
# Basic file information
|
100
|
+
file_group = QGroupBox("File Information")
|
101
|
+
file_layout = QVBoxLayout()
|
102
|
+
|
103
|
+
self.file_info_text = QTextEdit()
|
104
|
+
self.file_info_text.setReadOnly(True)
|
105
|
+
self.file_info_text.setMaximumHeight(120)
|
106
|
+
file_layout.addWidget(self.file_info_text)
|
107
|
+
file_group.setLayout(file_layout)
|
108
|
+
|
109
|
+
# Data statistics information (using table)
|
110
|
+
stats_group = QGroupBox("Data Statistics")
|
111
|
+
stats_layout = QVBoxLayout()
|
112
|
+
|
113
|
+
self.overview_stats_table = QTableWidget()
|
114
|
+
self.overview_stats_table.setColumnCount(2)
|
115
|
+
self.overview_stats_table.setHorizontalHeaderLabels(["Property", "Value"])
|
116
|
+
self.overview_stats_table.horizontalHeader().setStretchLastSection(True)
|
117
|
+
self.overview_stats_table.verticalHeader().setVisible(False) # Hide row numbers
|
118
|
+
self.overview_stats_table.setMaximumHeight(200)
|
119
|
+
|
120
|
+
stats_layout.addWidget(self.overview_stats_table)
|
121
|
+
stats_group.setLayout(stats_layout)
|
122
|
+
|
123
|
+
# Coordinate system and affine transformation
|
124
|
+
transform_group = QGroupBox("Coordinate System & Transform")
|
125
|
+
transform_layout = QVBoxLayout()
|
126
|
+
|
127
|
+
self.transform_text = QTextEdit()
|
128
|
+
self.transform_text.setReadOnly(True)
|
129
|
+
self.transform_text.setMaximumHeight(150)
|
130
|
+
transform_layout.addWidget(self.transform_text)
|
131
|
+
transform_group.setLayout(transform_layout)
|
132
|
+
|
133
|
+
layout.addWidget(file_group)
|
134
|
+
layout.addWidget(stats_group)
|
135
|
+
layout.addWidget(transform_group)
|
136
|
+
layout.addStretch()
|
137
|
+
|
138
|
+
overview_tab.setLayout(layout)
|
139
|
+
self.tabs.addTab(overview_tab, "File Overview")
|
140
|
+
|
141
|
+
def setup_detailed_info_tab(self):
|
142
|
+
"""Set up detailed info tab (merge NIfTI header info and complete metadata)"""
|
143
|
+
detailed_tab = QWidget()
|
144
|
+
layout = QVBoxLayout()
|
145
|
+
|
146
|
+
# 创建水平分割器
|
147
|
+
splitter = QSplitter(Qt.Horizontal)
|
148
|
+
|
149
|
+
# Left side: NIfTI header fields
|
150
|
+
left_widget = QWidget()
|
151
|
+
left_layout = QVBoxLayout()
|
152
|
+
left_layout.addWidget(QLabel("NIfTI Header Fields"))
|
153
|
+
|
154
|
+
self.header_tree = QTreeWidget()
|
155
|
+
self.header_tree.setHeaderLabels(["Field", "Value", "Description"])
|
156
|
+
self.header_tree.setAlternatingRowColors(False) # Turn off alternating row colors
|
157
|
+
self.header_tree.setRootIsDecorated(False) # Don't show root node decoration
|
158
|
+
|
159
|
+
# Set better styling
|
160
|
+
self.header_tree.setStyleSheet("""
|
161
|
+
QTreeWidget {
|
162
|
+
background-color: #2b2b2b;
|
163
|
+
color: white;
|
164
|
+
border: 1px solid #555;
|
165
|
+
selection-background-color: #404040;
|
166
|
+
}
|
167
|
+
QTreeWidget::item {
|
168
|
+
padding: 4px;
|
169
|
+
border-bottom: 1px solid #404040;
|
170
|
+
}
|
171
|
+
QTreeWidget::item:selected {
|
172
|
+
background-color: #404040;
|
173
|
+
}
|
174
|
+
QHeaderView::section {
|
175
|
+
background-color: #3c3c3c;
|
176
|
+
color: white;
|
177
|
+
padding: 6px;
|
178
|
+
border: 1px solid #555;
|
179
|
+
font-weight: bold;
|
180
|
+
}
|
181
|
+
""")
|
182
|
+
left_layout.addWidget(self.header_tree)
|
183
|
+
left_widget.setLayout(left_layout)
|
184
|
+
|
185
|
+
# Right side: Complete metadata JSON
|
186
|
+
right_widget = QWidget()
|
187
|
+
right_layout = QVBoxLayout()
|
188
|
+
right_layout.addWidget(QLabel("Complete Metadata (JSON Format)"))
|
189
|
+
|
190
|
+
self.raw_metadata_text = QTextEdit()
|
191
|
+
self.raw_metadata_text.setReadOnly(True)
|
192
|
+
self.raw_metadata_text.setFont(self.raw_metadata_text.font()) # 使用等宽字体
|
193
|
+
right_layout.addWidget(self.raw_metadata_text)
|
194
|
+
right_widget.setLayout(right_layout)
|
195
|
+
|
196
|
+
splitter.addWidget(left_widget)
|
197
|
+
splitter.addWidget(right_widget)
|
198
|
+
splitter.setSizes([1, 1]) # 平分空间
|
199
|
+
|
200
|
+
layout.addWidget(splitter)
|
201
|
+
detailed_tab.setLayout(layout)
|
202
|
+
self.tabs.addTab(detailed_tab, "Detailed Info")
|
203
|
+
|
204
|
+
def setup_labels_tab(self):
|
205
|
+
"""Set up label information tab"""
|
206
|
+
labels_tab = QWidget()
|
207
|
+
layout = QVBoxLayout()
|
208
|
+
|
209
|
+
# Label detection results
|
210
|
+
detection_group = QGroupBox("Label Detection")
|
211
|
+
detection_layout = QVBoxLayout()
|
212
|
+
|
213
|
+
self.label_detection_text = QTextEdit()
|
214
|
+
self.label_detection_text.setReadOnly(True)
|
215
|
+
self.label_detection_text.setMaximumHeight(100)
|
216
|
+
detection_layout.addWidget(self.label_detection_text)
|
217
|
+
detection_group.setLayout(detection_layout)
|
218
|
+
|
219
|
+
# Label statistics table
|
220
|
+
label_stats_group = QGroupBox("Label Statistics")
|
221
|
+
label_stats_layout = QVBoxLayout()
|
222
|
+
|
223
|
+
self.label_stats_table = QTableWidget()
|
224
|
+
self.label_stats_table.setColumnCount(3)
|
225
|
+
self.label_stats_table.setHorizontalHeaderLabels(["Label Value", "Voxel Count", "Percentage (%)"])
|
226
|
+
self.label_stats_table.horizontalHeader().setStretchLastSection(True)
|
227
|
+
self.label_stats_table.verticalHeader().setVisible(False) # Hide row numbers
|
228
|
+
|
229
|
+
label_stats_layout.addWidget(self.label_stats_table)
|
230
|
+
label_stats_group.setLayout(label_stats_layout)
|
231
|
+
|
232
|
+
layout.addWidget(detection_group)
|
233
|
+
layout.addWidget(label_stats_group)
|
234
|
+
layout.addStretch()
|
235
|
+
|
236
|
+
labels_tab.setLayout(layout)
|
237
|
+
self.tabs.addTab(labels_tab, "Label Analysis")
|
238
|
+
|
239
|
+
def load_file(self):
|
240
|
+
"""Load NIfTI file"""
|
241
|
+
file_path, _ = QFileDialog.getOpenFileName(
|
242
|
+
self,
|
243
|
+
"Select NIfTI File",
|
244
|
+
"",
|
245
|
+
"NIfTI Files (*.nii *.nii.gz);;All Files (*.*)"
|
246
|
+
)
|
247
|
+
|
248
|
+
if file_path:
|
249
|
+
try:
|
250
|
+
# 加载文件和元数据
|
251
|
+
data, metadata = self.nifti_loader.load_file(file_path)
|
252
|
+
self.current_data = data
|
253
|
+
self.current_metadata = metadata
|
254
|
+
|
255
|
+
# 更新界面
|
256
|
+
self.file_path.setText(file_path)
|
257
|
+
self.update_all_displays()
|
258
|
+
|
259
|
+
# 启用按钮
|
260
|
+
self.load_btn.setEnabled(True)
|
261
|
+
self.export_btn.setEnabled(True)
|
262
|
+
|
263
|
+
QMessageBox.information(self, "Success", "File loaded successfully!")
|
264
|
+
|
265
|
+
except Exception as e:
|
266
|
+
QMessageBox.critical(self, "Error", f"Error loading file: {str(e)}")
|
267
|
+
|
268
|
+
def load_to_napari(self):
|
269
|
+
"""Load data into Napari viewer"""
|
270
|
+
if self.current_data is not None:
|
271
|
+
try:
|
272
|
+
# 根据数据特征选择合适的显示方式
|
273
|
+
file_name = Path(self.file_path.text()).name
|
274
|
+
|
275
|
+
# 检查是否可能是标签图像
|
276
|
+
is_labels = (self.current_metadata.get('labels', {}).get('is_likely_labels', False)
|
277
|
+
if self.current_metadata else False)
|
278
|
+
|
279
|
+
if is_labels:
|
280
|
+
# Add as label layer
|
281
|
+
self.viewer.add_labels(
|
282
|
+
self.current_data.astype(np.int32),
|
283
|
+
name=f"Labels: {file_name}"
|
284
|
+
)
|
285
|
+
else:
|
286
|
+
# Add as image layer
|
287
|
+
self.viewer.add_image(
|
288
|
+
self.current_data,
|
289
|
+
name=f"Image: {file_name}"
|
290
|
+
)
|
291
|
+
|
292
|
+
QMessageBox.information(self, "Success", "Data added to Napari viewer!")
|
293
|
+
|
294
|
+
except Exception as e:
|
295
|
+
QMessageBox.critical(self, "Error", f"Error adding data to Napari: {str(e)}")
|
296
|
+
|
297
|
+
def update_all_displays(self):
|
298
|
+
"""更新所有显示内容"""
|
299
|
+
if self.current_metadata is None:
|
300
|
+
return
|
301
|
+
|
302
|
+
self.update_overview()
|
303
|
+
self.update_detailed_info()
|
304
|
+
self.update_labels_info()
|
305
|
+
|
306
|
+
def update_overview(self):
|
307
|
+
"""更新文件概览显示"""
|
308
|
+
metadata = self.current_metadata
|
309
|
+
|
310
|
+
# File information
|
311
|
+
if 'file_info' in metadata:
|
312
|
+
file_info = metadata['file_info']
|
313
|
+
file_text = f"""Filename: {file_info.get('file_name', 'Unknown')}
|
314
|
+
File Size: {file_info.get('file_size', 0) / 1024 / 1024:.2f} MB
|
315
|
+
Format: {file_info.get('format', 'Unknown')} (Version: {file_info.get('version', 'Unknown')})"""
|
316
|
+
self.file_info_text.setText(file_text)
|
317
|
+
|
318
|
+
# Data statistics table
|
319
|
+
if 'data_info' in metadata:
|
320
|
+
data_info = metadata['data_info']
|
321
|
+
|
322
|
+
stats_items = [
|
323
|
+
('Data Shape', str(data_info.get('shape', []))),
|
324
|
+
('Data Type', str(data_info.get('dtype', 'Unknown'))),
|
325
|
+
('Min Value', f"{data_info.get('min_value', 0):.6f}"),
|
326
|
+
('Max Value', f"{data_info.get('max_value', 0):.6f}"),
|
327
|
+
('Mean Value', f"{data_info.get('mean_value', 0):.6f}"),
|
328
|
+
('Std Deviation', f"{data_info.get('std_value', 0):.6f}"),
|
329
|
+
('Unique Values', str(data_info.get('unique_values_count', 0))),
|
330
|
+
('Non-zero Voxels', str(data_info.get('non_zero_count', 0))),
|
331
|
+
]
|
332
|
+
|
333
|
+
self.overview_stats_table.setRowCount(len(stats_items))
|
334
|
+
for i, (name, value) in enumerate(stats_items):
|
335
|
+
self.overview_stats_table.setItem(i, 0, QTableWidgetItem(name))
|
336
|
+
self.overview_stats_table.setItem(i, 1, QTableWidgetItem(value))
|
337
|
+
|
338
|
+
# 坐标系统和仿射变换
|
339
|
+
transform_parts = []
|
340
|
+
|
341
|
+
if 'coordinate_system' in metadata:
|
342
|
+
coord_info = metadata['coordinate_system']
|
343
|
+
transform_parts.append(f"Voxel Size: {coord_info.get('voxel_size', [])} {coord_info.get('spatial_unit', '')}")
|
344
|
+
transform_parts.append(f"Transform Codes: QForm={coord_info.get('qform_code', 0)}, SForm={coord_info.get('sform_code', 0)}")
|
345
|
+
|
346
|
+
if 'affine' in metadata:
|
347
|
+
affine_info = metadata['affine']
|
348
|
+
affine_matrix = np.array(affine_info['affine_matrix'])
|
349
|
+
transform_parts.append(f"\nAffine Transform Matrix:\n{affine_matrix}")
|
350
|
+
transform_parts.append(f"Translation Vector: {affine_info.get('translation', [])}")
|
351
|
+
transform_parts.append(f"Determinant: {affine_info.get('determinant', 0):.6f}")
|
352
|
+
|
353
|
+
self.transform_text.setText('\n'.join(transform_parts))
|
354
|
+
|
355
|
+
def update_detailed_info(self):
|
356
|
+
"""更新详细信息显示"""
|
357
|
+
# 更新头部信息树
|
358
|
+
self.header_tree.clear()
|
359
|
+
|
360
|
+
if 'header' in self.current_metadata:
|
361
|
+
header_info = self.current_metadata['header']
|
362
|
+
explanations = header_info.get('explanations', {})
|
363
|
+
|
364
|
+
for key, value in header_info.items():
|
365
|
+
if key == 'explanations':
|
366
|
+
continue
|
367
|
+
|
368
|
+
item = QTreeWidgetItem(self.header_tree)
|
369
|
+
item.setText(0, key)
|
370
|
+
item.setText(1, str(value))
|
371
|
+
item.setText(2, explanations.get(key, ''))
|
372
|
+
|
373
|
+
# 调整列宽
|
374
|
+
self.header_tree.resizeColumnToContents(0)
|
375
|
+
self.header_tree.resizeColumnToContents(1)
|
376
|
+
|
377
|
+
# Update complete metadata JSON
|
378
|
+
try:
|
379
|
+
# Use the same type conversion logic as export
|
380
|
+
def convert_numpy_types(obj):
|
381
|
+
if isinstance(obj, dict):
|
382
|
+
return {key: convert_numpy_types(value) for key, value in obj.items()}
|
383
|
+
elif isinstance(obj, list):
|
384
|
+
return [convert_numpy_types(item) for item in obj]
|
385
|
+
elif isinstance(obj, np.ndarray):
|
386
|
+
return obj.tolist()
|
387
|
+
elif isinstance(obj, (np.integer, np.floating)):
|
388
|
+
return obj.item()
|
389
|
+
elif isinstance(obj, np.bool_):
|
390
|
+
return bool(obj)
|
391
|
+
elif isinstance(obj, bytes):
|
392
|
+
return obj.decode('utf-8', errors='ignore').strip('\x00')
|
393
|
+
else:
|
394
|
+
return obj
|
395
|
+
|
396
|
+
json_serializable_metadata = convert_numpy_types(self.current_metadata)
|
397
|
+
|
398
|
+
import json
|
399
|
+
json_text = json.dumps(json_serializable_metadata, ensure_ascii=False, indent=2)
|
400
|
+
self.raw_metadata_text.setText(json_text)
|
401
|
+
except Exception as e:
|
402
|
+
self.raw_metadata_text.setText(f"JSON serialization failed: {str(e)}")
|
403
|
+
|
404
|
+
def update_labels_info(self):
|
405
|
+
"""Update label information display"""
|
406
|
+
if 'labels' not in self.current_metadata:
|
407
|
+
return
|
408
|
+
|
409
|
+
labels_info = self.current_metadata['labels']
|
410
|
+
|
411
|
+
# Label detection results
|
412
|
+
is_labels = labels_info.get('is_likely_labels', False)
|
413
|
+
label_count = labels_info.get('label_count', 0)
|
414
|
+
|
415
|
+
detection_text = f"""Label Image Detection: {'Yes' if is_labels else 'No'}
|
416
|
+
Label Count: {label_count}
|
417
|
+
All Unique Values: {labels_info.get('unique_values', [])}"""
|
418
|
+
self.label_detection_text.setText(detection_text)
|
419
|
+
|
420
|
+
# Label statistics table
|
421
|
+
if 'label_statistics' in labels_info:
|
422
|
+
stats = labels_info['label_statistics']
|
423
|
+
|
424
|
+
self.label_stats_table.setRowCount(len(stats))
|
425
|
+
for i, (label_val, label_stats) in enumerate(stats.items()):
|
426
|
+
self.label_stats_table.setItem(i, 0, QTableWidgetItem(label_val))
|
427
|
+
self.label_stats_table.setItem(i, 1, QTableWidgetItem(str(label_stats['voxel_count'])))
|
428
|
+
self.label_stats_table.setItem(i, 2, QTableWidgetItem(f"{label_stats['percentage']:.2f}"))
|
429
|
+
else:
|
430
|
+
self.label_stats_table.setRowCount(0)
|
431
|
+
|
432
|
+
def export_metadata(self):
|
433
|
+
"""Export metadata to JSON file"""
|
434
|
+
if self.current_metadata is None:
|
435
|
+
QMessageBox.warning(self, "Warning", "No metadata to export")
|
436
|
+
return
|
437
|
+
|
438
|
+
file_path, _ = QFileDialog.getSaveFileName(
|
439
|
+
self,
|
440
|
+
"Save Metadata",
|
441
|
+
f"{Path(self.file_path.text()).stem}_metadata.json",
|
442
|
+
"JSON Files (*.json);;All Files (*.*)"
|
443
|
+
)
|
444
|
+
|
445
|
+
if file_path:
|
446
|
+
try:
|
447
|
+
self.nifti_loader.export_metadata_to_json(self.current_metadata, file_path)
|
448
|
+
QMessageBox.information(self, "Success", f"Metadata saved to: {file_path}")
|
449
|
+
except Exception as e:
|
450
|
+
QMessageBox.critical(self, "Error", f"Error saving metadata: {str(e)}")
|
@@ -0,0 +1,10 @@
|
|
1
|
+
name: napari-nifti-viewer
|
2
|
+
display_name: NIfTI Viewer
|
3
|
+
contributions:
|
4
|
+
commands:
|
5
|
+
- id: napari-nifti-viewer.make_widget
|
6
|
+
python_name: napari_nifti_viewer._widget:NiftiViewerWidget
|
7
|
+
title: Open NIfTI Viewer
|
8
|
+
widgets:
|
9
|
+
- command: napari-nifti-viewer.make_widget
|
10
|
+
display_name: NIfTI Viewer
|
@@ -0,0 +1,248 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: napari-nifti-viewer
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: A comprehensive napari plugin for NIfTI file analysis and visualization
|
5
|
+
Home-page: https://github.com/yohanchiu/napari-nifti-viewer
|
6
|
+
Author: Yohanchiu
|
7
|
+
Author-email: Your Name <your.email@example.com>
|
8
|
+
Maintainer-email: Your Name <your.email@example.com>
|
9
|
+
License: MIT
|
10
|
+
Project-URL: Homepage, https://github.com/yohanchiu/napari-nifti-viewer
|
11
|
+
Project-URL: Bug Tracker, https://github.com/yohanchiu/napari-nifti-viewer/issues
|
12
|
+
Project-URL: Documentation, https://github.com/yohanchiu/napari-nifti-viewer/wiki
|
13
|
+
Project-URL: Source Code, https://github.com/yohanchiu/napari-nifti-viewer
|
14
|
+
Project-URL: Changelog, https://github.com/yohanchiu/napari-nifti-viewer/blob/main/CHANGELOG.md
|
15
|
+
Keywords: napari,nifti,neuroimaging,medical imaging,visualization,plugin
|
16
|
+
Classifier: Development Status :: 4 - Beta
|
17
|
+
Classifier: Framework :: napari
|
18
|
+
Classifier: Intended Audience :: Science/Research
|
19
|
+
Classifier: Intended Audience :: Healthcare Industry
|
20
|
+
Classifier: License :: OSI Approved :: MIT License
|
21
|
+
Classifier: Operating System :: OS Independent
|
22
|
+
Classifier: Programming Language :: Python :: 3
|
23
|
+
Classifier: Programming Language :: Python :: 3.8
|
24
|
+
Classifier: Programming Language :: Python :: 3.9
|
25
|
+
Classifier: Programming Language :: Python :: 3.10
|
26
|
+
Classifier: Programming Language :: Python :: 3.11
|
27
|
+
Classifier: Programming Language :: Python :: 3.12
|
28
|
+
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
29
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
30
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
31
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
32
|
+
Requires-Python: >=3.8
|
33
|
+
Description-Content-Type: text/markdown
|
34
|
+
Requires-Dist: napari>=0.4.18
|
35
|
+
Requires-Dist: numpy>=1.21.0
|
36
|
+
Requires-Dist: nibabel>=5.2.1
|
37
|
+
Requires-Dist: qtpy>=2.0.0
|
38
|
+
Requires-Dist: magicgui>=0.7.0
|
39
|
+
Provides-Extra: dev
|
40
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
41
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
42
|
+
Requires-Dist: black; extra == "dev"
|
43
|
+
Requires-Dist: isort; extra == "dev"
|
44
|
+
Requires-Dist: flake8; extra == "dev"
|
45
|
+
Requires-Dist: pre-commit; extra == "dev"
|
46
|
+
Provides-Extra: test
|
47
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
48
|
+
Requires-Dist: pytest-cov; extra == "test"
|
49
|
+
Dynamic: author
|
50
|
+
Dynamic: home-page
|
51
|
+
Dynamic: requires-python
|
52
|
+
|
53
|
+
# napari-nifti-viewer
|
54
|
+
|
55
|
+
A powerful napari plugin for comprehensive NIfTI file analysis and visualization.
|
56
|
+
|
57
|
+
[](https://opensource.org/licenses/MIT)
|
58
|
+
[](https://www.python.org/downloads/)
|
59
|
+
[](https://napari-hub.org/plugins/napari-nifti-viewer)
|
60
|
+
|
61
|
+
## Overview
|
62
|
+
|
63
|
+
napari-nifti-viewer is a comprehensive napari plugin specifically designed for reading, analyzing, and visualizing NIfTI (.nii/.nii.gz) files. It provides detailed metadata extraction, intelligent label detection, and seamless integration with napari's visualization capabilities.
|
64
|
+
|
65
|
+
## Features
|
66
|
+
|
67
|
+
### 🔍 **Complete NIfTI Support**
|
68
|
+
- Read .nii and .nii.gz format files
|
69
|
+
- Support for NIfTI-1 standard
|
70
|
+
- Compatible with both image and label data
|
71
|
+
|
72
|
+
### 📊 **Comprehensive Metadata Analysis**
|
73
|
+
- Extract complete NIfTI header information (40+ fields)
|
74
|
+
- Display affine transformation matrices
|
75
|
+
- Show coordinate system information
|
76
|
+
- Analyze voxel spacing and orientation
|
77
|
+
|
78
|
+
### 🏷️ **Intelligent Label Detection**
|
79
|
+
- Automatic label image detection
|
80
|
+
- Statistical analysis of label distributions
|
81
|
+
- Label value counting and percentage calculations
|
82
|
+
|
83
|
+
### 📈 **Data Statistics**
|
84
|
+
- Complete data shape and type information
|
85
|
+
- Statistical measures (min, max, mean, std)
|
86
|
+
- Non-zero voxel counting
|
87
|
+
- Unique value analysis
|
88
|
+
|
89
|
+
### 💾 **Export Capabilities**
|
90
|
+
- Export complete metadata as JSON
|
91
|
+
- Preserve all numerical precision
|
92
|
+
- Human-readable format
|
93
|
+
|
94
|
+
### 🎨 **User-Friendly Interface**
|
95
|
+
- Clean, organized tabbed interface
|
96
|
+
- Real-time data loading
|
97
|
+
- Seamless napari integration
|
98
|
+
|
99
|
+
## Interface
|
100
|
+
|
101
|
+
The plugin provides a clean, organized interface with three main tabs:
|
102
|
+
|
103
|
+
### 📋 File Overview Tab
|
104
|
+
Displays basic file information and data statistics including file size, format, data shape, and statistical measures.
|
105
|
+
|
106
|
+
### 📊 Detailed Information Tab
|
107
|
+
Shows complete NIfTI header fields and metadata in an organized table format, alongside full JSON metadata export.
|
108
|
+
|
109
|
+
### 🏷️ Label Analysis Tab
|
110
|
+
Provides intelligent label detection and statistical analysis with automatic identification of label images and distribution analysis.
|
111
|
+
|
112
|
+
## Installation
|
113
|
+
|
114
|
+
### From PyPI (Recommended)
|
115
|
+
```bash
|
116
|
+
pip install napari-nifti-viewer
|
117
|
+
```
|
118
|
+
|
119
|
+
### From Source
|
120
|
+
```bash
|
121
|
+
git clone https://github.com/yohanchiu/napari-nifti-viewer.git
|
122
|
+
cd napari-nifti-viewer
|
123
|
+
pip install -e .
|
124
|
+
```
|
125
|
+
|
126
|
+
## Quick Start
|
127
|
+
|
128
|
+
1. **Launch napari** with the plugin installed
|
129
|
+
2. **Open the plugin** from the Plugins menu → napari-nifti-viewer
|
130
|
+
3. **Load a file** by clicking "Browse..." and selecting a .nii/.nii.gz file
|
131
|
+
4. **Explore the data** across three informative tabs:
|
132
|
+
- **File Overview**: Basic information and statistics
|
133
|
+
- **Detailed Info**: Complete NIfTI headers and metadata
|
134
|
+
- **Label Analysis**: Label detection and analysis
|
135
|
+
5. **Visualize in napari** by clicking "Load to Napari"
|
136
|
+
|
137
|
+
## Usage Examples
|
138
|
+
|
139
|
+
### Loading a Medical Image
|
140
|
+
```python
|
141
|
+
import napari
|
142
|
+
from napari_nifti_viewer import NiftiViewerWidget
|
143
|
+
|
144
|
+
# Create napari viewer
|
145
|
+
viewer = napari.Viewer()
|
146
|
+
|
147
|
+
# The plugin will be available in the Plugins menu
|
148
|
+
# Or you can add it programmatically:
|
149
|
+
widget = NiftiViewerWidget(viewer)
|
150
|
+
viewer.window.add_dock_widget(widget, name="NIfTI Viewer")
|
151
|
+
```
|
152
|
+
|
153
|
+
### Exporting Metadata
|
154
|
+
The plugin allows you to export complete metadata including:
|
155
|
+
- File information (size, format, version)
|
156
|
+
- NIfTI header fields (all 40+ standard fields)
|
157
|
+
- Data statistics (shape, type, value ranges)
|
158
|
+
- Coordinate system information
|
159
|
+
- Affine transformation matrices
|
160
|
+
|
161
|
+
## Requirements
|
162
|
+
|
163
|
+
- **napari** >= 0.4.18
|
164
|
+
- **nibabel** >= 5.2.1
|
165
|
+
- **numpy** >= 1.21.0
|
166
|
+
- **qtpy** >= 2.0.0
|
167
|
+
- **magicgui** >= 0.7.0
|
168
|
+
- **Python** >= 3.8
|
169
|
+
|
170
|
+
## Supported File Formats
|
171
|
+
|
172
|
+
- `.nii` - Uncompressed NIfTI files
|
173
|
+
- `.nii.gz` - Compressed NIfTI files
|
174
|
+
- Compatible with NIfTI-1 standard
|
175
|
+
- Support for both neuroimaging and medical imaging data
|
176
|
+
|
177
|
+
## Development
|
178
|
+
|
179
|
+
### Setting up Development Environment
|
180
|
+
|
181
|
+
```bash
|
182
|
+
# Clone the repository
|
183
|
+
git clone https://github.com/yohanchiu/napari-nifti-viewer.git
|
184
|
+
cd napari-nifti-viewer
|
185
|
+
|
186
|
+
# Create virtual environment
|
187
|
+
python -m venv venv
|
188
|
+
source venv/bin/activate # On Windows: venv\Scripts\activate
|
189
|
+
|
190
|
+
# Install in development mode
|
191
|
+
pip install -e ".[dev]"
|
192
|
+
|
193
|
+
# Run tests
|
194
|
+
python -m pytest
|
195
|
+
```
|
196
|
+
|
197
|
+
### Running Tests
|
198
|
+
|
199
|
+
```bash
|
200
|
+
# Basic functionality test
|
201
|
+
python test_plugin.py
|
202
|
+
|
203
|
+
# Test with napari interface
|
204
|
+
python test_plugin.py --napari
|
205
|
+
```
|
206
|
+
|
207
|
+
## Contributing
|
208
|
+
|
209
|
+
We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for details.
|
210
|
+
|
211
|
+
### Ways to Contribute
|
212
|
+
- 🐛 Report bugs
|
213
|
+
- 💡 Suggest new features
|
214
|
+
- 📝 Improve documentation
|
215
|
+
- 🔧 Submit pull requests
|
216
|
+
|
217
|
+
## License
|
218
|
+
|
219
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
220
|
+
|
221
|
+
## Citation
|
222
|
+
|
223
|
+
If you use this plugin in your research, please consider citing:
|
224
|
+
|
225
|
+
```bibtex
|
226
|
+
@software{napari_nifti_viewer,
|
227
|
+
title={napari-nifti-viewer: Comprehensive NIfTI Analysis for napari},
|
228
|
+
author={Qiu Yuheng},
|
229
|
+
year={2024},
|
230
|
+
url={https://github.com/yohanchiu/napari-nifti-viewer}
|
231
|
+
}
|
232
|
+
```
|
233
|
+
|
234
|
+
## Acknowledgments
|
235
|
+
|
236
|
+
- Built with [napari](https://napari.org/) - a fast, interactive, multi-dimensional image viewer
|
237
|
+
- Uses [nibabel](https://nipy.org/nibabel/) for NIfTI file handling
|
238
|
+
- Inspired by the neuroimaging and medical imaging communities
|
239
|
+
|
240
|
+
## Support
|
241
|
+
|
242
|
+
- 📖 [Documentation](https://github.com/yohanchiu/napari-nifti-viewer/wiki)
|
243
|
+
- 🐛 [Issue Tracker](https://github.com/yohanchiu/napari-nifti-viewer/issues)
|
244
|
+
- 💬 [Discussions](https://github.com/yohanchiu/napari-nifti-viewer/discussions)
|
245
|
+
|
246
|
+
---
|
247
|
+
|
248
|
+
Made with ❤️ for the napari and neuroimaging communities
|
@@ -0,0 +1,10 @@
|
|
1
|
+
napari_nifti_viewer/__init__.py,sha256=upx6vlxvZOUHRc9IU3EYeNRXeoO4EzJ8PVVcBo5lJIM,630
|
2
|
+
napari_nifti_viewer/_nifti_loader.py,sha256=iU6ABtYBnhgS_6Qkik4iN6kENTKJ2Z9l9pNAseavnmU,14233
|
3
|
+
napari_nifti_viewer/_version.py,sha256=P6PH3l7fy5t1OWdcXmQlxIPx-A6KH00qwtSk1yhMJXA,22
|
4
|
+
napari_nifti_viewer/_widget.py,sha256=O1V04RZ-6tMU2iKOLr2fOhlPkZ8TsOQsj7di2CQRtu8,17876
|
5
|
+
napari_nifti_viewer/napari.yaml,sha256=GFTP3qRNvs_TjMP9Oam3vehr-oHgVK-Wd8XgNGfUTCY,309
|
6
|
+
napari_nifti_viewer-0.1.0.dist-info/METADATA,sha256=zhRAqslWI_VB83YsPmMb8O3Iw35xcffWgEE85FU5h10,8037
|
7
|
+
napari_nifti_viewer-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
8
|
+
napari_nifti_viewer-0.1.0.dist-info/entry_points.txt,sha256=tgdov62uBBpxLcafWsh3E6-nj8y-dt-hK-dh8NyWkgM,72
|
9
|
+
napari_nifti_viewer-0.1.0.dist-info/top_level.txt,sha256=PDphr8Xssxi-YtMuvhhU_Lcd4lS6wrVu-_LEdOdnWCw,20
|
10
|
+
napari_nifti_viewer-0.1.0.dist-info/RECORD,,
|
@@ -0,0 +1 @@
|
|
1
|
+
napari_nifti_viewer
|