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/__init__.py +6 -1
- miblab_plot/gui.py +652 -0
- miblab_plot/image_3d.py +138 -10
- miblab_plot/movie.py +79 -0
- miblab_plot/mp4.py +83 -0
- miblab_plot/pvplot.py +364 -0
- {miblab_plot-0.0.1.dist-info → miblab_plot-0.0.3.dist-info}/METADATA +7 -2
- miblab_plot-0.0.3.dist-info/RECORD +11 -0
- {miblab_plot-0.0.1.dist-info → miblab_plot-0.0.3.dist-info}/WHEEL +1 -1
- miblab_plot-0.0.1.dist-info/RECORD +0 -7
- {miblab_plot-0.0.1.dist-info → miblab_plot-0.0.3.dist-info}/licenses/LICENSE +0 -0
- {miblab_plot-0.0.1.dist-info → miblab_plot-0.0.3.dist-info}/top_level.txt +0 -0
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,
|
|
14
|
+
colors = [[1, 0, 0, opacity]]
|
|
18
15
|
elif len(rois)==2:
|
|
19
|
-
colors = [[1, 0, 0,
|
|
16
|
+
colors = [[1, 0, 0, opacity], [0, 1, 0, opacity]]
|
|
20
17
|
elif len(rois)==3:
|
|
21
|
-
colors = [[1, 0, 0,
|
|
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] + (
|
|
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=
|
|
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()
|