|
12 | 12 | def plot_upstream_projectome(experiment_ids, source_regions, target_region, save_location, |
13 | 13 | allen_atlas_path, atlas_resolution=10, atlas_type='allen', |
14 | 14 | slice_thickness=2, density_percentile=99, |
15 | | - static_ap=None, save_path=None): |
| 15 | + static_ap=None, save_path=None, |
| 16 | + save_gif=None, gif_ap_step=2, gif_fps=8, gif_width=720): |
16 | 17 | """Interactive coronal slice viewer of upstream projections into a target region. |
17 | 18 |
|
18 | 19 | Displays a Jupyter widget with a coronal AP slider showing: |
@@ -52,6 +53,16 @@ def plot_upstream_projectome(experiment_ids, source_regions, target_region, save |
52 | 53 | documentation figures headlessly. |
53 | 54 | save_path : str or None, optional |
54 | 55 | When *static_ap* is set, save the rendered figure to this path. |
| 56 | + save_gif : str or None, optional |
| 57 | + If given, render an animated GIF scrolling through the AP slices that |
| 58 | + contain the target region and write it to this path (requires Pillow). |
| 59 | + Skips the interactive widget. |
| 60 | + gif_ap_step : int, optional |
| 61 | + Step between AP slices when building the GIF (default 2). |
| 62 | + gif_fps : int, optional |
| 63 | + Frames per second of the GIF (default 8). |
| 64 | + gif_width : int, optional |
| 65 | + Width in pixels to downscale GIF frames to (default 720). |
55 | 66 | """ |
56 | 67 | AP, DV, ML = PROJECTION_GRID_SIZE |
57 | 68 | # Number of full-res atlas voxels per 100 µm projection grid voxel |
@@ -256,6 +267,36 @@ def _draw(ap_idx): |
256 | 267 | plt.tight_layout() |
257 | 268 | plt.show() |
258 | 269 |
|
| 270 | + # ------------------------------------- animated GIF (scroll through AP) |
| 271 | + if save_gif is not None: |
| 272 | + try: |
| 273 | + from PIL import Image |
| 274 | + except ImportError as exc: |
| 275 | + raise ImportError("Saving a GIF requires Pillow (pip install pillow).") from exc |
| 276 | + |
| 277 | + present = np.where(target_mask.any(axis=(1, 2)))[0] |
| 278 | + ap_lo, ap_hi = (int(present.min()), int(present.max())) if present.size else (0, AP - 1) |
| 279 | + ap_indices = list(range(ap_lo, ap_hi + 1, max(1, int(gif_ap_step)))) |
| 280 | + |
| 281 | + frames = [] |
| 282 | + for ap in ap_indices: |
| 283 | + _draw(ap) |
| 284 | + fig = plt.gcf() |
| 285 | + fig.canvas.draw() |
| 286 | + w, h = fig.canvas.get_width_height() |
| 287 | + rgba = np.frombuffer(fig.canvas.buffer_rgba(), dtype=np.uint8).reshape(h, w, 4) |
| 288 | + img = Image.fromarray(rgba[:, :, :3].copy()) |
| 289 | + if gif_width and img.width > gif_width: |
| 290 | + img = img.resize((int(gif_width), round(img.height * gif_width / img.width))) |
| 291 | + frames.append(img.convert('P', palette=Image.ADAPTIVE, colors=256)) |
| 292 | + plt.close(fig) |
| 293 | + |
| 294 | + if frames: |
| 295 | + frames[0].save(save_gif, save_all=True, append_images=frames[1:], |
| 296 | + duration=int(1000 / max(gif_fps, 1)), loop=0, optimize=True) |
| 297 | + print(f'Saved GIF: {save_gif} ({len(frames)} frames)') |
| 298 | + return |
| 299 | + |
259 | 300 | # ------------------------------------- static render (docs / headless use) |
260 | 301 | if static_ap is not None: |
261 | 302 | _draw(int(static_ap)) |
|
0 commit comments