generated from FNNDSC/python-chrisapp-template
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Jennings Zhang
authored and
Jennings Zhang
committed
Apr 12, 2023
1 parent
1bd8d68
commit a7eeed5
Showing
30 changed files
with
1,066 additions
and
804 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Large diffs are not rendered by default.
Oops, something went wrong.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
|
||
__version__ = '1.0.0' | ||
__version__ = '1.1.0' | ||
|
||
DISPLAY_TITLE = r""" | ||
_ __ _ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
""" | ||
Use MNI ``ray_trace`` and ImageMagick to create figures of surfaces. | ||
Inspired by the CIVET QC program ``verify_clasp``. | ||
""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
TILE_SIZE = 400 | ||
TILE_SIZE = 400 | ||
FONT_SIZE = 28 | ||
ROW_GAP = 50 | ||
COL_CAP = 1 | ||
|
||
HEMI_LABEL_RATIO_L = 0.13 | ||
HEMI_LABEL_RATIO_R = 1 - HEMI_LABEL_RATIO_L | ||
HEMI_LABEL_RATIO_Y = 0.15 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
""" | ||
Here lies the code which puts everything together. | ||
Note to developer: options such as colors and sizes should not be | ||
hard-coded anywhere. Here is the only place where the values for | ||
those options should be passed to the functions which accept them. | ||
""" | ||
|
||
from pathlib import Path | ||
from typing import Sequence | ||
from dataclasses import dataclass | ||
|
||
from surfigures.draw import constants | ||
from surfigures.draw.prep import SectionBuilder, BaseHemiPreparer, ColoredHemiPreparer | ||
from surfigures.draw.section import RowPair | ||
from surfigures.draw.tile import LazyTile | ||
from surfigures.inputs.subject import SubjectSet | ||
from surfigures.options import Options | ||
from surfigures.util.runnable import Runnable, Runner | ||
|
||
|
||
@dataclass(frozen=True) | ||
class FigureCreator(Runnable[Path]): | ||
|
||
inputs: SubjectSet | ||
output_path: Path | ||
options: Options | ||
|
||
def run(self, sp: Runner) -> Path: | ||
mid_surface_left = self.inputs.mid_surface_left(sp) | ||
mid_surface_right = self.inputs.mid_surface_right(sp) | ||
|
||
figure_data: Sequence[SectionBuilder] = ( | ||
*( | ||
SectionBuilder( | ||
BaseHemiPreparer(layer.left), | ||
BaseHemiPreparer(layer.right), | ||
) | ||
for layer in self.inputs.surfaces | ||
), | ||
*( | ||
SectionBuilder( | ||
ColoredHemiPreparer( | ||
mid_surface_left, | ||
files.left, | ||
*self.options.range_for(files.left), | ||
self.options.color_map | ||
), | ||
ColoredHemiPreparer( | ||
mid_surface_right, | ||
files.right, | ||
*self.options.range_for(files.right), | ||
self.options.color_map | ||
) | ||
) | ||
for files in self.inputs.data_files | ||
) | ||
) | ||
|
||
section_captions = [ | ||
*(s.caption for s in self.inputs.surfaces), | ||
*(s.caption for s in self.inputs.data_files) | ||
] | ||
|
||
figure_template = (f.run(sp) for f in figure_data) | ||
row_pairs = [f.to_row_pair() for f in figure_template] | ||
tile_grid = [row for rows in map(_rowpair2rows, row_pairs) for row in rows] | ||
"""2D matrix of LazyTile""" | ||
lazy_tiles = [tile for row in tile_grid for tile in row] | ||
"""1D flattened structure of data and order as tile_grid""" | ||
n_row = len(tile_grid) | ||
n_col = len(tile_grid[0]) | ||
|
||
tile_files = [sp.tmp_dir / f'{i}_{section_captions[i // (n_col * 2)]}.rgb' for i in range(len(lazy_tiles))] | ||
|
||
for tile, name in zip(lazy_tiles, tile_files): | ||
cmd = tile.ray_trace.to_cmd(self.options.bg, constants.TILE_SIZE, constants.TILE_SIZE, name) | ||
sp.run(cmd) | ||
|
||
montage_file = sp.tmp_dir / 'montage_output.png' | ||
montage_cmd = ( | ||
'montage', | ||
'-tile', f'{n_col}x{n_row}', | ||
'-background', self.options.bg, | ||
'-geometry', f'{constants.TILE_SIZE}x{constants.TILE_SIZE}+{constants.COL_CAP}+{constants.ROW_GAP}', | ||
*tile_files, | ||
montage_file | ||
) | ||
sp.run(montage_cmd) | ||
|
||
annotation_flags = [] | ||
for row, row_tiles in enumerate(tile_grid): | ||
for col, tile in enumerate(row_tiles): | ||
annotation_flags.extend(tile.labels2args( | ||
row, col, | ||
constants.TILE_SIZE, constants.TILE_SIZE, | ||
constants.COL_CAP, constants.ROW_GAP | ||
)) | ||
|
||
caption_x = round(constants.COL_CAP * 5 + constants.TILE_SIZE * 2 + 100) | ||
for i, caption in enumerate(section_captions): | ||
row = i * 2 | ||
caption_y = round(constants.ROW_GAP * (0.5 + 2 * row) + constants.TILE_SIZE * row) | ||
annot = ['-annotate', f'0x0+{caption_x}+{caption_y}', caption] | ||
annotation_flags.extend(annot) | ||
|
||
convert_cmd = ( | ||
'convert', | ||
'-box', self.options.bg, | ||
'-fill', self.options.font_color, | ||
'-pointsize', str(constants.FONT_SIZE), | ||
*annotation_flags, | ||
montage_file, | ||
self.output_path | ||
) | ||
sp.run(convert_cmd) | ||
|
||
return self.output_path | ||
|
||
|
||
def _rowpair2rows(row_pair: RowPair) -> tuple[Sequence[LazyTile], Sequence[LazyTile]]: | ||
half = len(row_pair) // 2 | ||
return row_pair[:half], row_pair[half:] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
""" | ||
Helper functions for preparing surfaces for figure generation. | ||
""" | ||
|
||
import os | ||
from dataclasses import dataclass | ||
from pathlib import Path | ||
from typing import Optional, Sequence | ||
|
||
from surfigures.draw.section import Section | ||
from surfigures.util.runnable import Runnable, Runner | ||
|
||
|
||
@dataclass(frozen=True) | ||
class BaseHemiPreparer(Runnable[tuple[Path, str]]): | ||
""" | ||
No-op surface file wrapper. Subclasses of ``DrawableHemiBuilder`` apply preprocessing | ||
to the surface to prepare the file for use with ``ray_trace``. | ||
""" | ||
surface: Path | ||
|
||
def preprocess_surface_cmd(self, output: Path) -> Optional[Sequence[str | os.PathLike]]: | ||
return None | ||
|
||
def generate_textblock_cmd(self) -> Optional[Sequence[str | os.PathLike]]: | ||
return None | ||
|
||
def get_uniqueish_name(self) -> str: | ||
return self.surface.name | ||
|
||
def run(self, sp: Runner) -> tuple[Path, str]: | ||
tmp_colored = sp.tmp_dir / (self.get_uniqueish_name() + self.surface.suffix) | ||
color_cmd = self.preprocess_surface_cmd(tmp_colored) | ||
if color_cmd: | ||
sp.run(color_cmd) | ||
colored_surface = tmp_colored | ||
else: | ||
colored_surface = self.surface | ||
|
||
stats_cmd = self.generate_textblock_cmd() | ||
if stats_cmd: | ||
textblock = sp.run(stats_cmd, stdout=sp.PIPE).stdout | ||
else: | ||
textblock = '' | ||
|
||
return colored_surface, textblock | ||
|
||
|
||
@dataclass(frozen=True) | ||
class ColoredHemiPreparer(BaseHemiPreparer): | ||
""" | ||
Wraps a surface file with a corresponding vertex-wise data file. | ||
It runs the commands ``colour_object`` and ``vertstats_stats``. | ||
""" | ||
data_file: Path | ||
data_min: str | ||
data_max: str | ||
color_map: Optional[str] | ||
""" | ||
See ``colour_object -help` | ||
``color_map`` should be a ``typing.Literal``, but I am tired... | ||
color_map is one of: | ||
gray, hot, hot_inv, cold_metal, cold_metal_inv, | ||
green_metal, green_metal_inv, lime_metal, lime_metal_inv, | ||
red_metal, red_metal_inv, purple_metal, purple_metal_inv, | ||
spectral, red, green, blue, label, rgba | ||
""" | ||
|
||
def preprocess_surface_cmd(self, output: Path) -> Optional[Sequence[str | os.PathLike]]: | ||
return 'colour_object', self.surface, self.data_file, output, self.color_map, self.data_min, self.data_max | ||
|
||
def generate_textblock_cmd(self) -> Optional[Sequence[str | os.PathLike]]: | ||
return 'vertstats_stats', self.data_file | ||
|
||
def get_uniqueish_name(self) -> str: | ||
parts = [self.surface.name, self.data_file.name, self.color_map, self.data_min, self.data_max] | ||
return '_'.join(map(str, parts)) | ||
|
||
|
||
@dataclass(frozen=True) | ||
class SectionBuilder(Runnable[Section]): | ||
""" | ||
Builder for ``DrawableBrain``. | ||
""" | ||
|
||
left: BaseHemiPreparer | ||
right: BaseHemiPreparer | ||
|
||
def run(self, sp: Runner) -> Section: | ||
surface_left, textblock_left = self.left.run(sp) | ||
surface_right, textblock_right = self.right.run(sp) | ||
return Section(surface_left, surface_right, textblock_left, textblock_right) |
Oops, something went wrong.