miblab-plot 0.0.1__py3-none-any.whl → 0.0.3__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.
miblab_plot/image_3d.py CHANGED
@@ -1,9 +1,6 @@
1
1
  from math import ceil
2
2
 
3
3
  from PIL import Image
4
- import numpy as np
5
-
6
-
7
4
  import numpy as np
8
5
  import matplotlib
9
6
  matplotlib.use('Agg')
@@ -12,31 +9,162 @@ from tqdm import tqdm
12
9
 
13
10
 
14
11
 
15
- def get_distinct_colors(rois, colormap='jet'):
12
+ def get_distinct_colors(rois, colormap='jet', opacity=0.6):
16
13
  if len(rois)==1:
17
- colors = [[1, 0, 0, 0.6]]
14
+ colors = [[1, 0, 0, opacity]]
18
15
  elif len(rois)==2:
19
- colors = [[1, 0, 0, 0.6], [0, 1, 0, 0.6]]
16
+ colors = [[1, 0, 0, opacity], [0, 1, 0, opacity]]
20
17
  elif len(rois)==3:
21
- colors = [[1, 0, 0, 0.6], [0, 1, 0, 0.6], [0, 0, 1, 0.6]]
18
+ colors = [[1, 0, 0, opacity], [0, 1, 0, opacity], [0, 0, 1, opacity]]
22
19
  else:
23
20
  n = len(rois)
24
21
  #cmap = cm.get_cmap(colormap, n)
25
22
  cmap = matplotlib.colormaps[colormap]
26
- colors = [cmap(i)[:3] + (0.6,) for i in np.linspace(0, 1, n)] # Set alpha to 0.6 for transparency
23
+ colors = [cmap(i)[:3] + (opacity,) for i in np.linspace(0, 1, n)]
27
24
 
28
25
  return colors
29
26
 
30
27
 
28
+ def mosaic_checkerboard(fixed, coreg, file, normalize=False, square_size=32, aspect_ratio=1.8, vmin=None, vmax=None):
29
+
30
+ fixed = np.array(fixed)
31
+ coreg = np.array(coreg)
32
+
33
+ if fixed.shape != coreg.shape:
34
+ raise ValueError("Input arrays must have the same shape")
35
+
36
+ if normalize:
37
+ fixed = _normalize(fixed)
38
+ coreg = _normalize(coreg)
39
+ # fixed = (fixed - fixed.min()) / (fixed.max() - fixed.min())
40
+ # coreg = (coreg - coreg.min()) / (coreg.max() - coreg.min())
41
+
42
+ # Set defaults color window
43
+ if vmin is None:
44
+ vmin=np.percentile(np.concatenate([fixed, coreg]), 5)
45
+ if vmax is None:
46
+ vmax=np.percentile(np.concatenate([fixed, coreg]), 95)
47
+
48
+ # Determine number of rows and columns
49
+ # c*r = n -> c=n/r
50
+ # c*w / r*h = a -> w*n/r = a*r*h -> (w*n) / (a*h) = r**2
51
+ width = fixed.shape[0]
52
+ height = fixed.shape[1]
53
+ n_mosaics = fixed.shape[2]
54
+ nrows = int(np.round(np.sqrt((width*n_mosaics)/(aspect_ratio*height))))
55
+ ncols = int(np.ceil(n_mosaics/nrows))
56
+
57
+ # Set up figure
58
+ fig, ax = plt.subplots(
59
+ nrows=nrows,
60
+ ncols=ncols,
61
+ gridspec_kw = {'wspace':0, 'hspace':0},
62
+ figsize=(ncols*width/max([width,height]), nrows*height/max([width,height])),
63
+ dpi=300,
64
+ )
65
+ plt.subplots_adjust(left=0, right=1, top=1, bottom=0)
66
+
67
+ # Build figure
68
+ i = 0
69
+ for row in tqdm(ax, desc='Building png'):
70
+ for col in row:
71
+
72
+ col.set_xticklabels([])
73
+ col.set_yticklabels([])
74
+ col.set_aspect('equal')
75
+ col.axis("off")
76
+
77
+ # Display the background image
78
+ if i < n_mosaics:
79
+ img = checkerboard(fixed[:,:,i], coreg[:,:,i], square_size)
80
+ col.imshow(
81
+ img.T,
82
+ cmap='gray',
83
+ interpolation='none',
84
+ vmin=vmin,
85
+ vmax=vmax,
86
+ )
87
+
88
+ i += 1
89
+
90
+ # fig.suptitle('Mask overlay', fontsize=14)
91
+ fig.savefig(file, bbox_inches='tight', pad_inches=0)
92
+ plt.close()
93
+
94
+
95
+
96
+ def _normalize(x: np.ndarray) -> np.ndarray:
97
+ """
98
+ Normalize an array so that median = 0 and IQR = 1.
99
+
100
+ Parameters
101
+ ----------
102
+ x : np.ndarray
103
+ Input array (any shape)
104
+
105
+ Returns
106
+ -------
107
+ np.ndarray
108
+ Normalized array
109
+ """
110
+ x = np.asarray(x, dtype=float)
111
+
112
+ q25, q50, q75 = np.percentile(x, [25, 50, 75])
113
+ iqr = q75 - q25
114
+
115
+ if iqr == 0:
116
+ raise ValueError("IQR is zero; cannot normalize")
117
+
118
+ return (x - q50) / iqr
119
+
120
+
121
+
122
+ def checkerboard(fixed: np.ndarray, coreg: np.ndarray, square_size: int) -> np.ndarray:
123
+ """
124
+ Creates a checkerboard pattern from two 2D arrays.
125
+
126
+ Parameters
127
+ ----------
128
+ fixed : np.ndarray
129
+ The first 2D array (e.g., fixed image)
130
+ coreg : np.ndarray
131
+ The second 2D array (e.g., coregistered image)
132
+ Must have the same shape as `fixed`.
133
+ square_size : int
134
+ The size of each checkerboard square in pixels.
135
+
136
+ Returns
137
+ -------
138
+ np.ndarray
139
+ Checkerboard array combining `fixed` and `coreg`.
140
+ """
141
+ if fixed.shape != coreg.shape:
142
+ raise ValueError("Input arrays must have the same shape")
143
+
144
+ rows, cols = fixed.shape
145
+
146
+ # Create a boolean checkerboard mask
147
+ row_blocks = np.arange(rows) // square_size
148
+ col_blocks = np.arange(cols) // square_size
149
+ mask = (row_blocks[:, None] + col_blocks[None, :]) % 2 == 0 # True = use fixed
150
+
151
+ # Build the checkerboard
152
+ checker = np.where(mask, fixed, coreg)
153
+
154
+ return checker
155
+
156
+
157
+
31
158
  def mosaic_overlay(
32
159
  img,
33
160
  rois,
34
161
  file,
35
162
  colormap='tab20',
36
- aspect_ratio=16/9,
163
+ aspect_ratio=1.8,
37
164
  margin=None,
38
165
  vmin=None,
39
166
  vmax=None,
167
+ opacity=0.6,
40
168
  ):
41
169
 
42
170
  # Set defaults color window
@@ -46,7 +174,7 @@ def mosaic_overlay(
46
174
  vmax=np.mean(img) + 2 * np.std(img)
47
175
 
48
176
  # Define RGBA colors (R, G, B, Alpha) — alpha controls transparency
49
- colors = get_distinct_colors(rois, colormap=colormap)
177
+ colors = get_distinct_colors(rois, colormap=colormap, opacity=opacity)
50
178
 
51
179
  # Get all masks as boolean arrays
52
180
  masks = [m.astype(bool) for m in rois.values()]
miblab_plot/movie.py ADDED
@@ -0,0 +1,79 @@
1
+ import os
2
+ import shutil
3
+
4
+ import imageio.v2 as imageio # Use v2 interface for compatibility
5
+ from moviepy import VideoFileClip
6
+ import matplotlib
7
+ import numpy as np
8
+ import matplotlib.pyplot as plt
9
+ from tqdm import tqdm
10
+
11
+
12
+
13
+ def get_distinct_colors(rois, colormap='jet'):
14
+ if len(rois)==1:
15
+ colors = [[255, 0, 0, 0.6]]
16
+ elif len(rois)==2:
17
+ colors = [[255, 0, 0, 0.6], [0, 255, 0, 0.6]]
18
+ elif len(rois)==3:
19
+ colors = [[255, 0, 0, 0.6], [0, 255, 0, 0.6], [0, 0, 255, 0.6]]
20
+ else:
21
+ n = len(rois)
22
+ #cmap = cm.get_cmap(colormap, n)
23
+ cmap = matplotlib.colormaps[colormap]
24
+ colors = [cmap(i)[:3] + (0.6,) for i in np.linspace(0, 1, n)] # Set alpha to 0.6 for transparency
25
+
26
+ return colors
27
+
28
+
29
+ def movie_overlay(img, rois, file):
30
+
31
+ # Define RGBA colors (R, G, B, Alpha) — alpha controls transparency
32
+ colors = get_distinct_colors(rois, colormap='tab20')
33
+
34
+ # Directory to store temporary frames
35
+ tmp = os.path.join(os.getcwd(), 'tmp')
36
+ os.makedirs(tmp, exist_ok=True)
37
+ filenames = []
38
+
39
+ # Generate and save a sequence of plots
40
+ for i in tqdm(range(img.shape[2]), desc='Building animation..'):
41
+
42
+ # Set up figure
43
+ fig, ax = plt.subplots(
44
+ figsize=(5, 5),
45
+ dpi=300,
46
+ )
47
+
48
+ # Display the background image
49
+ ax.imshow(img[:,:,i].T, cmap='gray', interpolation='none', vmin=0, vmax=np.mean(img) + 2 * np.std(img))
50
+
51
+ # Overlay each mask
52
+ for mask, color in zip([m.astype(bool) for m in rois.values()], colors):
53
+ rgba = np.zeros((img.shape[0], img.shape[1], 4), dtype=float)
54
+ for c in range(4): # RGBA
55
+ rgba[..., c] = mask[:,:,i] * color[c]
56
+ ax.imshow(rgba.transpose((1,0,2)), interpolation='none')
57
+
58
+ # Save eachg image to a tmp file
59
+ fname = os.path.join(tmp, f'frame_{i}.png')
60
+ fig.savefig(fname)
61
+ filenames.append(fname)
62
+ plt.close(fig)
63
+
64
+ # Create GIF
65
+ print('Creating movie')
66
+ gif = os.path.join(tmp, 'movie.gif')
67
+ with imageio.get_writer(gif, mode="I", duration=0.2) as writer:
68
+ for fname in filenames:
69
+ image = imageio.imread(fname)
70
+ writer.append_data(image)
71
+
72
+ # Load gif
73
+ clip = VideoFileClip(gif)
74
+
75
+ # Save as MP4
76
+ clip.write_videofile(file, codec='libx264')
77
+
78
+ # Clean up temporary files
79
+ shutil.rmtree(tmp)
miblab_plot/mp4.py ADDED
@@ -0,0 +1,83 @@
1
+ import os
2
+
3
+ from moviepy.video.io.ImageSequenceClip import ImageSequenceClip # NEW (v2.x)
4
+
5
+ def images_to_video(image_folder, output_file, fps=30):
6
+ # 1. Get and sort images
7
+ images = [os.path.join(image_folder, img)
8
+ for img in os.listdir(image_folder)
9
+ if img.endswith(".png")]
10
+ images.sort()
11
+
12
+ if not images:
13
+ print("No images found!")
14
+ return
15
+
16
+ # 2. Create the clip
17
+ clip = ImageSequenceClip(images, fps=fps)
18
+
19
+ # 3. Write to MP4 (Windows compatible)
20
+ # The 'logger=None' argument suppresses the progress bar if you want cleaner output
21
+ clip.write_videofile(output_file, codec='libx264', bitrate="50000k")
22
+
23
+ # Usage
24
+ # save_high_quality_mp4('your/image/folder', 'video.mp4')
25
+
26
+ # Usage
27
+ # create_lossless_video('your_folder', 'high_quality.mp4')
28
+
29
+ # Usage
30
+ # save_high_quality_mp4('my_folder', 'final_video.mp4')
31
+
32
+ # def _images_to_video(image_folder, output_video_file, fps=30):
33
+ # """
34
+ # Converts a folder of PNG images into a video file.
35
+
36
+ # Args:
37
+ # image_folder (str): Path to the folder containing images.
38
+ # output_video_file (str): Output filename (e.g., 'output.mp4').
39
+ # fps (int): Frames per second.
40
+ # """
41
+
42
+ # # 1. Get the list of files
43
+ # images = [img for img in os.listdir(image_folder) if img.endswith(".png")]
44
+
45
+ # # 2. Sort the images to ensure they are in the correct order
46
+ # # Note: This uses standard string sorting. If your files are named 1.png, 10.png, 2.png,
47
+ # # you might need "natural sorting" logic.
48
+ # images.sort()
49
+
50
+ # if not images:
51
+ # print("No PNG images found in the directory.")
52
+ # return
53
+
54
+ # # 3. Read the first image to determine width and height
55
+ # frame = cv2.imread(os.path.join(image_folder, images[0]))
56
+ # height, width, layers = frame.shape
57
+ # size = (width, height)
58
+
59
+ # # 4. Define the codec and create VideoWriter object
60
+ # # 'mp4v' is a standard codec for MP4 containers
61
+ # # fourcc = cv2.VideoWriter_fourcc(*'mp4v')
62
+ # fourcc = cv2.VideoWriter_fourcc(*'MJPG') # Motion JPEG codec
63
+ # out = cv2.VideoWriter(output_video_file, fourcc, fps, size)
64
+
65
+
66
+ # print(f"Processing {len(images)} images...")
67
+
68
+ # # 5. Write images to video
69
+ # for image in images:
70
+ # img_path = os.path.join(image_folder, image)
71
+ # frame = cv2.imread(img_path)
72
+
73
+ # # specific check: resizing might be needed if images vary in size
74
+ # # frame = cv2.resize(frame, size)
75
+
76
+ # out.write(frame)
77
+
78
+ # # 6. Release everything
79
+ # out.release()
80
+ # print(f"Video saved as {output_video_file}")
81
+
82
+ # # --- Usage Example ---
83
+ # # images_to_video('path/to/your/images', 'my_animation.mp4', fps=24)
miblab_plot/pvplot.py ADDED
@@ -0,0 +1,364 @@
1
+ import os
2
+ from pathlib import Path
3
+ from typing import Union
4
+
5
+ import numpy as np
6
+ from tqdm import tqdm
7
+ import pyvista as pv
8
+ import dbdicom as db
9
+ import zarr
10
+
11
+ import miblab_ssa as ssa
12
+
13
+
14
+ def mosaic_masks_dcm(masks, imagefile, labels=None, view_vector=(1, 0, 0)):
15
+
16
+ # Plot settings
17
+ aspect_ratio = 16/9
18
+ width = 150
19
+ height = 150
20
+
21
+ # Count nr of mosaics
22
+ n_mosaics = len(masks)
23
+ nrows = int(np.ceil(np.sqrt((width*n_mosaics)/(aspect_ratio*height))))
24
+ ncols = int(np.ceil(n_mosaics/nrows))
25
+
26
+ plotter = pv.Plotter(window_size=(ncols*width, nrows*height), shape=(nrows, ncols), border=False, off_screen=True)
27
+ plotter.background_color = 'white'
28
+
29
+ row = 0
30
+ col = 0
31
+ for i, mask_series in tqdm(enumerate(masks), desc=f'Building mosaic'):
32
+
33
+ # Set up plotter
34
+ plotter.subplot(row,col)
35
+ if labels is not None:
36
+ plotter.add_text(labels[i], font_size=6)
37
+ if col == ncols-1:
38
+ col = 0
39
+ row += 1
40
+ else:
41
+ col += 1
42
+
43
+ # Load data
44
+ vol = db.volume(mask_series, verbose=0)
45
+ mask_norm = ssa.sdf_ft.smooth_mask(vol.values, order=32)
46
+
47
+ # Plot tile
48
+ orig_vol = pv.wrap(mask_norm.astype(float))
49
+ orig_vol.spacing = vol.spacing
50
+ orig_surface = orig_vol.contour(isosurfaces=[0.5])
51
+ plotter.add_mesh(orig_surface, color='lightblue', opacity=1.0, style='surface')
52
+ plotter.camera_position = 'iso'
53
+ plotter.view_vector(view_vector) # rotate 180° around vertical axis
54
+
55
+ plotter.screenshot(imagefile)
56
+ plotter.close()
57
+
58
+
59
+ def rotating_masks_grid(
60
+ dir_output:str,
61
+ masks:Union[zarr.Array, np.ndarray],
62
+ labels:np.ndarray=None,
63
+ nviews=25,
64
+ ):
65
+ # masks: (cols, rows) + 3d shape
66
+ # labels: (cols, rows)
67
+ # Plot settings
68
+ width = 150
69
+ height = 150
70
+
71
+ # Define view points
72
+ angles = np.linspace(0, 2*np.pi, nviews)
73
+ dirs = [(np.cos(a), np.sin(a), 0.0) for a in angles] # rotate around z
74
+ dirs += [(np.cos(a), 0.0, np.sin(a)) for a in angles] # rotate around y
75
+
76
+ # Count nr of mosaics
77
+ ncols = masks.shape[0]
78
+ nrows = masks.shape[1]
79
+
80
+ plotters = {}
81
+ for i, vec in enumerate(dirs):
82
+ plotters[i] = pv.Plotter(
83
+ window_size=(ncols*width, nrows*height),
84
+ shape=(nrows, ncols),
85
+ border=False,
86
+ off_screen=True,
87
+ )
88
+ plotters[i].background_color = 'white'
89
+
90
+ for row in tqdm(range(nrows), desc=f'Building mosaic'):
91
+ for col in range(ncols):
92
+
93
+ # Load data once
94
+ mask_norm = masks[col, row, ...]
95
+
96
+ orig_vol = pv.wrap(mask_norm.astype(float))
97
+ orig_vol.spacing = [1.0, 1.0, 1.0]
98
+ orig_surface = orig_vol.contour(isosurfaces=[0.5])
99
+
100
+ prev_up = None
101
+ for i, vec in enumerate(dirs):
102
+ # Camera position
103
+ distance = orig_surface.length * 2.0 # controls zoom
104
+ center = list(orig_surface.center)
105
+ pos = center + distance * np.array(vec) # vec = direction
106
+ up = _camera_up_from_direction(vec, prev_up)
107
+ prev_up = up
108
+
109
+ # Set up plotter
110
+ plotters[i].subplot(row, col)
111
+ if labels is not None:
112
+ plotters[i].add_text(labels[col, row], font_size=6)
113
+ plotters[i].add_mesh(orig_surface, color='lightblue', opacity=1.0, style='surface')
114
+ plotters[i].camera_position = [pos, center, up]
115
+
116
+ for i, vec in tqdm(enumerate(dirs), desc='Saving mosaics..'):
117
+ file = os.path.join(dir_output, f"mosaic_{i:03d}.png")
118
+ os.makedirs(Path(file).parent, exist_ok=True)
119
+ plotters[i].screenshot(file)
120
+ plotters[i].close()
121
+
122
+
123
+
124
+
125
+ def rotating_mosaics_npz(dir_output, masks, labels=None, chunksize=None, nviews=25, columns=None, rows=None):
126
+
127
+ if labels is None:
128
+ labels = [str(i) for i in range(len(masks))]
129
+ if chunksize is None:
130
+ chunksize = len(masks)
131
+
132
+ # Split into numbered chunks
133
+ def chunk_list(lst, size):
134
+ chunks = [lst[i:i+size] for i in range(0, len(lst), size)]
135
+ return list(enumerate(chunks))
136
+
137
+ mask_chunks = chunk_list(masks, chunksize)
138
+ label_chunks = chunk_list(labels, chunksize)
139
+
140
+ # Define view points
141
+ angles = np.linspace(0, 2*np.pi, nviews)
142
+ dirs = [(np.cos(a), np.sin(a), 0.0) for a in angles] # rotate around z
143
+ dirs += [(np.cos(a), 0.0, np.sin(a)) for a in angles] # rotate around y
144
+
145
+ # Save mosaics for each chunk and view
146
+ for mask_chunk, label_chunk in zip(mask_chunks, label_chunks):
147
+ chunk_idx = mask_chunk[0]
148
+ names = [f"group_{str(chunk_idx).zfill(2)}_{i:02d}.png" for i in range(len(dirs))]
149
+ directions = {vec: os.path.join(dir_output, name) for name, vec in zip(names, dirs)}
150
+ multiple_mosaic_masks_npz(mask_chunk[1], directions, label_chunk[1], columns=columns, rows=rows)
151
+
152
+
153
+ def multiple_mosaic_masks_npz(masks, directions:dict, labels, columns=None, rows=None):
154
+ # Plot settings
155
+ aspect_ratio = 16/9
156
+ width = 150
157
+ height = 150
158
+
159
+ # Count nr of mosaics
160
+ n_mosaics = len(masks)
161
+ if columns is None:
162
+ ncols = int(np.ceil(np.sqrt((height*n_mosaics)/(aspect_ratio*width))))
163
+ else:
164
+ ncols = columns
165
+ if rows is None:
166
+ nrows = int(np.ceil(n_mosaics/ncols))
167
+ else:
168
+ nrows = rows
169
+ # nrows = int(np.ceil(np.sqrt((width*n_mosaics)/(aspect_ratio*height))))
170
+ # ncols = int(np.ceil(n_mosaics/nrows))
171
+
172
+ plotters = {}
173
+ for vec in directions.keys():
174
+ plotters[vec] = pv.Plotter(
175
+ window_size=(ncols*width, nrows*height),
176
+ shape=(nrows, ncols),
177
+ border=False,
178
+ off_screen=True,
179
+ )
180
+ plotters[vec].background_color = 'white'
181
+
182
+ row = 0
183
+ col = 0
184
+ for mask_label, mask_series in tqdm(zip(labels, masks), desc=f'Building mosaic'):
185
+
186
+ # Load data once
187
+ vol = db.npz.volume(mask_series)
188
+ mask_norm = ssa.sdf_ft.smooth_mask(vol.values.astype(bool), order=32)
189
+
190
+ orig_vol = pv.wrap(mask_norm.astype(float))
191
+ orig_vol.spacing = [1.0, 1.0, 1.0]
192
+ orig_surface = orig_vol.contour(isosurfaces=[0.5])
193
+
194
+ prev_up = None
195
+ for vec in directions.keys():
196
+ # Camera position
197
+ distance = orig_surface.length * 2.0 # controls zoom
198
+ center = list(orig_surface.center)
199
+ pos = center + distance * np.array(vec) # vec = direction
200
+ up = _camera_up_from_direction(vec, prev_up)
201
+ prev_up = up
202
+
203
+ # Set up plotter
204
+ plotter = plotters[vec]
205
+ plotter.subplot(row,col)
206
+ if labels is not None:
207
+ plotter.add_text(mask_label, font_size=6)
208
+ plotter.add_mesh(orig_surface, color='lightblue', opacity=1.0, style='surface')
209
+ plotter.camera_position = [pos, center, up]
210
+
211
+ # plotter.camera_position = 'iso'
212
+ # plotter.view_vector(vec) # rotate 180° around vertical axis
213
+
214
+ if col == ncols-1:
215
+ col = 0
216
+ row += 1
217
+ else:
218
+ col += 1
219
+
220
+ for vec, file in directions.items():
221
+ # plotters[vec].render()
222
+ plotters[vec].screenshot(file)
223
+ plotters[vec].close()
224
+
225
+
226
+
227
+ def _camera_up_from_direction(d, prev_up=None):
228
+ d = np.asarray(d, float)
229
+ d /= np.linalg.norm(d)
230
+
231
+ # 1. First Frame: Use your original logic to establish an initial Up vector
232
+ if prev_up is None:
233
+ ref = np.array([0, 0, 1])
234
+ # If looking straight down Z, switch ref to Y to avoid singularity
235
+ if abs(np.dot(d, ref)) > 0.99:
236
+ ref = np.array([0, 1, 0])
237
+
238
+ right = np.cross(ref, d)
239
+ right /= np.linalg.norm(right)
240
+ up = np.cross(d, right)
241
+
242
+ # 2. Subsequent Frames: Parallel Transport
243
+ else:
244
+ # Project the previous Up vector onto the plane perpendicular to the new direction.
245
+ # This removes the component of prev_up that is parallel to d.
246
+ # Formula: v_perp = v - (v . d) * d
247
+ up = prev_up - np.dot(prev_up, d) * d
248
+
249
+ # Normalize the result
250
+ norm = np.linalg.norm(up)
251
+
252
+ # Handle rare edge case where d aligns perfectly with prev_up (norm is 0)
253
+ if norm < 1e-6:
254
+ # Fallback to initial logic
255
+ ref = np.array([0, 0, 1])
256
+ if abs(np.dot(d, ref)) > 0.99:
257
+ ref = np.array([0, 1, 0])
258
+ right = np.cross(ref, d)
259
+ up = np.cross(d, right)
260
+ up /= np.linalg.norm(up)
261
+ else:
262
+ up /= norm
263
+
264
+ return up
265
+
266
+
267
+ def mosaic_masks_npz(masks, imagefile, labels=None, view_vector=(1, 0, 0)):
268
+
269
+ # Plot settings
270
+ aspect_ratio = 16/9
271
+ width = 150
272
+ height = 150
273
+
274
+ # Count nr of mosaics
275
+ n_mosaics = len(masks)
276
+ nrows = int(np.ceil(np.sqrt((width*n_mosaics)/(aspect_ratio*height))))
277
+ ncols = int(np.ceil(n_mosaics/nrows))
278
+
279
+ plotter = pv.Plotter(
280
+ window_size=(ncols*width, nrows*height),
281
+ shape=(nrows, ncols),
282
+ border=False,
283
+ off_screen=True,
284
+ )
285
+ plotter.background_color = 'white'
286
+
287
+ row = 0
288
+ col = 0
289
+ for i, mask_series in tqdm(enumerate(masks), desc=f'Building mosaic'):
290
+
291
+ # Set up plotter
292
+ plotter.subplot(row,col)
293
+ if labels is not None:
294
+ plotter.add_text(labels[i], font_size=6)
295
+ if col == ncols-1:
296
+ col = 0
297
+ row += 1
298
+ else:
299
+ col += 1
300
+
301
+ # Load data
302
+ vol = db.npz.volume(mask_series)
303
+ mask_norm = ssa.sdf_ft.smooth_mask(vol.values.astype(bool), order=32)
304
+
305
+ # Plot tile
306
+ orig_vol = pv.wrap(mask_norm.astype(float))
307
+ orig_vol.spacing = [1.0, 1.0, 1.0]
308
+ orig_surface = orig_vol.contour(isosurfaces=[0.5])
309
+ plotter.add_mesh(orig_surface, color='lightblue', opacity=1.0, style='surface')
310
+ plotter.camera_position = 'iso'
311
+ plotter.view_vector(view_vector) # rotate 180° around vertical axis
312
+
313
+ plotter.screenshot(imagefile)
314
+ plotter.close()
315
+
316
+
317
+ def mosaic_features_npz(features, imagefile, labels=None, view_vector=(1, 0, 0)):
318
+
319
+ # Plot settings
320
+ aspect_ratio = 16/9
321
+ width = 150
322
+ height = 150
323
+
324
+ # Count nr of mosaics
325
+ n_mosaics = len(features)
326
+ nrows = int(np.ceil(np.sqrt((width*n_mosaics)/(aspect_ratio*height))))
327
+ ncols = int(np.ceil(n_mosaics/nrows))
328
+
329
+ plotter = pv.Plotter(
330
+ window_size=(ncols*width, nrows*height),
331
+ shape=(nrows, ncols),
332
+ border=False,
333
+ off_screen=True,
334
+ )
335
+ plotter.background_color = 'white'
336
+
337
+ row = 0
338
+ col = 0
339
+ for i, feat in tqdm(enumerate(features), desc=f'Building mosaic'):
340
+
341
+ # Set up plotter
342
+ plotter.subplot(row,col)
343
+ if labels is not None:
344
+ plotter.add_text(labels[i], font_size=6)
345
+ if col == ncols-1:
346
+ col = 0
347
+ row += 1
348
+ else:
349
+ col += 1
350
+
351
+ # Load data
352
+ ft = np.load(feat)
353
+ mask_norm = ssa.sdf_ft.mask_from_features(ft['features'], ft['shape'], ft['order'])
354
+
355
+ # Plot tile
356
+ orig_vol = pv.wrap(mask_norm.astype(float))
357
+ orig_vol.spacing = [1.0, 1.0, 1.0]
358
+ orig_surface = orig_vol.contour(isosurfaces=[0.5])
359
+ plotter.add_mesh(orig_surface, color='lightblue', opacity=1.0, style='surface')
360
+ plotter.camera_position = 'iso'
361
+ plotter.view_vector(view_vector) # rotate 180° around vertical axis
362
+
363
+ plotter.screenshot(imagefile)
364
+ plotter.close()