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.
@@ -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
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
58
+ [![Python](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
59
+ [![napari hub](https://img.shields.io/endpoint?url=https://api.napari-hub.org/shields/napari-nifti-viewer)](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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [napari.manifest]
2
+ napari-nifti-viewer = napari_nifti_viewer:napari.yaml
@@ -0,0 +1 @@
1
+ napari_nifti_viewer