bids-mosaic 0.0.1__tar.gz

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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 - present Joseph Wexler
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: bids-mosaic
3
+ Version: 0.0.1
4
+ Summary: A BIDS tool for creating PDFs containing slices of nifti images
5
+ Author-email: Joseph Wexler <jbwexler@stanford.edu>
6
+ License-Expression: MIT
7
+ Project-URL: Source, https://github.com/jbwexler/bids-mosaic
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE.txt
15
+ Requires-Dist: numpy>=1.24.0
16
+ Requires-Dist: pybids>=0.16.0
17
+ Requires-Dist: nilearn>=0.10.0
18
+ Requires-Dist: nibabel>=5.0.0
19
+ Requires-Dist: reportlab>=3.6.12
20
+ Requires-Dist: pillow>=9.3.0
21
+ Requires-Dist: matplotlib>=3.7.0
22
+ Dynamic: license-file
23
+
24
+ ## BIDS-Mosaic
25
+ A BIDS tool for creating PDFs containing slices of nifti images
26
+
27
+ ## Installation
28
+ ```
29
+ pip install bidsmosaic
30
+ ```
31
+
32
+ ## Usage
33
+ ```
34
+ bids-mosiac <bids_dataset_directory>
35
+ ```
36
+
37
+ For more options:
38
+ ```
39
+ bids-mosaic --help
40
+ ```
41
+
42
+ ## Example
43
+ ![alt text](visuals/example.png)
44
+
45
+ ## License
46
+ BIDS-Mosaic is licensed under [MIT license](LICENSE.txt).
@@ -0,0 +1,23 @@
1
+ ## BIDS-Mosaic
2
+ A BIDS tool for creating PDFs containing slices of nifti images
3
+
4
+ ## Installation
5
+ ```
6
+ pip install bidsmosaic
7
+ ```
8
+
9
+ ## Usage
10
+ ```
11
+ bids-mosiac <bids_dataset_directory>
12
+ ```
13
+
14
+ For more options:
15
+ ```
16
+ bids-mosaic --help
17
+ ```
18
+
19
+ ## Example
20
+ ![alt text](visuals/example.png)
21
+
22
+ ## License
23
+ BIDS-Mosaic is licensed under [MIT license](LICENSE.txt).
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: bids-mosaic
3
+ Version: 0.0.1
4
+ Summary: A BIDS tool for creating PDFs containing slices of nifti images
5
+ Author-email: Joseph Wexler <jbwexler@stanford.edu>
6
+ License-Expression: MIT
7
+ Project-URL: Source, https://github.com/jbwexler/bids-mosaic
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE.txt
15
+ Requires-Dist: numpy>=1.24.0
16
+ Requires-Dist: pybids>=0.16.0
17
+ Requires-Dist: nilearn>=0.10.0
18
+ Requires-Dist: nibabel>=5.0.0
19
+ Requires-Dist: reportlab>=3.6.12
20
+ Requires-Dist: pillow>=9.3.0
21
+ Requires-Dist: matplotlib>=3.7.0
22
+ Dynamic: license-file
23
+
24
+ ## BIDS-Mosaic
25
+ A BIDS tool for creating PDFs containing slices of nifti images
26
+
27
+ ## Installation
28
+ ```
29
+ pip install bidsmosaic
30
+ ```
31
+
32
+ ## Usage
33
+ ```
34
+ bids-mosiac <bids_dataset_directory>
35
+ ```
36
+
37
+ For more options:
38
+ ```
39
+ bids-mosaic --help
40
+ ```
41
+
42
+ ## Example
43
+ ![alt text](visuals/example.png)
44
+
45
+ ## License
46
+ BIDS-Mosaic is licensed under [MIT license](LICENSE.txt).
@@ -0,0 +1,11 @@
1
+ LICENSE.txt
2
+ README.md
3
+ pyproject.toml
4
+ bids_mosaic.egg-info/PKG-INFO
5
+ bids_mosaic.egg-info/SOURCES.txt
6
+ bids_mosaic.egg-info/dependency_links.txt
7
+ bids_mosaic.egg-info/entry_points.txt
8
+ bids_mosaic.egg-info/requires.txt
9
+ bids_mosaic.egg-info/top_level.txt
10
+ bidsmosaic/__init__.py
11
+ bidsmosaic/mosaic.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ bids-mosaic = bidsmosaic.mosaic:main
@@ -0,0 +1,7 @@
1
+ numpy>=1.24.0
2
+ pybids>=0.16.0
3
+ nilearn>=0.10.0
4
+ nibabel>=5.0.0
5
+ reportlab>=3.6.12
6
+ pillow>=9.3.0
7
+ matplotlib>=3.7.0
@@ -0,0 +1 @@
1
+ bidsmosaic
File without changes
@@ -0,0 +1,365 @@
1
+ import numpy as np
2
+ import argparse
3
+ import os.path
4
+ import glob
5
+ import tempfile
6
+ import json
7
+ import logging
8
+ import PIL.Image
9
+ from bids import BIDSLayout
10
+ from nilearn.plotting import plot_img
11
+ import matplotlib.pyplot as plt
12
+ import nibabel as nb
13
+ from reportlab.platypus import (
14
+ Paragraph,
15
+ Image,
16
+ Table,
17
+ SimpleDocTemplate,
18
+ PageBreak,
19
+ TableStyle,
20
+ Spacer,
21
+ )
22
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
23
+ from reportlab.lib import colors
24
+
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ def enhance_brightness(
30
+ img: PIL.Image, target_brightness=100, threshold=10
31
+ ) -> PIL.Image:
32
+ """Attempts to change the brightness of an image to the target brightness."""
33
+ arr = np.array(img, dtype="float32")
34
+ mask = arr > threshold
35
+ rms = np.sqrt(np.mean(np.square(arr[mask])))
36
+ brightness_factor = target_brightness / rms
37
+ arr[mask] *= brightness_factor
38
+ return PIL.Image.fromarray(arr).convert("L")
39
+
40
+
41
+ def create_slice_img(
42
+ img_path: str,
43
+ out_dir: str,
44
+ ds_path: str,
45
+ display_mode="x",
46
+ cut_coords=np.array([0]),
47
+ colorbar=False,
48
+ ds_root=None,
49
+ downsample=None,
50
+ ) -> None:
51
+ """Creates a png of a slice(s) of a nifti. Defaults to a single midline
52
+ sagittal slice."""
53
+
54
+ logger.debug(f"Creating png from {img_path}")
55
+ try:
56
+ img = nb.load(img_path)
57
+ except FileNotFoundError:
58
+ logger.warning("%s was not found." % img_path)
59
+ return
60
+
61
+ if ds_root:
62
+ relpath = os.path.relpath(img_path, ds_path)
63
+ out_file = relpath.replace("/", ":") + ".png"
64
+ else:
65
+ out_file = os.path.basename(img_path) + ".png"
66
+
67
+ out_path = os.path.join(out_dir, out_file)
68
+
69
+ plot_img(
70
+ img,
71
+ display_mode=display_mode,
72
+ cut_coords=cut_coords,
73
+ colorbar=colorbar,
74
+ annotate=False,
75
+ )
76
+ plt.savefig(out_path, transparent=True)
77
+ plt.close()
78
+
79
+ # Remove transparent margins
80
+ png = PIL.Image.open(out_path)
81
+ new_png = png.crop(png.getbbox()).convert("L")
82
+
83
+ if downsample:
84
+ height, width = new_png.size
85
+ new_size = (round(height / downsample), round(width / downsample))
86
+ new_png = new_png.resize(new_size)
87
+
88
+ new_png = enhance_brightness(new_png)
89
+
90
+ new_png.save(out_path)
91
+
92
+
93
+ def create_sized_img(img_path: str, new_height: int) -> Image:
94
+ """Creates a reportlab Image from a .png. Resizes using new_height
95
+ and calculating new_width to maintain aspect ratio."""
96
+ img = PIL.Image.open(img_path)
97
+ width, height = img.size
98
+ new_width = (new_height / height) * width
99
+ return Image(img_path, height=new_height, width=new_width)
100
+
101
+
102
+ def create_filename_caption(img_path: str) -> Paragraph:
103
+ """Creates a reportlab Paragraph containing the filename of the image."""
104
+ caption_text = os.path.basename(img_path)
105
+ caption_text = caption_text.replace(":", "/")
106
+ caption_text = os.path.relpath(caption_text)
107
+ caption_text = caption_text.removesuffix(".png")
108
+ return caption_text
109
+
110
+
111
+ def create_mosaic_table(img_dir_path: str, page_width: int, styles) -> Table:
112
+ """Creates reportlab Table of slice images with image-name captions."""
113
+ img_height = 80
114
+ caption_style = ParagraphStyle(
115
+ "Caption",
116
+ parent=styles["Normal"],
117
+ fontSize=6,
118
+ leading=6,
119
+ textColor=colors.black,
120
+ alignment="CENTER",
121
+ leftIndent=0,
122
+ rightIndent=0,
123
+ spaceAfter=0,
124
+ spaceBefore=0,
125
+ )
126
+
127
+ image_path_list = sorted(glob.glob(img_dir_path + "/*"))
128
+
129
+ if not image_path_list:
130
+ logger.error(f"No images found in {img_dir_path}")
131
+ return
132
+
133
+ table_data = [
134
+ [
135
+ create_sized_img(img_path, img_height),
136
+ Paragraph(
137
+ f"<para align=center spaceb=3>{create_filename_caption(img_path)}</para>",
138
+ caption_style,
139
+ ),
140
+ ]
141
+ for img_path in image_path_list
142
+ ]
143
+ img_width = table_data[0][0]._width
144
+ num_col = int(page_width / img_width)
145
+ col_width = int(page_width / num_col)
146
+
147
+ table_data_rows = [
148
+ table_data[i : i + num_col] for i in range(0, len(image_path_list), num_col)
149
+ ]
150
+
151
+ table_style = TableStyle(
152
+ [
153
+ ("ALIGN", (0, 0), (-1, -1), "CENTER"),
154
+ ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
155
+ ("LEFTPADDING", (0, 0), (-1, -1), 2),
156
+ ("RIGHTPADDING", (0, 0), (-1, -1), 2),
157
+ ("TOPPADDING", (0, 0), (-1, -1), 2),
158
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 5),
159
+ ("INNERGRID", (0, 0), (-1, -1), 0.25, colors.grey),
160
+ ("BOX", (0, 0), (-1, -1), 0.25, colors.black),
161
+ ]
162
+ )
163
+ table = Table(table_data_rows, colWidths=col_width)
164
+ table.setStyle(table_style)
165
+
166
+ return table
167
+
168
+
169
+ def create_metadata_table(metadata: str) -> Table:
170
+ """Creates a reportlab Table containing user-inputted metadata."""
171
+ metadata_dict = json.loads(metadata)
172
+ metadata_list = list(metadata_dict.items())
173
+
174
+ table_style = TableStyle(
175
+ [
176
+ ("ALIGN", (0, 0), (-1, -1), "CENTER"),
177
+ ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
178
+ ("LEFTPADDING", (0, 0), (-1, -1), 5),
179
+ ("RIGHTPADDING", (0, 0), (-1, -1), 5),
180
+ ("TOPPADDING", (0, 0), (-1, -1), 5),
181
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 5),
182
+ ("INNERGRID", (0, 0), (-1, -1), 0.25, colors.grey),
183
+ ("BOX", (0, 0), (-1, -1), 0.25, colors.black),
184
+ ]
185
+ )
186
+
187
+ table = Table(metadata_list)
188
+ table.setStyle(table_style)
189
+
190
+ return table
191
+
192
+
193
+ def create_pdf(img_dir_path: str, out_path: str, metadata=None) -> None:
194
+ """Creates a pdf containing images aligned in a grid"""
195
+ styles = getSampleStyleSheet()
196
+ pdf = SimpleDocTemplate(
197
+ out_path,
198
+ leftMargin=18,
199
+ rightMargin=18,
200
+ topMargin=36,
201
+ bottomMargin=36,
202
+ )
203
+ page_width = int(pdf.width)
204
+
205
+ flowables = []
206
+
207
+ for d in glob.glob(os.path.join(img_dir_path, "*")):
208
+ title_text = os.path.basename(d) + " Images"
209
+ title = Paragraph(title_text, styles["Title"])
210
+ flowables.append(title)
211
+ flowables.append(Spacer(0, 15))
212
+
213
+ mosaic_table = create_mosaic_table(d, page_width, styles)
214
+ flowables.append(mosaic_table)
215
+
216
+ flowables.append(PageBreak())
217
+
218
+ if metadata:
219
+ title = Paragraph("Metadata", styles["Title"])
220
+ flowables.append(title)
221
+ flowables.append(Spacer(0, 15))
222
+
223
+ metadata_table = create_metadata_table(metadata)
224
+ flowables.append(metadata_table)
225
+
226
+ pdf.build(flowables)
227
+
228
+ logger.info("Successfully created pdf")
229
+
230
+
231
+ def create_anat_images(layout: BIDSLayout, png_dir: str, downsample=None) -> None:
232
+ """Creates anatomical mosaic .png files."""
233
+ anat_layout_kwargs = {
234
+ "datatype": "anat",
235
+ "extension": ["nii", "nii.gz"],
236
+ }
237
+
238
+ files = layout.get(**anat_layout_kwargs)
239
+ anat_png_dir = os.path.join(png_dir, "Anatomical")
240
+ os.makedirs(anat_png_dir, exist_ok=True)
241
+
242
+ for file in files:
243
+ create_slice_img(file.path, anat_png_dir, layout.root, downsample=downsample)
244
+
245
+
246
+ def create_fs_images(fs_dir: str, png_dir: str, downsample=None) -> None:
247
+ """Creates freesurfer mosaic .png files."""
248
+ fs_png_dir = os.path.join(png_dir, "Freesurfer")
249
+ os.makedirs(fs_png_dir, exist_ok=True)
250
+
251
+ for file_path in glob.glob(os.path.join(fs_dir, "sub-*/mri/orig/*")):
252
+ create_slice_img(
253
+ file_path, fs_png_dir, fs_dir, ds_root=fs_dir, downsample=downsample
254
+ )
255
+
256
+
257
+ def create_mosaic_pdf(
258
+ dataset: str,
259
+ out_file: str,
260
+ anat=True,
261
+ png_out_dir=None,
262
+ downsample=None,
263
+ freesurfer=None,
264
+ metadata=None,
265
+ ) -> None:
266
+ """Creates a mosaic pdf."""
267
+ if png_out_dir:
268
+ png_dir = png_out_dir
269
+ else:
270
+ temp_dir_obj = tempfile.TemporaryDirectory()
271
+ png_dir = temp_dir_obj.name
272
+
273
+ layout = BIDSLayout(dataset, validate=False)
274
+
275
+ if anat:
276
+ logger.info(f"Creating anat images in {png_dir}")
277
+ create_anat_images(layout, png_dir, downsample=downsample)
278
+ if freesurfer:
279
+ logger.info(f"Creating freesurfer images in {png_dir}")
280
+ create_fs_images(freesurfer, png_dir, downsample=downsample)
281
+
282
+ logger.info(f"Creating pdf at {out_file}")
283
+ create_pdf(png_dir, out_file, metadata)
284
+
285
+ if not png_out_dir:
286
+ temp_dir_obj.cleanup()
287
+
288
+
289
+ def main():
290
+ logging.basicConfig(level=logging.INFO)
291
+
292
+ parser = argparse.ArgumentParser()
293
+ parser.add_argument("dataset", type=str, help="Path to dataset")
294
+ parser.add_argument(
295
+ "-o",
296
+ "--out-file",
297
+ type=str,
298
+ help="Path to output pdf. Defaults to <input dir name>_mosaics.pdf in working directory.",
299
+ )
300
+ parser.add_argument(
301
+ "--png-in-dir",
302
+ type=str,
303
+ help="Path to existing directory of .png files, bypassing creation of those from .nii files.",
304
+ )
305
+ parser.add_argument(
306
+ "--png-out-dir",
307
+ type=str,
308
+ help="Path to directory to output .png slice images too, instead of creating a temp directory.",
309
+ )
310
+ parser.add_argument(
311
+ "-m",
312
+ "--metadata",
313
+ type=str,
314
+ help="JSON string to include as metadata at the end of the output file.",
315
+ )
316
+ parser.add_argument(
317
+ "--no-anat",
318
+ action="store_false",
319
+ dest="anat",
320
+ help="Do not include anatomical images.",
321
+ )
322
+ parser.add_argument(
323
+ "--freesurfer",
324
+ type=str,
325
+ help="Path to freesurfer data.",
326
+ )
327
+ parser.add_argument(
328
+ "--downsample",
329
+ type=int,
330
+ help="Factor by which to downsample images.",
331
+ )
332
+ parser.add_argument(
333
+ "--debug",
334
+ action="store_true",
335
+ help="Set logging level to DEBUG.",
336
+ )
337
+
338
+ args = parser.parse_args()
339
+
340
+ if args.debug:
341
+ logger.setLevel(logging.DEBUG)
342
+
343
+ if args.out_file:
344
+ out_file = args.out_file
345
+ else:
346
+ in_abs = os.path.abspath(args.dataset)
347
+ out_file = os.path.basename(in_abs) + "_mosaic.pdf"
348
+
349
+ if not args.png_in_dir:
350
+ create_mosaic_pdf(
351
+ args.dataset,
352
+ out_file,
353
+ anat=args.anat,
354
+ png_out_dir=args.png_out_dir,
355
+ downsample=args.downsample,
356
+ freesurfer=args.freesurfer,
357
+ metadata=args.metadata,
358
+ )
359
+ else:
360
+ logger.info(f"Creating pdf at {out_file}")
361
+ create_pdf(args.png_in_dir, out_file, args.metadata)
362
+
363
+
364
+ if __name__ == "__main__":
365
+ main()
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "bids-mosaic"
7
+ description = "A BIDS tool for creating PDFs containing slices of nifti images"
8
+ readme = "README.md"
9
+ license = "MIT"
10
+ authors = [
11
+ { name = "Joseph Wexler", email = "jbwexler@stanford.edu" },
12
+ ]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Operating System :: OS Independent",
16
+ "Programming Language :: Python",
17
+ "Programming Language :: Python :: 3",
18
+ ]
19
+ version = "0.0.1"
20
+ requires-python = ">=3.10"
21
+ dependencies = [
22
+ "numpy>=1.24.0",
23
+ "pybids>=0.16.0",
24
+ "nilearn>=0.10.0",
25
+ "nibabel>=5.0.0",
26
+ "reportlab>=3.6.12",
27
+ "pillow>=9.3.0",
28
+ "matplotlib>=3.7.0"
29
+ ]
30
+
31
+ [project.urls]
32
+ Source = "https://github.com/jbwexler/bids-mosaic"
33
+
34
+ [project.scripts]
35
+ bids-mosaic = "bidsmosaic.mosaic:main"
36
+
37
+ [tool.setuptools]
38
+ packages = ["bidsmosaic"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+