diff --git a/README.md b/README.md index 6f34ab0..846f5c4 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,17 @@ To print its available options, run: apptainer exec docker://fnndsc/pl-surfigures surfigures --help ``` +### Colors + +Valid values for the options `--font-color` and `--background-color` are described here: https://imagemagick.org/script/color.php + +Valid values for `--color-map` are described in `colour_object -help`. + +> 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 + ## Examples `surfigures` requires two positional arguments: a directory containing @@ -60,3 +71,22 @@ input data, and a directory where to create output data. ```shell apptainer exec docker://fnndsc/pl-surfigures:latest surfigures incoming/ outgoing/ ``` + +For a dark theme: + +```shell +apptainer exec docker://fnndsc/pl-surfigures:latest surfigures \ + --font-color green1 --background-color black \ + incoming/ outgoing/ +``` + +Let's say your vertex-wise data files use the file extensions `.area.s5` +and `.depth.s5`, where the range for values of `.area.s5` data are between +0 and 1, and the range for values of `.depth.s5` is `-0.5` to `0.5`. +Use the `--range` option to specify this. The format is`file_extension:min:max`, multiple values are comma-delimited. + +```shell +apptainer exec docker://fnndsc/pl-surfigures:latest surfigures \ + --range .area.s5:0.0:1.0,.depth.s5:0.0:5.0 \ + incoming/ outgoing/ +``` diff --git a/examples/outgoing/same_folder.log b/examples/outgoing/same_folder.log index debd31e..ad7ad59 100644 --- a/examples/outgoing/same_folder.log +++ b/examples/outgoing/same_folder.log @@ -1,58 +1,88 @@ -average_surfaces /tmp/QkCkoQwbLy/mid_surface_left.obj none none 1 /share/incoming/same_folder/mni_icbm_01_gray_surface_left_81920.obj /share/incoming/same_folder/mni_icbm_01_white_surface_left_81920.obj -1: /share/incoming/same_folder/mni_icbm_01_gray_surface_left_81920.obj -2: /share/incoming/same_folder/mni_icbm_01_white_surface_left_81920.obj -Objects output. -average_surfaces /tmp/QkCkoQwbLy/mid_surface_right.obj none none 1 /share/incoming/same_folder/mni_icbm_01_gray_surface_right_81920.obj /share/incoming/same_folder/mni_icbm_01_white_surface_right_81920.obj -1: /share/incoming/same_folder/mni_icbm_01_gray_surface_right_81920.obj -2: /share/incoming/same_folder/mni_icbm_01_white_surface_right_81920.obj -Objects output. -ray_trace -shadows -output /tmp/QkCkoQwbLy/1_hemi_default.rgb /share/incoming/same_folder/mni_icbm_01_gray_surface_left_81920.obj -bg white -crop -view 0.77 -0.18 -0.6 0.55 0.6 0.55 -ray_trace -shadows -output /tmp/QkCkoQwbLy/1_hemi_left.rgb /share/incoming/same_folder/mni_icbm_01_gray_surface_left_81920.obj -bg white -crop -left -ray_trace -shadows -output /tmp/QkCkoQwbLy/1_hemi_right.rgb /share/incoming/same_folder/mni_icbm_01_gray_surface_left_81920.obj -bg white -crop -right -ray_trace -shadows -output /tmp/QkCkoQwbLy/1_surf_top.rgb /share/incoming/same_folder/mni_icbm_01_gray_surface_left_81920.obj /share/incoming/same_folder/mni_icbm_01_gray_surface_right_81920.obj -bg white -crop -top -ray_trace -shadows -output /tmp/QkCkoQwbLy/1_surf_bottom.rgb /share/incoming/same_folder/mni_icbm_01_gray_surface_left_81920.obj /share/incoming/same_folder/mni_icbm_01_gray_surface_right_81920.obj -bg white -crop -bottom -ray_trace -shadows -output /tmp/QkCkoQwbLy/2_hemi_flipped.rgb /share/incoming/same_folder/mni_icbm_01_gray_surface_right_81920.obj -bg white -crop -view -0.77 -0.18 -0.6 -0.55 0.6 0.55 -ray_trace -shadows -output /tmp/QkCkoQwbLy/2_hemi_right.rgb /share/incoming/same_folder/mni_icbm_01_gray_surface_right_81920.obj -bg white -crop -right -ray_trace -shadows -output /tmp/QkCkoQwbLy/2_hemi_left.rgb /share/incoming/same_folder/mni_icbm_01_gray_surface_right_81920.obj -bg white -crop -left -ray_trace -shadows -output /tmp/QkCkoQwbLy/2_surf_front.rgb /share/incoming/same_folder/mni_icbm_01_gray_surface_left_81920.obj /share/incoming/same_folder/mni_icbm_01_gray_surface_right_81920.obj -bg white -crop -front -ray_trace -shadows -output /tmp/QkCkoQwbLy/2_surf_back.rgb /share/incoming/same_folder/mni_icbm_01_gray_surface_left_81920.obj /share/incoming/same_folder/mni_icbm_01_gray_surface_right_81920.obj -bg white -crop -back -ray_trace -shadows -output /tmp/QkCkoQwbLy/3_hemi_default.rgb /share/incoming/same_folder/mni_icbm_01_white_surface_left_81920.obj -bg white -crop -view 0.77 -0.18 -0.6 0.55 0.6 0.55 -ray_trace -shadows -output /tmp/QkCkoQwbLy/3_hemi_left.rgb /share/incoming/same_folder/mni_icbm_01_white_surface_left_81920.obj -bg white -crop -left -ray_trace -shadows -output /tmp/QkCkoQwbLy/3_hemi_right.rgb /share/incoming/same_folder/mni_icbm_01_white_surface_left_81920.obj -bg white -crop -right -ray_trace -shadows -output /tmp/QkCkoQwbLy/3_surf_top.rgb /share/incoming/same_folder/mni_icbm_01_white_surface_left_81920.obj /share/incoming/same_folder/mni_icbm_01_white_surface_right_81920.obj -bg white -crop -top -ray_trace -shadows -output /tmp/QkCkoQwbLy/3_surf_bottom.rgb /share/incoming/same_folder/mni_icbm_01_white_surface_left_81920.obj /share/incoming/same_folder/mni_icbm_01_white_surface_right_81920.obj -bg white -crop -bottom -ray_trace -shadows -output /tmp/QkCkoQwbLy/4_hemi_flipped.rgb /share/incoming/same_folder/mni_icbm_01_white_surface_right_81920.obj -bg white -crop -view -0.77 -0.18 -0.6 -0.55 0.6 0.55 -ray_trace -shadows -output /tmp/QkCkoQwbLy/4_hemi_right.rgb /share/incoming/same_folder/mni_icbm_01_white_surface_right_81920.obj -bg white -crop -right -ray_trace -shadows -output /tmp/QkCkoQwbLy/4_hemi_left.rgb /share/incoming/same_folder/mni_icbm_01_white_surface_right_81920.obj -bg white -crop -left -ray_trace -shadows -output /tmp/QkCkoQwbLy/4_surf_front.rgb /share/incoming/same_folder/mni_icbm_01_white_surface_left_81920.obj /share/incoming/same_folder/mni_icbm_01_white_surface_right_81920.obj -bg white -crop -front -ray_trace -shadows -output /tmp/QkCkoQwbLy/4_surf_back.rgb /share/incoming/same_folder/mni_icbm_01_white_surface_left_81920.obj /share/incoming/same_folder/mni_icbm_01_white_surface_right_81920.obj -bg white -crop -back -colour_object /tmp/QkCkoQwbLy/mid_surface_left.obj /share/incoming/same_folder/mni_icbm_01_native_rms_tlaplace_30mm_left.txt /tmp/QkCkoQwbLy/4_mid_rms_left.obj spectral 0.0 10.0 -Value range: 1.4768 3.98805 -colour_object /tmp/QkCkoQwbLy/mid_surface_right.obj /share/incoming/same_folder/mni_icbm_01_native_rms_tlaplace_30mm_right.txt /tmp/QkCkoQwbLy/4_mid_rms_right.obj spectral 0.0 10.0 -Value range: 1.47367 4.02445 -ray_trace -shadows -output /tmp/QkCkoQwbLy/5_mid_left_default.rgb /tmp/QkCkoQwbLy/4_mid_rms_left.obj -bg white -crop -view 0.77 -0.18 -0.6 0.55 0.6 0.55 -ray_trace -shadows -output /tmp/QkCkoQwbLy/5_mid_left_left.rgb /tmp/QkCkoQwbLy/4_mid_rms_left.obj -bg white -crop -left -ray_trace -shadows -output /tmp/QkCkoQwbLy/5_mid_left_right.rgb /tmp/QkCkoQwbLy/4_mid_rms_left.obj -bg white -crop -right -ray_trace -shadows -output /tmp/QkCkoQwbLy/5_mid_top.rgb /tmp/QkCkoQwbLy/4_mid_rms_left.obj /tmp/QkCkoQwbLy/4_mid_rms_right.obj -bg white -crop -top -ray_trace -shadows -output /tmp/QkCkoQwbLy/5_mid_bottom.rgb /tmp/QkCkoQwbLy/4_mid_rms_left.obj /tmp/QkCkoQwbLy/4_mid_rms_right.obj -bg white -crop -bottom -ray_trace -shadows -output /tmp/QkCkoQwbLy/6_mid_right_flipped.rgb /tmp/QkCkoQwbLy/4_mid_rms_right.obj -bg white -crop -view -0.77 -0.18 -0.6 -0.55 0.6 0.55 -ray_trace -shadows -output /tmp/QkCkoQwbLy/6_mid_right_right.rgb /tmp/QkCkoQwbLy/4_mid_rms_right.obj -bg white -crop -right -ray_trace -shadows -output /tmp/QkCkoQwbLy/6_mid_right_left.rgb /tmp/QkCkoQwbLy/4_mid_rms_right.obj -bg white -crop -left -ray_trace -shadows -output /tmp/QkCkoQwbLy/6_mid_front.rgb /tmp/QkCkoQwbLy/4_mid_rms_left.obj /tmp/QkCkoQwbLy/4_mid_rms_right.obj -bg white -crop -front -ray_trace -shadows -output /tmp/QkCkoQwbLy/6_mid_back.rgb /tmp/QkCkoQwbLy/4_mid_rms_left.obj /tmp/QkCkoQwbLy/4_mid_rms_right.obj -bg white -crop -back -colour_object /tmp/QkCkoQwbLy/mid_surface_left.obj /share/incoming/same_folder/mni_icbm_01_gray_surface_left_81920.smtherr.txt /tmp/QkCkoQwbLy/6_mid_rms_left.obj spectral 0.0 2.0 -Value range: 7.19126e-09 42.6139 -colour_object /tmp/QkCkoQwbLy/mid_surface_right.obj /share/incoming/same_folder/mni_icbm_01_gray_surface_right_81920.smtherr.txt /tmp/QkCkoQwbLy/6_mid_rms_right.obj spectral 0.0 2.0 -Value range: 8.08211e-09 64.7933 -ray_trace -shadows -output /tmp/QkCkoQwbLy/7_mid_left_default.rgb /tmp/QkCkoQwbLy/6_mid_rms_left.obj -bg white -crop -view 0.77 -0.18 -0.6 0.55 0.6 0.55 -ray_trace -shadows -output /tmp/QkCkoQwbLy/7_mid_left_left.rgb /tmp/QkCkoQwbLy/6_mid_rms_left.obj -bg white -crop -left -ray_trace -shadows -output /tmp/QkCkoQwbLy/7_mid_left_right.rgb /tmp/QkCkoQwbLy/6_mid_rms_left.obj -bg white -crop -right -ray_trace -shadows -output /tmp/QkCkoQwbLy/7_mid_top.rgb /tmp/QkCkoQwbLy/6_mid_rms_left.obj /tmp/QkCkoQwbLy/6_mid_rms_right.obj -bg white -crop -top -ray_trace -shadows -output /tmp/QkCkoQwbLy/7_mid_bottom.rgb /tmp/QkCkoQwbLy/6_mid_rms_left.obj /tmp/QkCkoQwbLy/6_mid_rms_right.obj -bg white -crop -bottom -ray_trace -shadows -output /tmp/QkCkoQwbLy/8_mid_right_flipped.rgb /tmp/QkCkoQwbLy/6_mid_rms_right.obj -bg white -crop -view -0.77 -0.18 -0.6 -0.55 0.6 0.55 -ray_trace -shadows -output /tmp/QkCkoQwbLy/8_mid_right_right.rgb /tmp/QkCkoQwbLy/6_mid_rms_right.obj -bg white -crop -right -ray_trace -shadows -output /tmp/QkCkoQwbLy/8_mid_right_left.rgb /tmp/QkCkoQwbLy/6_mid_rms_right.obj -bg white -crop -left -ray_trace -shadows -output /tmp/QkCkoQwbLy/8_mid_front.rgb /tmp/QkCkoQwbLy/6_mid_rms_left.obj /tmp/QkCkoQwbLy/6_mid_rms_right.obj -bg white -crop -front -ray_trace -shadows -output /tmp/QkCkoQwbLy/8_mid_back.rgb /tmp/QkCkoQwbLy/6_mid_rms_left.obj /tmp/QkCkoQwbLy/6_mid_rms_right.obj -bg white -crop -back -montage -tile 5x8 -background white -geometry 200x200+1+1 /tmp/QkCkoQwbLy/1_hemi_default.rgb /tmp/QkCkoQwbLy/1_hemi_left.rgb /tmp/QkCkoQwbLy/1_hemi_right.rgb /tmp/QkCkoQwbLy/1_surf_top.rgb /tmp/QkCkoQwbLy/1_surf_bottom.rgb /tmp/QkCkoQwbLy/2_hemi_flipped.rgb /tmp/QkCkoQwbLy/2_hemi_right.rgb /tmp/QkCkoQwbLy/2_hemi_left.rgb /tmp/QkCkoQwbLy/2_surf_front.rgb /tmp/QkCkoQwbLy/2_surf_back.rgb /tmp/QkCkoQwbLy/3_hemi_default.rgb /tmp/QkCkoQwbLy/3_hemi_left.rgb /tmp/QkCkoQwbLy/3_hemi_right.rgb /tmp/QkCkoQwbLy/3_surf_top.rgb /tmp/QkCkoQwbLy/3_surf_bottom.rgb /tmp/QkCkoQwbLy/4_hemi_flipped.rgb /tmp/QkCkoQwbLy/4_hemi_right.rgb /tmp/QkCkoQwbLy/4_hemi_left.rgb /tmp/QkCkoQwbLy/4_surf_front.rgb /tmp/QkCkoQwbLy/4_surf_back.rgb /tmp/QkCkoQwbLy/5_mid_left_default.rgb /tmp/QkCkoQwbLy/5_mid_left_left.rgb /tmp/QkCkoQwbLy/5_mid_left_right.rgb /tmp/QkCkoQwbLy/5_mid_top.rgb /tmp/QkCkoQwbLy/5_mid_bottom.rgb /tmp/QkCkoQwbLy/6_mid_right_flipped.rgb /tmp/QkCkoQwbLy/6_mid_right_right.rgb /tmp/QkCkoQwbLy/6_mid_right_left.rgb /tmp/QkCkoQwbLy/6_mid_front.rgb /tmp/QkCkoQwbLy/6_mid_back.rgb /tmp/QkCkoQwbLy/7_mid_left_default.rgb /tmp/QkCkoQwbLy/7_mid_left_left.rgb /tmp/QkCkoQwbLy/7_mid_left_right.rgb /tmp/QkCkoQwbLy/7_mid_top.rgb /tmp/QkCkoQwbLy/7_mid_bottom.rgb /tmp/QkCkoQwbLy/8_mid_right_flipped.rgb /tmp/QkCkoQwbLy/8_mid_right_right.rgb /tmp/QkCkoQwbLy/8_mid_right_left.rgb /tmp/QkCkoQwbLy/8_mid_front.rgb /tmp/QkCkoQwbLy/8_mid_back.rgb /tmp/QkCkoQwbLy/mont.png -convert -box white -stroke green -pointsize 16 -font DejaVu-Sans -annotate 0x0+400+15 same_folder -annotate 0x0+637+28 L -annotate 0x0+768+28 R -annotate 0x0+837+28 R -annotate 0x0+968+28 L -annotate 0x0+637+224 R -annotate 0x0+768+224 L -annotate 0x0+837+224 L -annotate 0x0+968+224 R -annotate 0x0+400+215 mni_icbm_01_gray_surface_81920.obj -annotate 0x0+637+441 L -annotate 0x0+768+441 R -annotate 0x0+837+441 R -annotate 0x0+968+441 L -annotate 0x0+637+637 R -annotate 0x0+768+637 L -annotate 0x0+837+637 L -annotate 0x0+968+637 R -annotate 0x0+400+628 mni_icbm_01_white_surface_81920.obj -annotate 0x0+340+1041 mni_icbm_01_native_rms_tlaplace_30mm.txt -annotate 0x0+340+1441 mni_icbm_01_gray_surface_81920.smtherr.txt /tmp/QkCkoQwbLy/mont.png /share/outgoing/same_folder.png +average_surfaces /tmp/tmpcdj_i5gq/same_folder_left.obj none none 1 /examples/incoming/same_folder/mni_icbm_01_gray_surface_left_81920.obj /examples/incoming/same_folder/mni_icbm_01_white_surface_left_81920.obj +average_surfaces /tmp/tmpcdj_i5gq/same_folder_right.obj none none 1 /examples/incoming/same_folder/mni_icbm_01_gray_surface_right_81920.obj /examples/incoming/same_folder/mni_icbm_01_white_surface_right_81920.obj +colour_object /tmp/tmpcdj_i5gq/same_folder_left.obj /examples/incoming/same_folder/mni_icbm_01_native_rms_tlaplace_30mm_left.txt /tmp/tmpcdj_i5gq/same_folder_left.obj_mni_icbm_01_native_rms_tlaplace_30mm_left.txt_spectral_0.0_10.0.obj spectral 0.0 10.0 +vertstats_stats /examples/incoming/same_folder/mni_icbm_01_native_rms_tlaplace_30mm_left.txt +colour_object /tmp/tmpcdj_i5gq/same_folder_right.obj /examples/incoming/same_folder/mni_icbm_01_native_rms_tlaplace_30mm_right.txt /tmp/tmpcdj_i5gq/same_folder_right.obj_mni_icbm_01_native_rms_tlaplace_30mm_right.txt_spectral_0.0_10.0.obj spectral 0.0 10.0 +vertstats_stats /examples/incoming/same_folder/mni_icbm_01_native_rms_tlaplace_30mm_right.txt +colour_object /tmp/tmpcdj_i5gq/same_folder_left.obj /examples/incoming/same_folder/mni_icbm_01_gray_surface_left_81920.smtherr.txt /tmp/tmpcdj_i5gq/same_folder_left.obj_mni_icbm_01_gray_surface_left_81920.smtherr.txt_spectral_0.0_2.0.obj spectral 0.0 2.0 +vertstats_stats /examples/incoming/same_folder/mni_icbm_01_gray_surface_left_81920.smtherr.txt +colour_object /tmp/tmpcdj_i5gq/same_folder_right.obj /examples/incoming/same_folder/mni_icbm_01_gray_surface_right_81920.smtherr.txt /tmp/tmpcdj_i5gq/same_folder_right.obj_mni_icbm_01_gray_surface_right_81920.smtherr.txt_spectral_0.0_2.0.obj spectral 0.0 2.0 +vertstats_stats /examples/incoming/same_folder/mni_icbm_01_gray_surface_right_81920.smtherr.txt +ray_trace -shadows -output /tmp/tmpcdj_i5gq/0_mni_icbm_01_gray_surface_81920.obj.rgb -bg black -crop -size 400 400 -top /examples/incoming/same_folder/mni_icbm_01_gray_surface_left_81920.obj /examples/incoming/same_folder/mni_icbm_01_gray_surface_right_81920.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/1_mni_icbm_01_gray_surface_81920.obj.rgb -bg black -crop -size 400 400 -bottom /examples/incoming/same_folder/mni_icbm_01_gray_surface_left_81920.obj /examples/incoming/same_folder/mni_icbm_01_gray_surface_right_81920.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/2_mni_icbm_01_gray_surface_81920.obj.rgb -bg black -crop -size 400 400 -view 0.77 -0.18 -0.6 0.55 0.6 0.55 /examples/incoming/same_folder/mni_icbm_01_gray_surface_left_81920.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/3_mni_icbm_01_gray_surface_81920.obj.rgb -bg black -crop -size 400 400 -left /examples/incoming/same_folder/mni_icbm_01_gray_surface_left_81920.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/4_mni_icbm_01_gray_surface_81920.obj.rgb -bg black -crop -size 400 400 -right /examples/incoming/same_folder/mni_icbm_01_gray_surface_left_81920.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/5_mni_icbm_01_gray_surface_81920.obj.rgb -bg black -crop -size 400 400 +ray_trace -shadows -output /tmp/tmpcdj_i5gq/6_mni_icbm_01_gray_surface_81920.obj.rgb -bg black -crop -size 400 400 -front /examples/incoming/same_folder/mni_icbm_01_gray_surface_left_81920.obj /examples/incoming/same_folder/mni_icbm_01_gray_surface_right_81920.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/7_mni_icbm_01_gray_surface_81920.obj.rgb -bg black -crop -size 400 400 -back /examples/incoming/same_folder/mni_icbm_01_gray_surface_left_81920.obj /examples/incoming/same_folder/mni_icbm_01_gray_surface_right_81920.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/8_mni_icbm_01_gray_surface_81920.obj.rgb -bg black -crop -size 400 400 -view -0.77 -0.18 -0.6 -0.55 0.6 0.55 /examples/incoming/same_folder/mni_icbm_01_gray_surface_right_81920.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/9_mni_icbm_01_gray_surface_81920.obj.rgb -bg black -crop -size 400 400 -left /examples/incoming/same_folder/mni_icbm_01_gray_surface_right_81920.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/10_mni_icbm_01_gray_surface_81920.obj.rgb -bg black -crop -size 400 400 -right /examples/incoming/same_folder/mni_icbm_01_gray_surface_right_81920.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/11_mni_icbm_01_gray_surface_81920.obj.rgb -bg black -crop -size 400 400 +ray_trace -shadows -output /tmp/tmpcdj_i5gq/12_mni_icbm_01_white_surface_81920.obj.rgb -bg black -crop -size 400 400 -top /examples/incoming/same_folder/mni_icbm_01_white_surface_left_81920.obj /examples/incoming/same_folder/mni_icbm_01_white_surface_right_81920.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/13_mni_icbm_01_white_surface_81920.obj.rgb -bg black -crop -size 400 400 -bottom /examples/incoming/same_folder/mni_icbm_01_white_surface_left_81920.obj /examples/incoming/same_folder/mni_icbm_01_white_surface_right_81920.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/14_mni_icbm_01_white_surface_81920.obj.rgb -bg black -crop -size 400 400 -view 0.77 -0.18 -0.6 0.55 0.6 0.55 /examples/incoming/same_folder/mni_icbm_01_white_surface_left_81920.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/15_mni_icbm_01_white_surface_81920.obj.rgb -bg black -crop -size 400 400 -left /examples/incoming/same_folder/mni_icbm_01_white_surface_left_81920.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/16_mni_icbm_01_white_surface_81920.obj.rgb -bg black -crop -size 400 400 -right /examples/incoming/same_folder/mni_icbm_01_white_surface_left_81920.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/17_mni_icbm_01_white_surface_81920.obj.rgb -bg black -crop -size 400 400 +ray_trace -shadows -output /tmp/tmpcdj_i5gq/18_mni_icbm_01_white_surface_81920.obj.rgb -bg black -crop -size 400 400 -front /examples/incoming/same_folder/mni_icbm_01_white_surface_left_81920.obj /examples/incoming/same_folder/mni_icbm_01_white_surface_right_81920.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/19_mni_icbm_01_white_surface_81920.obj.rgb -bg black -crop -size 400 400 -back /examples/incoming/same_folder/mni_icbm_01_white_surface_left_81920.obj /examples/incoming/same_folder/mni_icbm_01_white_surface_right_81920.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/20_mni_icbm_01_white_surface_81920.obj.rgb -bg black -crop -size 400 400 -view -0.77 -0.18 -0.6 -0.55 0.6 0.55 /examples/incoming/same_folder/mni_icbm_01_white_surface_right_81920.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/21_mni_icbm_01_white_surface_81920.obj.rgb -bg black -crop -size 400 400 -left /examples/incoming/same_folder/mni_icbm_01_white_surface_right_81920.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/22_mni_icbm_01_white_surface_81920.obj.rgb -bg black -crop -size 400 400 -right /examples/incoming/same_folder/mni_icbm_01_white_surface_right_81920.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/23_mni_icbm_01_white_surface_81920.obj.rgb -bg black -crop -size 400 400 +ray_trace -shadows -output /tmp/tmpcdj_i5gq/24_mni_icbm_01_native_rms_tlaplace_30mm.txt.rgb -bg black -crop -size 400 400 -top /tmp/tmpcdj_i5gq/same_folder_left.obj_mni_icbm_01_native_rms_tlaplace_30mm_left.txt_spectral_0.0_10.0.obj /tmp/tmpcdj_i5gq/same_folder_right.obj_mni_icbm_01_native_rms_tlaplace_30mm_right.txt_spectral_0.0_10.0.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/25_mni_icbm_01_native_rms_tlaplace_30mm.txt.rgb -bg black -crop -size 400 400 -bottom /tmp/tmpcdj_i5gq/same_folder_left.obj_mni_icbm_01_native_rms_tlaplace_30mm_left.txt_spectral_0.0_10.0.obj /tmp/tmpcdj_i5gq/same_folder_right.obj_mni_icbm_01_native_rms_tlaplace_30mm_right.txt_spectral_0.0_10.0.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/26_mni_icbm_01_native_rms_tlaplace_30mm.txt.rgb -bg black -crop -size 400 400 -view 0.77 -0.18 -0.6 0.55 0.6 0.55 /tmp/tmpcdj_i5gq/same_folder_left.obj_mni_icbm_01_native_rms_tlaplace_30mm_left.txt_spectral_0.0_10.0.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/27_mni_icbm_01_native_rms_tlaplace_30mm.txt.rgb -bg black -crop -size 400 400 -left /tmp/tmpcdj_i5gq/same_folder_left.obj_mni_icbm_01_native_rms_tlaplace_30mm_left.txt_spectral_0.0_10.0.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/28_mni_icbm_01_native_rms_tlaplace_30mm.txt.rgb -bg black -crop -size 400 400 -right /tmp/tmpcdj_i5gq/same_folder_left.obj_mni_icbm_01_native_rms_tlaplace_30mm_left.txt_spectral_0.0_10.0.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/29_mni_icbm_01_native_rms_tlaplace_30mm.txt.rgb -bg black -crop -size 400 400 +ray_trace -shadows -output /tmp/tmpcdj_i5gq/30_mni_icbm_01_native_rms_tlaplace_30mm.txt.rgb -bg black -crop -size 400 400 -front /tmp/tmpcdj_i5gq/same_folder_left.obj_mni_icbm_01_native_rms_tlaplace_30mm_left.txt_spectral_0.0_10.0.obj /tmp/tmpcdj_i5gq/same_folder_right.obj_mni_icbm_01_native_rms_tlaplace_30mm_right.txt_spectral_0.0_10.0.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/31_mni_icbm_01_native_rms_tlaplace_30mm.txt.rgb -bg black -crop -size 400 400 -back /tmp/tmpcdj_i5gq/same_folder_left.obj_mni_icbm_01_native_rms_tlaplace_30mm_left.txt_spectral_0.0_10.0.obj /tmp/tmpcdj_i5gq/same_folder_right.obj_mni_icbm_01_native_rms_tlaplace_30mm_right.txt_spectral_0.0_10.0.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/32_mni_icbm_01_native_rms_tlaplace_30mm.txt.rgb -bg black -crop -size 400 400 -view -0.77 -0.18 -0.6 -0.55 0.6 0.55 /tmp/tmpcdj_i5gq/same_folder_right.obj_mni_icbm_01_native_rms_tlaplace_30mm_right.txt_spectral_0.0_10.0.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/33_mni_icbm_01_native_rms_tlaplace_30mm.txt.rgb -bg black -crop -size 400 400 -left /tmp/tmpcdj_i5gq/same_folder_right.obj_mni_icbm_01_native_rms_tlaplace_30mm_right.txt_spectral_0.0_10.0.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/34_mni_icbm_01_native_rms_tlaplace_30mm.txt.rgb -bg black -crop -size 400 400 -right /tmp/tmpcdj_i5gq/same_folder_right.obj_mni_icbm_01_native_rms_tlaplace_30mm_right.txt_spectral_0.0_10.0.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/35_mni_icbm_01_native_rms_tlaplace_30mm.txt.rgb -bg black -crop -size 400 400 +ray_trace -shadows -output /tmp/tmpcdj_i5gq/36_mni_icbm_01_gray_surface_81920.smtherr.txt.rgb -bg black -crop -size 400 400 -top /tmp/tmpcdj_i5gq/same_folder_left.obj_mni_icbm_01_gray_surface_left_81920.smtherr.txt_spectral_0.0_2.0.obj /tmp/tmpcdj_i5gq/same_folder_right.obj_mni_icbm_01_gray_surface_right_81920.smtherr.txt_spectral_0.0_2.0.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/37_mni_icbm_01_gray_surface_81920.smtherr.txt.rgb -bg black -crop -size 400 400 -bottom /tmp/tmpcdj_i5gq/same_folder_left.obj_mni_icbm_01_gray_surface_left_81920.smtherr.txt_spectral_0.0_2.0.obj /tmp/tmpcdj_i5gq/same_folder_right.obj_mni_icbm_01_gray_surface_right_81920.smtherr.txt_spectral_0.0_2.0.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/38_mni_icbm_01_gray_surface_81920.smtherr.txt.rgb -bg black -crop -size 400 400 -view 0.77 -0.18 -0.6 0.55 0.6 0.55 /tmp/tmpcdj_i5gq/same_folder_left.obj_mni_icbm_01_gray_surface_left_81920.smtherr.txt_spectral_0.0_2.0.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/39_mni_icbm_01_gray_surface_81920.smtherr.txt.rgb -bg black -crop -size 400 400 -left /tmp/tmpcdj_i5gq/same_folder_left.obj_mni_icbm_01_gray_surface_left_81920.smtherr.txt_spectral_0.0_2.0.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/40_mni_icbm_01_gray_surface_81920.smtherr.txt.rgb -bg black -crop -size 400 400 -right /tmp/tmpcdj_i5gq/same_folder_left.obj_mni_icbm_01_gray_surface_left_81920.smtherr.txt_spectral_0.0_2.0.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/41_mni_icbm_01_gray_surface_81920.smtherr.txt.rgb -bg black -crop -size 400 400 +ray_trace -shadows -output /tmp/tmpcdj_i5gq/42_mni_icbm_01_gray_surface_81920.smtherr.txt.rgb -bg black -crop -size 400 400 -front /tmp/tmpcdj_i5gq/same_folder_left.obj_mni_icbm_01_gray_surface_left_81920.smtherr.txt_spectral_0.0_2.0.obj /tmp/tmpcdj_i5gq/same_folder_right.obj_mni_icbm_01_gray_surface_right_81920.smtherr.txt_spectral_0.0_2.0.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/43_mni_icbm_01_gray_surface_81920.smtherr.txt.rgb -bg black -crop -size 400 400 -back /tmp/tmpcdj_i5gq/same_folder_left.obj_mni_icbm_01_gray_surface_left_81920.smtherr.txt_spectral_0.0_2.0.obj /tmp/tmpcdj_i5gq/same_folder_right.obj_mni_icbm_01_gray_surface_right_81920.smtherr.txt_spectral_0.0_2.0.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/44_mni_icbm_01_gray_surface_81920.smtherr.txt.rgb -bg black -crop -size 400 400 -view -0.77 -0.18 -0.6 -0.55 0.6 0.55 /tmp/tmpcdj_i5gq/same_folder_right.obj_mni_icbm_01_gray_surface_right_81920.smtherr.txt_spectral_0.0_2.0.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/45_mni_icbm_01_gray_surface_81920.smtherr.txt.rgb -bg black -crop -size 400 400 -left /tmp/tmpcdj_i5gq/same_folder_right.obj_mni_icbm_01_gray_surface_right_81920.smtherr.txt_spectral_0.0_2.0.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/46_mni_icbm_01_gray_surface_81920.smtherr.txt.rgb -bg black -crop -size 400 400 -right /tmp/tmpcdj_i5gq/same_folder_right.obj_mni_icbm_01_gray_surface_right_81920.smtherr.txt_spectral_0.0_2.0.obj +ray_trace -shadows -output /tmp/tmpcdj_i5gq/47_mni_icbm_01_gray_surface_81920.smtherr.txt.rgb -bg black -crop -size 400 400 +montage -tile 6x8 -background black -geometry 400x400+1+50 /tmp/tmpcdj_i5gq/0_mni_icbm_01_gray_surface_81920.obj.rgb /tmp/tmpcdj_i5gq/1_mni_icbm_01_gray_surface_81920.obj.rgb /tmp/tmpcdj_i5gq/2_mni_icbm_01_gray_surface_81920.obj.rgb /tmp/tmpcdj_i5gq/3_mni_icbm_01_gray_surface_81920.obj.rgb /tmp/tmpcdj_i5gq/4_mni_icbm_01_gray_surface_81920.obj.rgb /tmp/tmpcdj_i5gq/5_mni_icbm_01_gray_surface_81920.obj.rgb /tmp/tmpcdj_i5gq/6_mni_icbm_01_gray_surface_81920.obj.rgb /tmp/tmpcdj_i5gq/7_mni_icbm_01_gray_surface_81920.obj.rgb /tmp/tmpcdj_i5gq/8_mni_icbm_01_gray_surface_81920.obj.rgb /tmp/tmpcdj_i5gq/9_mni_icbm_01_gray_surface_81920.obj.rgb /tmp/tmpcdj_i5gq/10_mni_icbm_01_gray_surface_81920.obj.rgb /tmp/tmpcdj_i5gq/11_mni_icbm_01_gray_surface_81920.obj.rgb /tmp/tmpcdj_i5gq/12_mni_icbm_01_white_surface_81920.obj.rgb /tmp/tmpcdj_i5gq/13_mni_icbm_01_white_surface_81920.obj.rgb /tmp/tmpcdj_i5gq/14_mni_icbm_01_white_surface_81920.obj.rgb /tmp/tmpcdj_i5gq/15_mni_icbm_01_white_surface_81920.obj.rgb /tmp/tmpcdj_i5gq/16_mni_icbm_01_white_surface_81920.obj.rgb /tmp/tmpcdj_i5gq/17_mni_icbm_01_white_surface_81920.obj.rgb /tmp/tmpcdj_i5gq/18_mni_icbm_01_white_surface_81920.obj.rgb /tmp/tmpcdj_i5gq/19_mni_icbm_01_white_surface_81920.obj.rgb /tmp/tmpcdj_i5gq/20_mni_icbm_01_white_surface_81920.obj.rgb /tmp/tmpcdj_i5gq/21_mni_icbm_01_white_surface_81920.obj.rgb /tmp/tmpcdj_i5gq/22_mni_icbm_01_white_surface_81920.obj.rgb /tmp/tmpcdj_i5gq/23_mni_icbm_01_white_surface_81920.obj.rgb /tmp/tmpcdj_i5gq/24_mni_icbm_01_native_rms_tlaplace_30mm.txt.rgb /tmp/tmpcdj_i5gq/25_mni_icbm_01_native_rms_tlaplace_30mm.txt.rgb /tmp/tmpcdj_i5gq/26_mni_icbm_01_native_rms_tlaplace_30mm.txt.rgb /tmp/tmpcdj_i5gq/27_mni_icbm_01_native_rms_tlaplace_30mm.txt.rgb /tmp/tmpcdj_i5gq/28_mni_icbm_01_native_rms_tlaplace_30mm.txt.rgb /tmp/tmpcdj_i5gq/29_mni_icbm_01_native_rms_tlaplace_30mm.txt.rgb /tmp/tmpcdj_i5gq/30_mni_icbm_01_native_rms_tlaplace_30mm.txt.rgb /tmp/tmpcdj_i5gq/31_mni_icbm_01_native_rms_tlaplace_30mm.txt.rgb /tmp/tmpcdj_i5gq/32_mni_icbm_01_native_rms_tlaplace_30mm.txt.rgb /tmp/tmpcdj_i5gq/33_mni_icbm_01_native_rms_tlaplace_30mm.txt.rgb /tmp/tmpcdj_i5gq/34_mni_icbm_01_native_rms_tlaplace_30mm.txt.rgb /tmp/tmpcdj_i5gq/35_mni_icbm_01_native_rms_tlaplace_30mm.txt.rgb /tmp/tmpcdj_i5gq/36_mni_icbm_01_gray_surface_81920.smtherr.txt.rgb /tmp/tmpcdj_i5gq/37_mni_icbm_01_gray_surface_81920.smtherr.txt.rgb /tmp/tmpcdj_i5gq/38_mni_icbm_01_gray_surface_81920.smtherr.txt.rgb /tmp/tmpcdj_i5gq/39_mni_icbm_01_gray_surface_81920.smtherr.txt.rgb /tmp/tmpcdj_i5gq/40_mni_icbm_01_gray_surface_81920.smtherr.txt.rgb /tmp/tmpcdj_i5gq/41_mni_icbm_01_gray_surface_81920.smtherr.txt.rgb /tmp/tmpcdj_i5gq/42_mni_icbm_01_gray_surface_81920.smtherr.txt.rgb /tmp/tmpcdj_i5gq/43_mni_icbm_01_gray_surface_81920.smtherr.txt.rgb /tmp/tmpcdj_i5gq/44_mni_icbm_01_gray_surface_81920.smtherr.txt.rgb /tmp/tmpcdj_i5gq/45_mni_icbm_01_gray_surface_81920.smtherr.txt.rgb /tmp/tmpcdj_i5gq/46_mni_icbm_01_gray_surface_81920.smtherr.txt.rgb /tmp/tmpcdj_i5gq/47_mni_icbm_01_gray_surface_81920.smtherr.txt.rgb /tmp/tmpcdj_i5gq/montage_output.png +convert -box black -fill green1 -pointsize 28 -annotate 0x0+53+110 L -annotate 0x0+349+110 R -annotate 0x0+455+110 R -annotate 0x0+751+110 L -annotate 0x0+2071+130 '' -annotate 0x0+53+610 R -annotate 0x0+349+610 L -annotate 0x0+455+610 L -annotate 0x0+751+610 R -annotate 0x0+2071+630 '' -annotate 0x0+53+1110 L -annotate 0x0+349+1110 R -annotate 0x0+455+1110 R -annotate 0x0+751+1110 L -annotate 0x0+2071+1130 '' -annotate 0x0+53+1610 R -annotate 0x0+349+1610 L -annotate 0x0+455+1610 L -annotate 0x0+751+1610 R -annotate 0x0+2071+1630 '' -annotate 0x0+53+2110 L -annotate 0x0+349+2110 R -annotate 0x0+455+2110 R -annotate 0x0+751+2110 L -annotate 0x0+2071+2130 'Column0: + Maximum: 3.98805 + Minimum: 1.4768 + Median: 3.0403 + Mean: 3.01144 + Stdev: 0.357987 + Sum: 123355 +' -annotate 0x0+53+2610 R -annotate 0x0+349+2610 L -annotate 0x0+455+2610 L -annotate 0x0+751+2610 R -annotate 0x0+2071+2630 'Column0: + Maximum: 4.02445 + Minimum: 1.47367 + Median: 3.04275 + Mean: 3.00986 + Stdev: 0.353593 + Sum: 123290 +' -annotate 0x0+53+3110 L -annotate 0x0+349+3110 R -annotate 0x0+455+3110 R -annotate 0x0+751+3110 L -annotate 0x0+2071+3130 'Column0: + Maximum: 42.6139 + Minimum: 7.19126e-09 + Median: 0.433313 + Mean: 1.0035 + Stdev: 1.64782 + Sum: 41105.2 +' -annotate 0x0+53+3610 R -annotate 0x0+349+3610 L -annotate 0x0+455+3610 L -annotate 0x0+751+3610 R -annotate 0x0+2071+3630 'Column0: + Maximum: 64.7933 + Minimum: 8.08211e-09 + Median: 0.428238 + Mean: 1.00115 + Stdev: 1.82946 + Sum: 41009.1 +' -annotate 0x0+905+25 mni_icbm_01_gray_surface_81920.obj -annotate 0x0+905+1025 mni_icbm_01_white_surface_81920.obj -annotate 0x0+905+2025 mni_icbm_01_native_rms_tlaplace_30mm.txt -annotate 0x0+905+3025 mni_icbm_01_gray_surface_81920.smtherr.txt /tmp/tmpcdj_i5gq/montage_output.png /examples/outgoing/same_folder.png diff --git a/examples/outgoing/same_folder.png b/examples/outgoing/same_folder.png index e4212e6..bf8f9b9 100644 Binary files a/examples/outgoing/same_folder.png and b/examples/outgoing/same_folder.png differ diff --git a/examples/outgoing/separate_folders/mni_icbm.log b/examples/outgoing/separate_folders/mni_icbm.log index 5fad71f..1683068 100644 --- a/examples/outgoing/separate_folders/mni_icbm.log +++ b/examples/outgoing/separate_folders/mni_icbm.log @@ -1,58 +1,88 @@ -average_surfaces /tmp/axORTPI0D1/mid_surface_left.obj none none 1 /share/incoming/separate_folders/mni_icbm-left/mni_icbm_02_gray_surface_81920.obj /share/incoming/separate_folders/mni_icbm-left/mni_icbm_02_white_surface_81920.obj -1: /share/incoming/separate_folders/mni_icbm-left/mni_icbm_02_gray_surface_81920.obj -2: /share/incoming/separate_folders/mni_icbm-left/mni_icbm_02_white_surface_81920.obj -Objects output. -average_surfaces /tmp/axORTPI0D1/mid_surface_right.obj none none 1 /share/incoming/separate_folders/mni_icbm-right/mni_icbm_02_gray_surface_81920.obj /share/incoming/separate_folders/mni_icbm-right/mni_icbm_02_white_surface_81920.obj -1: /share/incoming/separate_folders/mni_icbm-right/mni_icbm_02_gray_surface_81920.obj -2: /share/incoming/separate_folders/mni_icbm-right/mni_icbm_02_white_surface_81920.obj -Objects output. -ray_trace -shadows -output /tmp/axORTPI0D1/1_hemi_default.rgb /share/incoming/separate_folders/mni_icbm-left/mni_icbm_02_gray_surface_81920.obj -bg white -crop -view 0.77 -0.18 -0.6 0.55 0.6 0.55 -ray_trace -shadows -output /tmp/axORTPI0D1/1_hemi_left.rgb /share/incoming/separate_folders/mni_icbm-left/mni_icbm_02_gray_surface_81920.obj -bg white -crop -left -ray_trace -shadows -output /tmp/axORTPI0D1/1_hemi_right.rgb /share/incoming/separate_folders/mni_icbm-left/mni_icbm_02_gray_surface_81920.obj -bg white -crop -right -ray_trace -shadows -output /tmp/axORTPI0D1/1_surf_top.rgb /share/incoming/separate_folders/mni_icbm-left/mni_icbm_02_gray_surface_81920.obj /share/incoming/separate_folders/mni_icbm-right/mni_icbm_02_gray_surface_81920.obj -bg white -crop -top -ray_trace -shadows -output /tmp/axORTPI0D1/1_surf_bottom.rgb /share/incoming/separate_folders/mni_icbm-left/mni_icbm_02_gray_surface_81920.obj /share/incoming/separate_folders/mni_icbm-right/mni_icbm_02_gray_surface_81920.obj -bg white -crop -bottom -ray_trace -shadows -output /tmp/axORTPI0D1/2_hemi_flipped.rgb /share/incoming/separate_folders/mni_icbm-right/mni_icbm_02_gray_surface_81920.obj -bg white -crop -view -0.77 -0.18 -0.6 -0.55 0.6 0.55 -ray_trace -shadows -output /tmp/axORTPI0D1/2_hemi_right.rgb /share/incoming/separate_folders/mni_icbm-right/mni_icbm_02_gray_surface_81920.obj -bg white -crop -right -ray_trace -shadows -output /tmp/axORTPI0D1/2_hemi_left.rgb /share/incoming/separate_folders/mni_icbm-right/mni_icbm_02_gray_surface_81920.obj -bg white -crop -left -ray_trace -shadows -output /tmp/axORTPI0D1/2_surf_front.rgb /share/incoming/separate_folders/mni_icbm-left/mni_icbm_02_gray_surface_81920.obj /share/incoming/separate_folders/mni_icbm-right/mni_icbm_02_gray_surface_81920.obj -bg white -crop -front -ray_trace -shadows -output /tmp/axORTPI0D1/2_surf_back.rgb /share/incoming/separate_folders/mni_icbm-left/mni_icbm_02_gray_surface_81920.obj /share/incoming/separate_folders/mni_icbm-right/mni_icbm_02_gray_surface_81920.obj -bg white -crop -back -ray_trace -shadows -output /tmp/axORTPI0D1/3_hemi_default.rgb /share/incoming/separate_folders/mni_icbm-left/mni_icbm_02_white_surface_81920.obj -bg white -crop -view 0.77 -0.18 -0.6 0.55 0.6 0.55 -ray_trace -shadows -output /tmp/axORTPI0D1/3_hemi_left.rgb /share/incoming/separate_folders/mni_icbm-left/mni_icbm_02_white_surface_81920.obj -bg white -crop -left -ray_trace -shadows -output /tmp/axORTPI0D1/3_hemi_right.rgb /share/incoming/separate_folders/mni_icbm-left/mni_icbm_02_white_surface_81920.obj -bg white -crop -right -ray_trace -shadows -output /tmp/axORTPI0D1/3_surf_top.rgb /share/incoming/separate_folders/mni_icbm-left/mni_icbm_02_white_surface_81920.obj /share/incoming/separate_folders/mni_icbm-right/mni_icbm_02_white_surface_81920.obj -bg white -crop -top -ray_trace -shadows -output /tmp/axORTPI0D1/3_surf_bottom.rgb /share/incoming/separate_folders/mni_icbm-left/mni_icbm_02_white_surface_81920.obj /share/incoming/separate_folders/mni_icbm-right/mni_icbm_02_white_surface_81920.obj -bg white -crop -bottom -ray_trace -shadows -output /tmp/axORTPI0D1/4_hemi_flipped.rgb /share/incoming/separate_folders/mni_icbm-right/mni_icbm_02_white_surface_81920.obj -bg white -crop -view -0.77 -0.18 -0.6 -0.55 0.6 0.55 -ray_trace -shadows -output /tmp/axORTPI0D1/4_hemi_right.rgb /share/incoming/separate_folders/mni_icbm-right/mni_icbm_02_white_surface_81920.obj -bg white -crop -right -ray_trace -shadows -output /tmp/axORTPI0D1/4_hemi_left.rgb /share/incoming/separate_folders/mni_icbm-right/mni_icbm_02_white_surface_81920.obj -bg white -crop -left -ray_trace -shadows -output /tmp/axORTPI0D1/4_surf_front.rgb /share/incoming/separate_folders/mni_icbm-left/mni_icbm_02_white_surface_81920.obj /share/incoming/separate_folders/mni_icbm-right/mni_icbm_02_white_surface_81920.obj -bg white -crop -front -ray_trace -shadows -output /tmp/axORTPI0D1/4_surf_back.rgb /share/incoming/separate_folders/mni_icbm-left/mni_icbm_02_white_surface_81920.obj /share/incoming/separate_folders/mni_icbm-right/mni_icbm_02_white_surface_81920.obj -bg white -crop -back -colour_object /tmp/axORTPI0D1/mid_surface_left.obj /share/incoming/separate_folders/mni_icbm-left/mni_icbm_02_gray_surface_81920.smtherr.txt /tmp/axORTPI0D1/4_mid_rms_left.obj spectral 0.0 2.0 -Value range: 7.19126e-09 42.6139 -colour_object /tmp/axORTPI0D1/mid_surface_right.obj /share/incoming/separate_folders/mni_icbm-right/mni_icbm_02_gray_surface_81920.smtherr.txt /tmp/axORTPI0D1/4_mid_rms_right.obj spectral 0.0 2.0 -Value range: 8.08211e-09 64.7933 -ray_trace -shadows -output /tmp/axORTPI0D1/5_mid_left_default.rgb /tmp/axORTPI0D1/4_mid_rms_left.obj -bg white -crop -view 0.77 -0.18 -0.6 0.55 0.6 0.55 -ray_trace -shadows -output /tmp/axORTPI0D1/5_mid_left_left.rgb /tmp/axORTPI0D1/4_mid_rms_left.obj -bg white -crop -left -ray_trace -shadows -output /tmp/axORTPI0D1/5_mid_left_right.rgb /tmp/axORTPI0D1/4_mid_rms_left.obj -bg white -crop -right -ray_trace -shadows -output /tmp/axORTPI0D1/5_mid_top.rgb /tmp/axORTPI0D1/4_mid_rms_left.obj /tmp/axORTPI0D1/4_mid_rms_right.obj -bg white -crop -top -ray_trace -shadows -output /tmp/axORTPI0D1/5_mid_bottom.rgb /tmp/axORTPI0D1/4_mid_rms_left.obj /tmp/axORTPI0D1/4_mid_rms_right.obj -bg white -crop -bottom -ray_trace -shadows -output /tmp/axORTPI0D1/6_mid_right_flipped.rgb /tmp/axORTPI0D1/4_mid_rms_right.obj -bg white -crop -view -0.77 -0.18 -0.6 -0.55 0.6 0.55 -ray_trace -shadows -output /tmp/axORTPI0D1/6_mid_right_right.rgb /tmp/axORTPI0D1/4_mid_rms_right.obj -bg white -crop -right -ray_trace -shadows -output /tmp/axORTPI0D1/6_mid_right_left.rgb /tmp/axORTPI0D1/4_mid_rms_right.obj -bg white -crop -left -ray_trace -shadows -output /tmp/axORTPI0D1/6_mid_front.rgb /tmp/axORTPI0D1/4_mid_rms_left.obj /tmp/axORTPI0D1/4_mid_rms_right.obj -bg white -crop -front -ray_trace -shadows -output /tmp/axORTPI0D1/6_mid_back.rgb /tmp/axORTPI0D1/4_mid_rms_left.obj /tmp/axORTPI0D1/4_mid_rms_right.obj -bg white -crop -back -colour_object /tmp/axORTPI0D1/mid_surface_left.obj /share/incoming/separate_folders/mni_icbm-left/mni_icbm_02_native_rms_tlaplace_30mm.txt /tmp/axORTPI0D1/6_mid_rms_left.obj spectral 0.0 10.0 -Value range: 1.4768 3.98805 -colour_object /tmp/axORTPI0D1/mid_surface_right.obj /share/incoming/separate_folders/mni_icbm-right/mni_icbm_02_native_rms_tlaplace_30mm.txt /tmp/axORTPI0D1/6_mid_rms_right.obj spectral 0.0 10.0 -Value range: 1.47367 4.02445 -ray_trace -shadows -output /tmp/axORTPI0D1/7_mid_left_default.rgb /tmp/axORTPI0D1/6_mid_rms_left.obj -bg white -crop -view 0.77 -0.18 -0.6 0.55 0.6 0.55 -ray_trace -shadows -output /tmp/axORTPI0D1/7_mid_left_left.rgb /tmp/axORTPI0D1/6_mid_rms_left.obj -bg white -crop -left -ray_trace -shadows -output /tmp/axORTPI0D1/7_mid_left_right.rgb /tmp/axORTPI0D1/6_mid_rms_left.obj -bg white -crop -right -ray_trace -shadows -output /tmp/axORTPI0D1/7_mid_top.rgb /tmp/axORTPI0D1/6_mid_rms_left.obj /tmp/axORTPI0D1/6_mid_rms_right.obj -bg white -crop -top -ray_trace -shadows -output /tmp/axORTPI0D1/7_mid_bottom.rgb /tmp/axORTPI0D1/6_mid_rms_left.obj /tmp/axORTPI0D1/6_mid_rms_right.obj -bg white -crop -bottom -ray_trace -shadows -output /tmp/axORTPI0D1/8_mid_right_flipped.rgb /tmp/axORTPI0D1/6_mid_rms_right.obj -bg white -crop -view -0.77 -0.18 -0.6 -0.55 0.6 0.55 -ray_trace -shadows -output /tmp/axORTPI0D1/8_mid_right_right.rgb /tmp/axORTPI0D1/6_mid_rms_right.obj -bg white -crop -right -ray_trace -shadows -output /tmp/axORTPI0D1/8_mid_right_left.rgb /tmp/axORTPI0D1/6_mid_rms_right.obj -bg white -crop -left -ray_trace -shadows -output /tmp/axORTPI0D1/8_mid_front.rgb /tmp/axORTPI0D1/6_mid_rms_left.obj /tmp/axORTPI0D1/6_mid_rms_right.obj -bg white -crop -front -ray_trace -shadows -output /tmp/axORTPI0D1/8_mid_back.rgb /tmp/axORTPI0D1/6_mid_rms_left.obj /tmp/axORTPI0D1/6_mid_rms_right.obj -bg white -crop -back -montage -tile 5x8 -background white -geometry 200x200+1+1 /tmp/axORTPI0D1/1_hemi_default.rgb /tmp/axORTPI0D1/1_hemi_left.rgb /tmp/axORTPI0D1/1_hemi_right.rgb /tmp/axORTPI0D1/1_surf_top.rgb /tmp/axORTPI0D1/1_surf_bottom.rgb /tmp/axORTPI0D1/2_hemi_flipped.rgb /tmp/axORTPI0D1/2_hemi_right.rgb /tmp/axORTPI0D1/2_hemi_left.rgb /tmp/axORTPI0D1/2_surf_front.rgb /tmp/axORTPI0D1/2_surf_back.rgb /tmp/axORTPI0D1/3_hemi_default.rgb /tmp/axORTPI0D1/3_hemi_left.rgb /tmp/axORTPI0D1/3_hemi_right.rgb /tmp/axORTPI0D1/3_surf_top.rgb /tmp/axORTPI0D1/3_surf_bottom.rgb /tmp/axORTPI0D1/4_hemi_flipped.rgb /tmp/axORTPI0D1/4_hemi_right.rgb /tmp/axORTPI0D1/4_hemi_left.rgb /tmp/axORTPI0D1/4_surf_front.rgb /tmp/axORTPI0D1/4_surf_back.rgb /tmp/axORTPI0D1/5_mid_left_default.rgb /tmp/axORTPI0D1/5_mid_left_left.rgb /tmp/axORTPI0D1/5_mid_left_right.rgb /tmp/axORTPI0D1/5_mid_top.rgb /tmp/axORTPI0D1/5_mid_bottom.rgb /tmp/axORTPI0D1/6_mid_right_flipped.rgb /tmp/axORTPI0D1/6_mid_right_right.rgb /tmp/axORTPI0D1/6_mid_right_left.rgb /tmp/axORTPI0D1/6_mid_front.rgb /tmp/axORTPI0D1/6_mid_back.rgb /tmp/axORTPI0D1/7_mid_left_default.rgb /tmp/axORTPI0D1/7_mid_left_left.rgb /tmp/axORTPI0D1/7_mid_left_right.rgb /tmp/axORTPI0D1/7_mid_top.rgb /tmp/axORTPI0D1/7_mid_bottom.rgb /tmp/axORTPI0D1/8_mid_right_flipped.rgb /tmp/axORTPI0D1/8_mid_right_right.rgb /tmp/axORTPI0D1/8_mid_right_left.rgb /tmp/axORTPI0D1/8_mid_front.rgb /tmp/axORTPI0D1/8_mid_back.rgb /tmp/axORTPI0D1/mont.png -convert -box white -stroke green -pointsize 16 -font DejaVu-Sans -annotate 0x0+400+15 mni_icbm -annotate 0x0+637+28 L -annotate 0x0+768+28 R -annotate 0x0+837+28 R -annotate 0x0+968+28 L -annotate 0x0+637+224 R -annotate 0x0+768+224 L -annotate 0x0+837+224 L -annotate 0x0+968+224 R -annotate 0x0+400+215 mni_icbm_02_gray_surface_81920.obj -annotate 0x0+637+441 L -annotate 0x0+768+441 R -annotate 0x0+837+441 R -annotate 0x0+968+441 L -annotate 0x0+637+637 R -annotate 0x0+768+637 L -annotate 0x0+837+637 L -annotate 0x0+968+637 R -annotate 0x0+400+628 mni_icbm_02_white_surface_81920.obj -annotate 0x0+340+1041 mni_icbm_02_gray_surface_81920.smtherr.txt -annotate 0x0+340+1441 mni_icbm_02_native_rms_tlaplace_30mm.txt /tmp/axORTPI0D1/mont.png /share/outgoing/separate_folders/mni_icbm.png +average_surfaces /tmp/tmp1scgh3oo/mni_icbm_left.obj none none 1 /examples/incoming/separate_folders/mni_icbm-left/mni_icbm_02_gray_surface_81920.obj /examples/incoming/separate_folders/mni_icbm-left/mni_icbm_02_white_surface_81920.obj +average_surfaces /tmp/tmp1scgh3oo/mni_icbm_right.obj none none 1 /examples/incoming/separate_folders/mni_icbm-right/mni_icbm_02_gray_surface_81920.obj /examples/incoming/separate_folders/mni_icbm-right/mni_icbm_02_white_surface_81920.obj +colour_object /tmp/tmp1scgh3oo/mni_icbm_left.obj /examples/incoming/separate_folders/mni_icbm-left/mni_icbm_02_gray_surface_81920.smtherr.txt /tmp/tmp1scgh3oo/mni_icbm_left.obj_mni_icbm_02_gray_surface_81920.smtherr.txt_spectral_0.0_2.0.obj spectral 0.0 2.0 +vertstats_stats /examples/incoming/separate_folders/mni_icbm-left/mni_icbm_02_gray_surface_81920.smtherr.txt +colour_object /tmp/tmp1scgh3oo/mni_icbm_right.obj /examples/incoming/separate_folders/mni_icbm-right/mni_icbm_02_gray_surface_81920.smtherr.txt /tmp/tmp1scgh3oo/mni_icbm_right.obj_mni_icbm_02_gray_surface_81920.smtherr.txt_spectral_0.0_2.0.obj spectral 0.0 2.0 +vertstats_stats /examples/incoming/separate_folders/mni_icbm-right/mni_icbm_02_gray_surface_81920.smtherr.txt +colour_object /tmp/tmp1scgh3oo/mni_icbm_left.obj /examples/incoming/separate_folders/mni_icbm-left/mni_icbm_02_native_rms_tlaplace_30mm.txt /tmp/tmp1scgh3oo/mni_icbm_left.obj_mni_icbm_02_native_rms_tlaplace_30mm.txt_spectral_0.0_10.0.obj spectral 0.0 10.0 +vertstats_stats /examples/incoming/separate_folders/mni_icbm-left/mni_icbm_02_native_rms_tlaplace_30mm.txt +colour_object /tmp/tmp1scgh3oo/mni_icbm_right.obj /examples/incoming/separate_folders/mni_icbm-right/mni_icbm_02_native_rms_tlaplace_30mm.txt /tmp/tmp1scgh3oo/mni_icbm_right.obj_mni_icbm_02_native_rms_tlaplace_30mm.txt_spectral_0.0_10.0.obj spectral 0.0 10.0 +vertstats_stats /examples/incoming/separate_folders/mni_icbm-right/mni_icbm_02_native_rms_tlaplace_30mm.txt +ray_trace -shadows -output /tmp/tmp1scgh3oo/0_mni_icbm_02_gray_surface_81920.obj.rgb -bg black -crop -size 400 400 -top /examples/incoming/separate_folders/mni_icbm-left/mni_icbm_02_gray_surface_81920.obj /examples/incoming/separate_folders/mni_icbm-right/mni_icbm_02_gray_surface_81920.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/1_mni_icbm_02_gray_surface_81920.obj.rgb -bg black -crop -size 400 400 -bottom /examples/incoming/separate_folders/mni_icbm-left/mni_icbm_02_gray_surface_81920.obj /examples/incoming/separate_folders/mni_icbm-right/mni_icbm_02_gray_surface_81920.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/2_mni_icbm_02_gray_surface_81920.obj.rgb -bg black -crop -size 400 400 -view 0.77 -0.18 -0.6 0.55 0.6 0.55 /examples/incoming/separate_folders/mni_icbm-left/mni_icbm_02_gray_surface_81920.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/3_mni_icbm_02_gray_surface_81920.obj.rgb -bg black -crop -size 400 400 -left /examples/incoming/separate_folders/mni_icbm-left/mni_icbm_02_gray_surface_81920.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/4_mni_icbm_02_gray_surface_81920.obj.rgb -bg black -crop -size 400 400 -right /examples/incoming/separate_folders/mni_icbm-left/mni_icbm_02_gray_surface_81920.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/5_mni_icbm_02_gray_surface_81920.obj.rgb -bg black -crop -size 400 400 +ray_trace -shadows -output /tmp/tmp1scgh3oo/6_mni_icbm_02_gray_surface_81920.obj.rgb -bg black -crop -size 400 400 -front /examples/incoming/separate_folders/mni_icbm-left/mni_icbm_02_gray_surface_81920.obj /examples/incoming/separate_folders/mni_icbm-right/mni_icbm_02_gray_surface_81920.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/7_mni_icbm_02_gray_surface_81920.obj.rgb -bg black -crop -size 400 400 -back /examples/incoming/separate_folders/mni_icbm-left/mni_icbm_02_gray_surface_81920.obj /examples/incoming/separate_folders/mni_icbm-right/mni_icbm_02_gray_surface_81920.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/8_mni_icbm_02_gray_surface_81920.obj.rgb -bg black -crop -size 400 400 -view -0.77 -0.18 -0.6 -0.55 0.6 0.55 /examples/incoming/separate_folders/mni_icbm-right/mni_icbm_02_gray_surface_81920.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/9_mni_icbm_02_gray_surface_81920.obj.rgb -bg black -crop -size 400 400 -left /examples/incoming/separate_folders/mni_icbm-right/mni_icbm_02_gray_surface_81920.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/10_mni_icbm_02_gray_surface_81920.obj.rgb -bg black -crop -size 400 400 -right /examples/incoming/separate_folders/mni_icbm-right/mni_icbm_02_gray_surface_81920.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/11_mni_icbm_02_gray_surface_81920.obj.rgb -bg black -crop -size 400 400 +ray_trace -shadows -output /tmp/tmp1scgh3oo/12_mni_icbm_02_white_surface_81920.obj.rgb -bg black -crop -size 400 400 -top /examples/incoming/separate_folders/mni_icbm-left/mni_icbm_02_white_surface_81920.obj /examples/incoming/separate_folders/mni_icbm-right/mni_icbm_02_white_surface_81920.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/13_mni_icbm_02_white_surface_81920.obj.rgb -bg black -crop -size 400 400 -bottom /examples/incoming/separate_folders/mni_icbm-left/mni_icbm_02_white_surface_81920.obj /examples/incoming/separate_folders/mni_icbm-right/mni_icbm_02_white_surface_81920.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/14_mni_icbm_02_white_surface_81920.obj.rgb -bg black -crop -size 400 400 -view 0.77 -0.18 -0.6 0.55 0.6 0.55 /examples/incoming/separate_folders/mni_icbm-left/mni_icbm_02_white_surface_81920.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/15_mni_icbm_02_white_surface_81920.obj.rgb -bg black -crop -size 400 400 -left /examples/incoming/separate_folders/mni_icbm-left/mni_icbm_02_white_surface_81920.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/16_mni_icbm_02_white_surface_81920.obj.rgb -bg black -crop -size 400 400 -right /examples/incoming/separate_folders/mni_icbm-left/mni_icbm_02_white_surface_81920.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/17_mni_icbm_02_white_surface_81920.obj.rgb -bg black -crop -size 400 400 +ray_trace -shadows -output /tmp/tmp1scgh3oo/18_mni_icbm_02_white_surface_81920.obj.rgb -bg black -crop -size 400 400 -front /examples/incoming/separate_folders/mni_icbm-left/mni_icbm_02_white_surface_81920.obj /examples/incoming/separate_folders/mni_icbm-right/mni_icbm_02_white_surface_81920.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/19_mni_icbm_02_white_surface_81920.obj.rgb -bg black -crop -size 400 400 -back /examples/incoming/separate_folders/mni_icbm-left/mni_icbm_02_white_surface_81920.obj /examples/incoming/separate_folders/mni_icbm-right/mni_icbm_02_white_surface_81920.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/20_mni_icbm_02_white_surface_81920.obj.rgb -bg black -crop -size 400 400 -view -0.77 -0.18 -0.6 -0.55 0.6 0.55 /examples/incoming/separate_folders/mni_icbm-right/mni_icbm_02_white_surface_81920.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/21_mni_icbm_02_white_surface_81920.obj.rgb -bg black -crop -size 400 400 -left /examples/incoming/separate_folders/mni_icbm-right/mni_icbm_02_white_surface_81920.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/22_mni_icbm_02_white_surface_81920.obj.rgb -bg black -crop -size 400 400 -right /examples/incoming/separate_folders/mni_icbm-right/mni_icbm_02_white_surface_81920.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/23_mni_icbm_02_white_surface_81920.obj.rgb -bg black -crop -size 400 400 +ray_trace -shadows -output /tmp/tmp1scgh3oo/24_mni_icbm_02_gray_surface_81920.smtherr.txt.rgb -bg black -crop -size 400 400 -top /tmp/tmp1scgh3oo/mni_icbm_left.obj_mni_icbm_02_gray_surface_81920.smtherr.txt_spectral_0.0_2.0.obj /tmp/tmp1scgh3oo/mni_icbm_right.obj_mni_icbm_02_gray_surface_81920.smtherr.txt_spectral_0.0_2.0.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/25_mni_icbm_02_gray_surface_81920.smtherr.txt.rgb -bg black -crop -size 400 400 -bottom /tmp/tmp1scgh3oo/mni_icbm_left.obj_mni_icbm_02_gray_surface_81920.smtherr.txt_spectral_0.0_2.0.obj /tmp/tmp1scgh3oo/mni_icbm_right.obj_mni_icbm_02_gray_surface_81920.smtherr.txt_spectral_0.0_2.0.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/26_mni_icbm_02_gray_surface_81920.smtherr.txt.rgb -bg black -crop -size 400 400 -view 0.77 -0.18 -0.6 0.55 0.6 0.55 /tmp/tmp1scgh3oo/mni_icbm_left.obj_mni_icbm_02_gray_surface_81920.smtherr.txt_spectral_0.0_2.0.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/27_mni_icbm_02_gray_surface_81920.smtherr.txt.rgb -bg black -crop -size 400 400 -left /tmp/tmp1scgh3oo/mni_icbm_left.obj_mni_icbm_02_gray_surface_81920.smtherr.txt_spectral_0.0_2.0.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/28_mni_icbm_02_gray_surface_81920.smtherr.txt.rgb -bg black -crop -size 400 400 -right /tmp/tmp1scgh3oo/mni_icbm_left.obj_mni_icbm_02_gray_surface_81920.smtherr.txt_spectral_0.0_2.0.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/29_mni_icbm_02_gray_surface_81920.smtherr.txt.rgb -bg black -crop -size 400 400 +ray_trace -shadows -output /tmp/tmp1scgh3oo/30_mni_icbm_02_gray_surface_81920.smtherr.txt.rgb -bg black -crop -size 400 400 -front /tmp/tmp1scgh3oo/mni_icbm_left.obj_mni_icbm_02_gray_surface_81920.smtherr.txt_spectral_0.0_2.0.obj /tmp/tmp1scgh3oo/mni_icbm_right.obj_mni_icbm_02_gray_surface_81920.smtherr.txt_spectral_0.0_2.0.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/31_mni_icbm_02_gray_surface_81920.smtherr.txt.rgb -bg black -crop -size 400 400 -back /tmp/tmp1scgh3oo/mni_icbm_left.obj_mni_icbm_02_gray_surface_81920.smtherr.txt_spectral_0.0_2.0.obj /tmp/tmp1scgh3oo/mni_icbm_right.obj_mni_icbm_02_gray_surface_81920.smtherr.txt_spectral_0.0_2.0.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/32_mni_icbm_02_gray_surface_81920.smtherr.txt.rgb -bg black -crop -size 400 400 -view -0.77 -0.18 -0.6 -0.55 0.6 0.55 /tmp/tmp1scgh3oo/mni_icbm_right.obj_mni_icbm_02_gray_surface_81920.smtherr.txt_spectral_0.0_2.0.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/33_mni_icbm_02_gray_surface_81920.smtherr.txt.rgb -bg black -crop -size 400 400 -left /tmp/tmp1scgh3oo/mni_icbm_right.obj_mni_icbm_02_gray_surface_81920.smtherr.txt_spectral_0.0_2.0.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/34_mni_icbm_02_gray_surface_81920.smtherr.txt.rgb -bg black -crop -size 400 400 -right /tmp/tmp1scgh3oo/mni_icbm_right.obj_mni_icbm_02_gray_surface_81920.smtherr.txt_spectral_0.0_2.0.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/35_mni_icbm_02_gray_surface_81920.smtherr.txt.rgb -bg black -crop -size 400 400 +ray_trace -shadows -output /tmp/tmp1scgh3oo/36_mni_icbm_02_native_rms_tlaplace_30mm.txt.rgb -bg black -crop -size 400 400 -top /tmp/tmp1scgh3oo/mni_icbm_left.obj_mni_icbm_02_native_rms_tlaplace_30mm.txt_spectral_0.0_10.0.obj /tmp/tmp1scgh3oo/mni_icbm_right.obj_mni_icbm_02_native_rms_tlaplace_30mm.txt_spectral_0.0_10.0.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/37_mni_icbm_02_native_rms_tlaplace_30mm.txt.rgb -bg black -crop -size 400 400 -bottom /tmp/tmp1scgh3oo/mni_icbm_left.obj_mni_icbm_02_native_rms_tlaplace_30mm.txt_spectral_0.0_10.0.obj /tmp/tmp1scgh3oo/mni_icbm_right.obj_mni_icbm_02_native_rms_tlaplace_30mm.txt_spectral_0.0_10.0.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/38_mni_icbm_02_native_rms_tlaplace_30mm.txt.rgb -bg black -crop -size 400 400 -view 0.77 -0.18 -0.6 0.55 0.6 0.55 /tmp/tmp1scgh3oo/mni_icbm_left.obj_mni_icbm_02_native_rms_tlaplace_30mm.txt_spectral_0.0_10.0.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/39_mni_icbm_02_native_rms_tlaplace_30mm.txt.rgb -bg black -crop -size 400 400 -left /tmp/tmp1scgh3oo/mni_icbm_left.obj_mni_icbm_02_native_rms_tlaplace_30mm.txt_spectral_0.0_10.0.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/40_mni_icbm_02_native_rms_tlaplace_30mm.txt.rgb -bg black -crop -size 400 400 -right /tmp/tmp1scgh3oo/mni_icbm_left.obj_mni_icbm_02_native_rms_tlaplace_30mm.txt_spectral_0.0_10.0.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/41_mni_icbm_02_native_rms_tlaplace_30mm.txt.rgb -bg black -crop -size 400 400 +ray_trace -shadows -output /tmp/tmp1scgh3oo/42_mni_icbm_02_native_rms_tlaplace_30mm.txt.rgb -bg black -crop -size 400 400 -front /tmp/tmp1scgh3oo/mni_icbm_left.obj_mni_icbm_02_native_rms_tlaplace_30mm.txt_spectral_0.0_10.0.obj /tmp/tmp1scgh3oo/mni_icbm_right.obj_mni_icbm_02_native_rms_tlaplace_30mm.txt_spectral_0.0_10.0.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/43_mni_icbm_02_native_rms_tlaplace_30mm.txt.rgb -bg black -crop -size 400 400 -back /tmp/tmp1scgh3oo/mni_icbm_left.obj_mni_icbm_02_native_rms_tlaplace_30mm.txt_spectral_0.0_10.0.obj /tmp/tmp1scgh3oo/mni_icbm_right.obj_mni_icbm_02_native_rms_tlaplace_30mm.txt_spectral_0.0_10.0.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/44_mni_icbm_02_native_rms_tlaplace_30mm.txt.rgb -bg black -crop -size 400 400 -view -0.77 -0.18 -0.6 -0.55 0.6 0.55 /tmp/tmp1scgh3oo/mni_icbm_right.obj_mni_icbm_02_native_rms_tlaplace_30mm.txt_spectral_0.0_10.0.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/45_mni_icbm_02_native_rms_tlaplace_30mm.txt.rgb -bg black -crop -size 400 400 -left /tmp/tmp1scgh3oo/mni_icbm_right.obj_mni_icbm_02_native_rms_tlaplace_30mm.txt_spectral_0.0_10.0.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/46_mni_icbm_02_native_rms_tlaplace_30mm.txt.rgb -bg black -crop -size 400 400 -right /tmp/tmp1scgh3oo/mni_icbm_right.obj_mni_icbm_02_native_rms_tlaplace_30mm.txt_spectral_0.0_10.0.obj +ray_trace -shadows -output /tmp/tmp1scgh3oo/47_mni_icbm_02_native_rms_tlaplace_30mm.txt.rgb -bg black -crop -size 400 400 +montage -tile 6x8 -background black -geometry 400x400+1+50 /tmp/tmp1scgh3oo/0_mni_icbm_02_gray_surface_81920.obj.rgb /tmp/tmp1scgh3oo/1_mni_icbm_02_gray_surface_81920.obj.rgb /tmp/tmp1scgh3oo/2_mni_icbm_02_gray_surface_81920.obj.rgb /tmp/tmp1scgh3oo/3_mni_icbm_02_gray_surface_81920.obj.rgb /tmp/tmp1scgh3oo/4_mni_icbm_02_gray_surface_81920.obj.rgb /tmp/tmp1scgh3oo/5_mni_icbm_02_gray_surface_81920.obj.rgb /tmp/tmp1scgh3oo/6_mni_icbm_02_gray_surface_81920.obj.rgb /tmp/tmp1scgh3oo/7_mni_icbm_02_gray_surface_81920.obj.rgb /tmp/tmp1scgh3oo/8_mni_icbm_02_gray_surface_81920.obj.rgb /tmp/tmp1scgh3oo/9_mni_icbm_02_gray_surface_81920.obj.rgb /tmp/tmp1scgh3oo/10_mni_icbm_02_gray_surface_81920.obj.rgb /tmp/tmp1scgh3oo/11_mni_icbm_02_gray_surface_81920.obj.rgb /tmp/tmp1scgh3oo/12_mni_icbm_02_white_surface_81920.obj.rgb /tmp/tmp1scgh3oo/13_mni_icbm_02_white_surface_81920.obj.rgb /tmp/tmp1scgh3oo/14_mni_icbm_02_white_surface_81920.obj.rgb /tmp/tmp1scgh3oo/15_mni_icbm_02_white_surface_81920.obj.rgb /tmp/tmp1scgh3oo/16_mni_icbm_02_white_surface_81920.obj.rgb /tmp/tmp1scgh3oo/17_mni_icbm_02_white_surface_81920.obj.rgb /tmp/tmp1scgh3oo/18_mni_icbm_02_white_surface_81920.obj.rgb /tmp/tmp1scgh3oo/19_mni_icbm_02_white_surface_81920.obj.rgb /tmp/tmp1scgh3oo/20_mni_icbm_02_white_surface_81920.obj.rgb /tmp/tmp1scgh3oo/21_mni_icbm_02_white_surface_81920.obj.rgb /tmp/tmp1scgh3oo/22_mni_icbm_02_white_surface_81920.obj.rgb /tmp/tmp1scgh3oo/23_mni_icbm_02_white_surface_81920.obj.rgb /tmp/tmp1scgh3oo/24_mni_icbm_02_gray_surface_81920.smtherr.txt.rgb /tmp/tmp1scgh3oo/25_mni_icbm_02_gray_surface_81920.smtherr.txt.rgb /tmp/tmp1scgh3oo/26_mni_icbm_02_gray_surface_81920.smtherr.txt.rgb /tmp/tmp1scgh3oo/27_mni_icbm_02_gray_surface_81920.smtherr.txt.rgb /tmp/tmp1scgh3oo/28_mni_icbm_02_gray_surface_81920.smtherr.txt.rgb /tmp/tmp1scgh3oo/29_mni_icbm_02_gray_surface_81920.smtherr.txt.rgb /tmp/tmp1scgh3oo/30_mni_icbm_02_gray_surface_81920.smtherr.txt.rgb /tmp/tmp1scgh3oo/31_mni_icbm_02_gray_surface_81920.smtherr.txt.rgb /tmp/tmp1scgh3oo/32_mni_icbm_02_gray_surface_81920.smtherr.txt.rgb /tmp/tmp1scgh3oo/33_mni_icbm_02_gray_surface_81920.smtherr.txt.rgb /tmp/tmp1scgh3oo/34_mni_icbm_02_gray_surface_81920.smtherr.txt.rgb /tmp/tmp1scgh3oo/35_mni_icbm_02_gray_surface_81920.smtherr.txt.rgb /tmp/tmp1scgh3oo/36_mni_icbm_02_native_rms_tlaplace_30mm.txt.rgb /tmp/tmp1scgh3oo/37_mni_icbm_02_native_rms_tlaplace_30mm.txt.rgb /tmp/tmp1scgh3oo/38_mni_icbm_02_native_rms_tlaplace_30mm.txt.rgb /tmp/tmp1scgh3oo/39_mni_icbm_02_native_rms_tlaplace_30mm.txt.rgb /tmp/tmp1scgh3oo/40_mni_icbm_02_native_rms_tlaplace_30mm.txt.rgb /tmp/tmp1scgh3oo/41_mni_icbm_02_native_rms_tlaplace_30mm.txt.rgb /tmp/tmp1scgh3oo/42_mni_icbm_02_native_rms_tlaplace_30mm.txt.rgb /tmp/tmp1scgh3oo/43_mni_icbm_02_native_rms_tlaplace_30mm.txt.rgb /tmp/tmp1scgh3oo/44_mni_icbm_02_native_rms_tlaplace_30mm.txt.rgb /tmp/tmp1scgh3oo/45_mni_icbm_02_native_rms_tlaplace_30mm.txt.rgb /tmp/tmp1scgh3oo/46_mni_icbm_02_native_rms_tlaplace_30mm.txt.rgb /tmp/tmp1scgh3oo/47_mni_icbm_02_native_rms_tlaplace_30mm.txt.rgb /tmp/tmp1scgh3oo/montage_output.png +convert -box black -fill green1 -pointsize 28 -annotate 0x0+53+110 L -annotate 0x0+349+110 R -annotate 0x0+455+110 R -annotate 0x0+751+110 L -annotate 0x0+2071+130 '' -annotate 0x0+53+610 R -annotate 0x0+349+610 L -annotate 0x0+455+610 L -annotate 0x0+751+610 R -annotate 0x0+2071+630 '' -annotate 0x0+53+1110 L -annotate 0x0+349+1110 R -annotate 0x0+455+1110 R -annotate 0x0+751+1110 L -annotate 0x0+2071+1130 '' -annotate 0x0+53+1610 R -annotate 0x0+349+1610 L -annotate 0x0+455+1610 L -annotate 0x0+751+1610 R -annotate 0x0+2071+1630 '' -annotate 0x0+53+2110 L -annotate 0x0+349+2110 R -annotate 0x0+455+2110 R -annotate 0x0+751+2110 L -annotate 0x0+2071+2130 'Column0: + Maximum: 42.6139 + Minimum: 7.19126e-09 + Median: 0.433313 + Mean: 1.0035 + Stdev: 1.64782 + Sum: 41105.2 +' -annotate 0x0+53+2610 R -annotate 0x0+349+2610 L -annotate 0x0+455+2610 L -annotate 0x0+751+2610 R -annotate 0x0+2071+2630 'Column0: + Maximum: 64.7933 + Minimum: 8.08211e-09 + Median: 0.428238 + Mean: 1.00115 + Stdev: 1.82946 + Sum: 41009.1 +' -annotate 0x0+53+3110 L -annotate 0x0+349+3110 R -annotate 0x0+455+3110 R -annotate 0x0+751+3110 L -annotate 0x0+2071+3130 'Column0: + Maximum: 3.98805 + Minimum: 1.4768 + Median: 3.0403 + Mean: 3.01144 + Stdev: 0.357987 + Sum: 123355 +' -annotate 0x0+53+3610 R -annotate 0x0+349+3610 L -annotate 0x0+455+3610 L -annotate 0x0+751+3610 R -annotate 0x0+2071+3630 'Column0: + Maximum: 4.02445 + Minimum: 1.47367 + Median: 3.04275 + Mean: 3.00986 + Stdev: 0.353593 + Sum: 123290 +' -annotate 0x0+905+25 mni_icbm_02_gray_surface_81920.obj -annotate 0x0+905+1025 mni_icbm_02_white_surface_81920.obj -annotate 0x0+905+2025 mni_icbm_02_gray_surface_81920.smtherr.txt -annotate 0x0+905+3025 mni_icbm_02_native_rms_tlaplace_30mm.txt /tmp/tmp1scgh3oo/montage_output.png /examples/outgoing/separate_folders/mni_icbm.png diff --git a/examples/outgoing/separate_folders/mni_icbm.png b/examples/outgoing/separate_folders/mni_icbm.png index f4eb5a7..f04871c 100644 Binary files a/examples/outgoing/separate_folders/mni_icbm.png and b/examples/outgoing/separate_folders/mni_icbm.png differ diff --git a/setup.py b/setup.py index 425af5d..e51c9e6 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -from setuptools import setup +from setuptools import setup, find_packages import re _version_re = re.compile(r"(?<=^__version__ = (\"|'))(.+)(?=\"|')") @@ -21,14 +21,13 @@ def get_version(rel_path: str) -> str: setup( name='surfigures', version=get_version('surfigures/__init__.py'), - packages=['surfigures'], + packages=find_packages('.' ,exclude='tests'), description='Create PNG figures of surfaces and vertex-wise data', author='Jennings Zhang', author_email='Jennings.Zhang@childrens.harvard.edu', url='https://github.com/FNNDSC/pl-surfigures', install_requires=['chris_plugin==0.2.0a1', 'loguru~=0.6.0'], license='MIT', - scripts=['verify_surface_all.pl'], entry_points={ 'console_scripts': [ 'surfigures = surfigures.__main__:main' diff --git a/surfigures/__init__.py b/surfigures/__init__.py index 27fc0a2..e466378 100644 --- a/surfigures/__init__.py +++ b/surfigures/__init__.py @@ -1,5 +1,5 @@ -__version__ = '1.0.0' +__version__ = '1.1.0' DISPLAY_TITLE = r""" _ __ _ diff --git a/surfigures/__main__.py b/surfigures/__main__.py index 5f7e6cf..1fd2bf4 100644 --- a/surfigures/__main__.py +++ b/surfigures/__main__.py @@ -10,7 +10,7 @@ from surfigures import DISPLAY_TITLE, __version__ from surfigures.args import parser from surfigures.options import Options -from surfigures.inputs import InputFinder +from surfigures.inputs.find import SubjectMapper from concurrent.futures import ThreadPoolExecutor from surfigures.run import run_surfigures @@ -30,8 +30,8 @@ def main(given_args, inputdir: Path, outputdir: Path): options = Options.from_args(given_args) - finder = InputFinder(input_dir=inputdir, output_dir=outputdir) - usable_mapper, skipped_inputs = zip(*finder.map(given_args.suffix, given_args.output)) + mapper = SubjectMapper(input_dir=inputdir, output_dir=outputdir) + usable_mapper, skipped_inputs = zip(*mapper.map(given_args.suffix, given_args.output)) skipped_inputs = list(filter(is_some, skipped_inputs)) if skipped_inputs: diff --git a/surfigures/args.py b/surfigures/args.py index 16c793f..fbcf8d5 100644 --- a/surfigures/args.py +++ b/surfigures/args.py @@ -11,13 +11,13 @@ parser.add_argument('-o', '--output', default='{}.png', type=str, help='output file template and file type. "{}" is replaced by the subject name.') -# TODO: feature bloat. -# This should be its own ChRIS plugin, https://github.com/FNNDSC/pl-abs -# but I'm doing it here in Python because I am out of time! -parser.add_argument('-a', '--abs', default='.disterr.txt', type=str, - help='file extension of input files which should have their absolute values be taken.') - parser.add_argument('-r', '--range', default='.disterr.txt:-2.0:2.0,.smtherr.txt:0.0:2.0', type=str, help='Ranges for specific file extensions.') parser.add_argument('--min', type=str, default='0.0', help='Default range minimum value') parser.add_argument('--max', type=str, default='10.0', help='Default range maximum value') +parser.add_argument('-b', '--background-color', type=str, default='white', + help='Figure background color') +parser.add_argument('-f', '--font-color', type=str, default='green', + help='Figure labels font color') +parser.add_argument('-c', '--color-map', type=str, default='spectral', + help='color map to use for data value visualization') diff --git a/surfigures/draw/__init__.py b/surfigures/draw/__init__.py new file mode 100644 index 0000000..c50da31 --- /dev/null +++ b/surfigures/draw/__init__.py @@ -0,0 +1,5 @@ +""" +Use MNI ``ray_trace`` and ImageMagick to create figures of surfaces. + +Inspired by the CIVET QC program ``verify_clasp``. +""" diff --git a/surfigures/draw/constants.py b/surfigures/draw/constants.py new file mode 100644 index 0000000..cee3430 --- /dev/null +++ b/surfigures/draw/constants.py @@ -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 diff --git a/surfigures/draw/fig.py b/surfigures/draw/fig.py new file mode 100644 index 0000000..f94bac3 --- /dev/null +++ b/surfigures/draw/fig.py @@ -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:] diff --git a/surfigures/draw/prep.py b/surfigures/draw/prep.py new file mode 100644 index 0000000..c7bf920 --- /dev/null +++ b/surfigures/draw/prep.py @@ -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) diff --git a/surfigures/draw/ray_trace.py b/surfigures/draw/ray_trace.py new file mode 100644 index 0000000..ef8dfbb --- /dev/null +++ b/surfigures/draw/ray_trace.py @@ -0,0 +1,80 @@ +""" +Pre-configurations of MNI ``ray_trace`` for creating figures of brain surfaces. +""" + +import abc +import enum +import os +from dataclasses import dataclass +from typing import Sequence, Iterable + + +class IRayTrace(abc.ABC): + """ + ``ray_trace`` inputs and pre-configuration. + """ + + def to_cmd(self, bg: str, x_size: int, y_size: int, output: str | os.PathLike) -> Sequence[str]: + """ + Produce a command which runs ``ray_trace`` with this as input. + """ + return ( + 'ray_trace', '-shadows', '-output', str(output), + '-bg', bg, '-crop', '-size', str(x_size), str(y_size), + *self.to_args() + ) + + @abc.abstractmethod + def to_args(self) -> Iterable[str]: + """ + Produce arguments for ``ray_trace`` which specify this as input. + """ + ... + + +class EmptyRayTrace(IRayTrace): + """ + Empty image file ``ray_trace`` configuration. + """ + + def to_args(self) -> Iterable[str]: + return [] + + +class HemiPos(tuple[str, ...], enum.Enum): + """ + ``ray_trace`` view orientations suitable for a single brain hemisphere. + """ + default = ('-view', '0.77', '-0.18', '-0.6', '0.55', '0.6', '0.55') + flipped = ('-view', '-0.77', '-0.18', '-0.6', '-0.55', '0.6', '0.55') + left = ('-left',) + right = ('-right',) + + +class WholeBrainPos(str, enum.Enum): + """ + ``ray_trace`` view orientations suitable for left and right hemispheres together. + """ + top = '-top' + bottom = '-bottom' + front = '-front' + back = '-back' + + +@dataclass(frozen=True) +class HemiRayTrace(IRayTrace): + surface: os.PathLike + view: HemiPos + + def to_args(self) -> Iterable[str]: + return *self.view.value, self.surface + + +@dataclass(frozen=True) +class WholeBrainRayTrace(IRayTrace): + surface_left: os.PathLike + surface_right: os.PathLike + view: WholeBrainPos + + def to_args(self) -> Iterable[str]: + return self.view.value, self.surface_left, self.surface_right diff --git a/surfigures/draw/section.py b/surfigures/draw/section.py new file mode 100644 index 0000000..5caa003 --- /dev/null +++ b/surfigures/draw/section.py @@ -0,0 +1,64 @@ +from dataclasses import dataclass +from pathlib import Path +from typing import Iterable + +from surfigures.draw.ray_trace import HemiRayTrace, WholeBrainRayTrace, HemiPos, WholeBrainPos +from surfigures.draw.tile import PositionedLabel, LazyTile +from surfigures.draw import constants + +LABEL_LR = [ + PositionedLabel(x=constants.HEMI_LABEL_RATIO_L, y=constants.HEMI_LABEL_RATIO_Y, msg='L'), + PositionedLabel(x=constants.HEMI_LABEL_RATIO_R, y=constants.HEMI_LABEL_RATIO_Y, msg='R'), +] + +LABEL_RL = [ + PositionedLabel(x=constants.HEMI_LABEL_RATIO_L, y=constants.HEMI_LABEL_RATIO_Y, msg='R'), + PositionedLabel(x=constants.HEMI_LABEL_RATIO_R, y=constants.HEMI_LABEL_RATIO_Y, msg='L'), +] + +RowPair = tuple[ + LazyTile, LazyTile, LazyTile, LazyTile, LazyTile, LazyTile, + LazyTile, LazyTile, LazyTile, LazyTile, LazyTile, LazyTile +] +""" +Two rows of tiles, each row having 6 columns. +""" + + +@dataclass(frozen=True) +class Section: + # maybe it'd be cool to also show the T1/T2 like `verify_image` + surface_left: Path + surface_right: Path + textblock_left: str + textblock_right: str + + def to_row_pair(self) -> RowPair: + """ + Create two rows depicting left and right hemispheres, as well as whole brain figures. + """ + return ( + self._whole_brain(WholeBrainPos.top, labels=LABEL_LR), + self._whole_brain(WholeBrainPos.bottom, labels=LABEL_RL), + self._hemi_left(HemiPos.default), + self._hemi_left(HemiPos.left), + self._hemi_left(HemiPos.right), + LazyTile.text_only(self.textblock_left), + + self._whole_brain(WholeBrainPos.front, labels=LABEL_RL), + self._whole_brain(WholeBrainPos.back, labels=LABEL_LR), + self._hemi_right(HemiPos.flipped), + self._hemi_right(HemiPos.left), + self._hemi_right(HemiPos.right), + LazyTile.text_only(self.textblock_right), + ) + + def _hemi_left(self, position: HemiPos) -> LazyTile: + return LazyTile(HemiRayTrace(self.surface_left, position)) + + def _hemi_right(self, position: HemiPos) -> LazyTile: + return LazyTile(HemiRayTrace(self.surface_right, position)) + + def _whole_brain(self, position: WholeBrainPos, labels: Iterable[PositionedLabel]) -> LazyTile: + rt = WholeBrainRayTrace(self.surface_left, self.surface_right, position) + return LazyTile(rt, labels) diff --git a/surfigures/draw/tile.py b/surfigures/draw/tile.py new file mode 100644 index 0000000..16c26e3 --- /dev/null +++ b/surfigures/draw/tile.py @@ -0,0 +1,59 @@ +""" +Representations of tiles and parts of tiles. +""" + +import dataclasses +from dataclasses import dataclass +from typing import Iterable, Sequence, Self + +from surfigures.draw.ray_trace import IRayTrace, EmptyRayTrace + + +@dataclass(frozen=True) +class PositionedLabel: + """ + Some text to draw using ImageMagick over a tile. + """ + x: float + """x position ratio""" + y: float + """y position ratio""" + msg: str + """label contents""" + + def at(self, row: int, col: int, tile_size_x: int, tile_size_y: int, spacing_x: int, spacing_y: int) -> Sequence[str]: + """ + Produce an ``-annotate`` option which draws this label on a montage. + """ + x = round((self.x + col) * tile_size_x + (2 * col + 1) * spacing_x) + y = round((self.y + row) * tile_size_y + (2 * row + 1) * spacing_y) + return ( + '-annotate', + f'0x0+{x}+{y}', + self.msg + ) + + +@dataclass(frozen=True) +class LazyTile: + """ + Inputs which can create a tile. + """ + + ray_trace: IRayTrace + labels: Iterable[PositionedLabel] = dataclasses.field(default_factory=tuple) + + def labels2args(self, row: int, col: int, tile_size_x: int, tile_size_y: int, + spacing_x: int, spacing_y: int) -> list[str]: + """ + :returns: ``-annotate`` flags for all labels in this tile + """ + annots = (label.at(row, col, tile_size_x, tile_size_y, spacing_x, spacing_y) for label in self.labels) + return [arg for annot in annots for arg in annot] + + @classmethod + def text_only(cls, text: str, x: float = 0.15, y: float = 0.20) -> Self: + """ + Create a tile containing only a block of text (without brain images). + """ + return cls(EmptyRayTrace(), [PositionedLabel(msg=text, x=x, y=y)]) diff --git a/surfigures/inputs.py b/surfigures/inputs.py deleted file mode 100644 index 7c747d3..0000000 --- a/surfigures/inputs.py +++ /dev/null @@ -1,371 +0,0 @@ -""" -Helper functions to find input files for ``verify_surface_all.pl`` -based on naming conventions. -""" -import shlex -from dataclasses import dataclass -from pathlib import Path -from typing import Sequence, Iterator, Optional, Callable, Iterable, TypeVar, Generic -import subprocess as sp -from tempfile import NamedTemporaryFile - -from loguru import logger -from chris_plugin import PathMapper - -from surfigures.options import Options - -LEFT_WORDS = ('left', 'Left', 'LEFT', 'lh.') -RIGHT_WORDS = ('right', 'Right', 'RIGHT', 'rh.') -SIDES: tuple[str, ...] = (*LEFT_WORDS, *RIGHT_WORDS) -SEPARATORS = ('-', '_', '.', ' ') - - -@dataclass(frozen=True) -class WholeBrainLayer: - caption: str - left: Path - right: Path - - -@dataclass(frozen=True) -class VertexDataFiles: - caption: str - left: Path - right: Path - - -@dataclass(frozen=True) -class VertexDataGroup(VertexDataFiles): - min: str - max: str - - -def surfaces2row(s: WholeBrainLayer): - return s.caption, s.left, s.right - - -def datas2row(d: VertexDataGroup): - return d.caption, d.left, d.right, d.min, d.max - - -@dataclass(frozen=True) -class SortedInputs: - title: str - surfaces: Sequence[WholeBrainLayer] - data: Sequence[VertexDataGroup] - - def to_cmd(self, output: Path) -> Sequence[str | Path]: - return [ - 'verify_surface_all.pl', - self.title, - output, - *(arg for row in map(surfaces2row, self.surfaces) for arg in row), - *(arg for row in map(datas2row, self.data) for arg in row) - ] - - -@dataclass(frozen=True) -class InputSet: - """ - (Unsorted) batch of inputs for ``verify_surfaces_all.pl`` - """ - - title: str - src: tuple[Path, ...] - surfaces: list[WholeBrainLayer] - data_files: list[VertexDataFiles] - - def expand(self, options: Options, tmp_dir: Path) -> SortedInputs: - """ - Run the extra computations specified by ``options`` and then sort the inputs. - """ - # TODO use surface-stats to sort inputs by surface area - # for now, we depend on the arbitrary order given by Path.glob - surfaces = self.surfaces.copy() - surfaces.sort(key=surface_area_of_left, reverse=True) - data_files = [ - VertexDataGroup(f.caption, f.left, f.right, *options.range_for(f.left)) - for f in self.data_files - ] - data_files += self._abs(data_files, options.abs, tmp_dir) - return SortedInputs( - title=self.title, - surfaces=surfaces, - data=data_files - ) - - @staticmethod - def _abs(data_files: list[VertexDataGroup], suffixes: Iterable[str], tmp_dir: Path) -> list[VertexDataGroup]: - """ - For every data file which has a file extension that is one of the given suffixes, - compute the absolute values to a file in ``tmp_dir``. Returns created files. - """ - check_needs_abs = ((orig, abs_files(orig, suffixes, tmp_dir)) for orig in data_files) - return [ - calc_abs(orig, result) - for orig, result in check_needs_abs - if result is not None - ] - - @classmethod - def from_folders(cls, left_folder: Path, right_folder: Path, data_file_suffix: str) -> 'InputSet': - surface_pairs = cls._find_left_and_right_in_folders(left_folder, right_folder, '.obj') - data_file_pairs = cls._find_left_and_right_in_folders(left_folder, right_folder, data_file_suffix) - return cls( - title=fname_without_side(left_folder), - src=(left_folder, right_folder), - surfaces=[WholeBrainLayer(l.name, l, r) for l, r in surface_pairs], - data_files=[VertexDataFiles(l.name, l, r) for l, r in data_file_pairs] - ) - - @classmethod - def from_folder(cls, folder: Path, data_file_suffix: str) -> 'InputSet': - surface_pairs = cls._find_left_and_right_files(folder, '.obj') - data_file_pairs = cls._find_left_and_right_files(folder, data_file_suffix) - return cls( - title=folder.name, - src=(folder,), - surfaces=[WholeBrainLayer(fname_without_side(l), l, r) for l, r in surface_pairs], - data_files=[VertexDataFiles(fname_without_side(l), l, r) for l, r in data_file_pairs] - ) - - @staticmethod - def _find_left_and_right_in_folders(left_folder: Path, right_folder: Path, ext: str) -> Sequence[tuple[Path, Path]]: - # file names in left and right folders must be *exactly* the same. - # TODO tolerate "left" and "right" substrings being in path names - pairs = [ - (left_file, right_folder / left_file.name) - for left_file in left_folder.glob(f'*{ext}') - ] - return validate_pairs(pairs) - - @staticmethod - def _find_left_and_right_files(folder: Path, ext: str) -> Sequence[tuple[Path, Path]]: - left_files = find_side_files(folder, ext, LEFT_WORDS) - pairs = find_right_files_for(left_files) - return validate_pairs(pairs) - - -def surface_area_of_left(l: WholeBrainLayer) -> float: - surface = l.left - cmd = ('surface-stats', '-face_area', surface) - str_cmd = shlex.join(map(str, cmd)) - p = sp.run(cmd, text=True, stdout=sp.DEVNULL, stderr=sp.PIPE) - if p.returncode != 0: - raise InputError(f'Command failed: {str_cmd}') - try: - area = float(p.stderr.rsplit('=', 1)[-1].strip()) - except ValueError: - raise InputError(f'Unable to parse output from command: {str_cmd} :::: output: {output}') - logger.info('running: {} => Total Surface Area = {}', str_cmd, area) - return area - - -def calc_abs(orig: VertexDataGroup, result: VertexDataGroup) -> VertexDataGroup: - for files in ((orig.left, result.left), (orig.right, result.right)): - cmd = ('vertstats_math', '-old_style_file', '-abs', *files) - str_cmd = shlex.join(map(str, cmd)) - logger.info('running: {}', str_cmd) - p = sp.run(cmd, stdout=sp.DEVNULL, stderr=sp.STDOUT) - if p.returncode != 0: - raise InputError(f'Command failed: {str_cmd}') - return result - - -def abs_files(g: VertexDataGroup, suffixes: Iterable[str], tmp_dir: Path) -> Optional[VertexDataGroup]: - """ - If the abs function should be applied to the files of ``g``, - return file names for where the output files should be written to. - """ - for suffix in suffixes: - left = name_tempfile_if_endswith(g.left, suffix, '.abs', tmp_dir) - right = name_tempfile_if_endswith(g.right, suffix, '.abs', tmp_dir) - if left and right: - return VertexDataGroup(g.caption, left, right, '0.0', g.max) - return None - - -def name_tempfile_if_endswith(path: Path, suffix: str, prefix_for_suffix: str, tmp_dir: Path) -> Optional[Path]: - s = path.name.rsplit(suffix, maxsplit=1) - if len(s) != 2: - return None - stem, _ = s - parent = tmp_dir / path.parent.name - parent.mkdir(exist_ok=True) - return parent / (stem + prefix_for_suffix + suffix) - - -def find_side_files(folder: Path, ext: str, sides: Iterable[str] = ('',)) -> Iterator[Path]: - for side in sides: - yield from folder.glob(f'*{side}*{ext}') - - -def find_right_files_for(left_files: Iterator[Path]) -> Sequence[tuple[Path, Optional[Path]]]: - return list(_find_right_files_for_generator(left_files)) - - -def _find_right_files_for_generator(left_files: Iterator[Path]) -> Iterator[tuple[Path, Optional[Path]]]: - for left_file in left_files: - yield left_file, corresponding_right_path_to(left_file) - - -def fname_without_side(path: Path) -> str: - name = path.name - for side in SIDES: - start = name.find(side) - if start == -1: - continue - end = start + len(side) - if start - 1 >= 0 and name[start - 1] in SEPARATORS: - start -= 1 - if start == 0 and name[end] in SEPARATORS: - end += 1 - return name[:start] + name[end:] - raise InputError(f'File name of {path} does not contain "left" nor "right"') - - -def validate_pairs(pairs: Sequence[tuple[Path, Optional[Path]]]) -> Sequence[tuple[Path, Path]]: - for left, right in pairs: - if not left.is_file(): - raise InputError(f'"{left}" is not a file') - if right is None: - raise InputError(f'No corresponding right-sided file found for left-sided file "{left}"') - if not right.is_file(): - raise InputError(f'"{right}" is not a file') - return pairs - - -@dataclass(frozen=True) -class InputSetBuilder: - """ - Helper to curry the ``data_file_suffix`` of ``InputSet`` constructors. - """ - data_file_suffix: str - - def from_folders(self, left_folder: Path, right_folder: Path) -> InputSet: - return InputSet.from_folders(left_folder, right_folder, self.data_file_suffix) - - def from_folder(self, folder: Path) -> InputSet: - return InputSet.from_folder(folder, self.data_file_suffix) - - -class InputError(Exception): - """ - Input files not found or not suitable. - """ - pass - - -_T = TypeVar('_T') -_R = TypeVar('_R') - - -@dataclass(frozen=True) -class InputMonad(Generic[_T]): - """ - Fancy functional programming pattern for propagating errors from within a generator - then continuing iteration without ``raise`` (which would end the loop). - """ - inner: _T | InputError - - @classmethod - def wrap(cls, f: Callable[[], _T]) -> 'InputMonad[_T]': - try: - inner = f() - except InputError as e: - inner = e - return cls(inner) - - @classmethod - def new_err(cls, why): - return cls(InputError(why)) - - def map(self, f: Callable[[_T], _R]) -> 'InputMonad[_R]': - if self.is_err(): - return self - return InputMonad(f(self.inner)) - - def starmap(self, f: Callable[..., _R]) -> 'InputMonad[_R]': - if self.is_err(): - return self - return InputMonad(f(*self.inner)) - - def is_err(self): - return isinstance(self.inner, InputError) - - def unwrap(self) -> _T: - if self.is_err(): - raise self.inner - return self.inner - - -@dataclass(frozen=True) -class InputFinder: - input_dir: Path - output_dir: Path - - def map(self, data_file_suffix: str, output_template: str - ) -> Iterator[tuple[Optional[tuple[InputSet, Path]], Optional[InputError]]]: - """ - Find inputs for ``verify_surface_all.pl`` and yield them along with a path for an output file. - - This is an awkward generator which yields a union data type in the form of a two-tuple. - - Usable sets of input files are yielded as ``(input_set, output_file_path), None`` - whereas inputs which must be skipped are yielded as ``None, InputError``. - """ - for maybe_inputs, sub_output in self._map_sided_and_everything_folders(data_file_suffix): - try: - inputs = maybe_inputs.unwrap() - output_file = sub_output.with_name(output_template.replace('{}', inputs.title)) - yield (inputs, output_file), None - except InputError as e: - yield None, e - - def _map_sided_and_everything_folders(self, data_file_suffix: str) -> Iterator[tuple[InputMonad[InputSet], Path]]: - inputs_builder = InputSetBuilder(data_file_suffix) - for maybe_pair, sub_output in self._sided_folders_mapper(): - yield InputMonad(maybe_pair).starmap(inputs_builder.from_folders), sub_output - for subject_folder, sub_output in self._subject_folders_mapper(): - yield InputMonad.wrap(lambda: inputs_builder.from_folder(subject_folder)), sub_output - - def _sided_folders_mapper(self) -> Iterator[tuple[tuple[Path, Path] | InputError, Path]]: - left_folder_mapper = PathMapper.dir_mapper_deep(self.input_dir, self.output_dir, - fail_if_empty=False, - filter=is_left_folder_containing_obj) - for left_folder, sub_output in left_folder_mapper: - right_folder = corresponding_right_path_to(left_folder) - if right_folder is None: - pair = InputError('Cannot find folder for right-sided inputs ' - f'corresponding to left folder "{left_folder}"') - else: - pair = left_folder, right_folder - yield pair, sub_output - - def _subject_folders_mapper(self) -> Iterator[tuple[Path, Path]]: - mapper = PathMapper.dir_mapper_deep(self.input_dir, self.output_dir, - fail_if_empty=False, - filter=is_unsided_subjects_folder) - return iter(mapper) - - -def is_left_folder_containing_obj(folder: Path) -> bool: - return contains_obj(folder) and is_side_folder(folder, 'left') - - -def is_unsided_subjects_folder(folder: Path) -> bool: - return contains_obj(folder) and not any(is_side_folder(folder, side) for side in SIDES) - - -def contains_obj(folder: Path) -> bool: - return next(folder.glob('*.obj'), None) is not None - - -def is_side_folder(folder: Path, side: str) -> bool: - return side.lower() in folder.name.lower() - - -def corresponding_right_path_to(path: Path) -> Optional[Path]: - possible_paths = map(lambda l, r: path.with_name(path.name.replace(l, r)), LEFT_WORDS, RIGHT_WORDS) - existing_right_paths = filter(lambda f: f.exists(), possible_paths) - return next(existing_right_paths, None) diff --git a/surfigures/inputs/__init__.py b/surfigures/inputs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/surfigures/inputs/_helpers.py b/surfigures/inputs/_helpers.py new file mode 100644 index 0000000..a4c3741 --- /dev/null +++ b/surfigures/inputs/_helpers.py @@ -0,0 +1,46 @@ +from dataclasses import dataclass +from typing import Generic, TypeVar, Callable + +from surfigures.inputs.err import InputError + +_T = TypeVar('_T') +_R = TypeVar('_R') + + +@dataclass(frozen=True) +class InputMonad(Generic[_T]): + """ + Fancy functional programming pattern for propagating errors from within a generator + then continuing iteration without ``raise`` (which would end the loop). + """ + inner: _T | InputError + + @classmethod + def wrap(cls, f: Callable[[], _T]) -> 'InputMonad[_T]': + try: + inner = f() + except InputError as e: + inner = e + return cls(inner) + + @classmethod + def new_err(cls, why): + return cls(InputError(why)) + + def map(self, f: Callable[[_T], _R]) -> 'InputMonad[_R]': + if self.is_err(): + return self + return InputMonad(f(self.inner)) + + def starmap(self, f: Callable[..., _R]) -> 'InputMonad[_R]': + if self.is_err(): + return self + return InputMonad(f(*self.inner)) + + def is_err(self) -> bool: + return isinstance(self.inner, InputError) + + def unwrap(self) -> _T: + if self.is_err(): + raise self.inner + return self.inner diff --git a/surfigures/inputs/constants.py b/surfigures/inputs/constants.py new file mode 100644 index 0000000..22743ab --- /dev/null +++ b/surfigures/inputs/constants.py @@ -0,0 +1,4 @@ +LEFT_WORDS = ('left', 'Left', 'LEFT', 'lh.') +RIGHT_WORDS = ('right', 'Right', 'RIGHT', 'rh.') +SIDES: tuple[str, ...] = (*LEFT_WORDS, *RIGHT_WORDS) +SEPARATORS = ('-', '_', '.', ' ') diff --git a/surfigures/inputs/err.py b/surfigures/inputs/err.py new file mode 100644 index 0000000..d513284 --- /dev/null +++ b/surfigures/inputs/err.py @@ -0,0 +1,5 @@ +class InputError(Exception): + """ + Input files not found or not suitable. + """ + pass diff --git a/surfigures/inputs/find.py b/surfigures/inputs/find.py new file mode 100644 index 0000000..fc5ddb5 --- /dev/null +++ b/surfigures/inputs/find.py @@ -0,0 +1,181 @@ +""" +Helper functions to find input files. +""" +from dataclasses import dataclass +from pathlib import Path +from typing import Sequence, Iterator, Optional, Iterable + +from chris_plugin import PathMapper + +import surfigures.inputs.constants as constants +from surfigures.inputs.err import InputError +from surfigures.inputs._helpers import InputMonad +from surfigures.inputs.groups import Layer, DataFiles +from surfigures.inputs.subject import SubjectSet + + +@dataclass(frozen=True) +class SubjectMapper: + """ + A class with methods for finding subject input files in an input directory, + and mapping them to output file names in an output directory. + """ + input_dir: Path + output_dir: Path + + def map(self, data_file_suffix: str, output_template: str + ) -> Iterator[tuple[Optional[tuple[SubjectSet, Path]], Optional[InputError]]]: + """ + Find inputs subject-wise and yield them along with a path for an output file. + + This is an (awkward) generator which yields a union data type in the form of a two-tuple. + + Usable sets of input files are yielded as ``(input_set, output_file_path), None`` + whereas inputs which must be skipped are yielded as ``None, InputError``. + """ + for maybe_inputs, sub_output in self._map_sided_and_everything_folders(data_file_suffix): + try: + inputs = maybe_inputs.unwrap() + output_file = sub_output.with_name(output_template.replace('{}', inputs.title)) + yield (inputs, output_file), None + except InputError as e: + yield None, e + + def _map_sided_and_everything_folders(self, data_file_suffix: str) -> Iterator[tuple[InputMonad[SubjectSet], Path]]: + inputs_builder = _SubjectSetFinder(data_file_suffix) + for maybe_pair, sub_output in self._sided_folders_mapper(): + yield InputMonad(maybe_pair).starmap(inputs_builder.in_folders), sub_output + for subject_folder, sub_output in self._subject_folders_mapper(): + yield InputMonad.wrap(lambda: inputs_builder.in_folder(subject_folder)), sub_output + + def _sided_folders_mapper(self) -> Iterator[tuple[tuple[Path, Path] | InputError, Path]]: + left_folder_mapper = PathMapper.dir_mapper_deep(self.input_dir, self.output_dir, + fail_if_empty=False, + filter=_is_left_folder_containing_obj) + for left_folder, sub_output in left_folder_mapper: + right_folder = _corresponding_right_path_to(left_folder) + if right_folder is None: + pair = InputError('Cannot find folder for right-sided inputs ' + f'corresponding to left folder "{left_folder}"') + else: + pair = left_folder, right_folder + yield pair, sub_output + + def _subject_folders_mapper(self) -> Iterator[tuple[Path, Path]]: + mapper = PathMapper.dir_mapper_deep(self.input_dir, self.output_dir, + fail_if_empty=False, + filter=_is_unsided_subjects_folder) + return iter(mapper) + + +@dataclass(frozen=True) +class _SubjectSetFinder: + """ + Namespace of curried helper functions to find input files of a single subject. + """ + data_file_suffix: str + + def in_folders(self, left_folder: Path, right_folder: Path) -> SubjectSet: + """ + Find left/right pairs of input files for one subject from two folders. + """ + surface_pairs = _find_left_and_right_in_folders(left_folder, right_folder, '.obj') + data_file_pairs = _find_left_and_right_in_folders(left_folder, right_folder, self.data_file_suffix) + return SubjectSet( + title=_fname_without_side(left_folder), + src=(left_folder, right_folder), + surfaces=[Layer(l.name, l, r) for l, r in surface_pairs], + data_files=[DataFiles(l.name, l, r) for l, r in data_file_pairs] + ) + + def in_folder(self, folder: Path) -> SubjectSet: + """ + Find left/right pairs of input files under a folder, where left and right data files are found + in the same folder by similar names. + """ + surface_pairs = _find_left_and_right_files(folder, '.obj') + data_file_pairs = _find_left_and_right_files(folder, self.data_file_suffix) + return SubjectSet( + title=folder.name, + src=(folder,), + surfaces=[Layer(_fname_without_side(l), l, r) for l, r in surface_pairs], + data_files=[DataFiles(_fname_without_side(l), l, r) for l, r in data_file_pairs] + ) + + +def _find_side_files(folder: Path, ext: str, sides: Iterable[str] = ('',)) -> Iterator[Path]: + for side in sides: + yield from folder.glob(f'*{side}*{ext}') + + +def _find_right_files_for(left_files: Iterator[Path]) -> Sequence[tuple[Path, Optional[Path]]]: + return list(_find_right_files_for_generator(left_files)) + + +def _find_right_files_for_generator(left_files: Iterator[Path]) -> Iterator[tuple[Path, Optional[Path]]]: + for left_file in left_files: + yield left_file, _corresponding_right_path_to(left_file) + + +def _fname_without_side(path: Path) -> str: + name = path.name + for side in constants.SIDES: + start = name.find(side) + if start == -1: + continue + end = start + len(side) + if start - 1 >= 0 and name[start - 1] in constants.SEPARATORS: + start -= 1 + if start == 0 and name[end] in constants.SEPARATORS: + end += 1 + return name[:start] + name[end:] + raise InputError(f'File name of {path} does not contain "left" nor "right"') + + +def _validate_pairs(pairs: Sequence[tuple[Path, Optional[Path]]]) -> Sequence[tuple[Path, Path]]: + for left, right in pairs: + if not left.is_file(): + raise InputError(f'"{left}" is not a file') + if right is None: + raise InputError(f'No corresponding right-sided file found for left-sided file "{left}"') + if not right.is_file(): + raise InputError(f'"{right}" is not a file') + return pairs + + +def _find_left_and_right_in_folders(left_folder: Path, right_folder: Path, ext: str) -> Sequence[tuple[Path, Path]]: + # file names in left and right folders must be *exactly* the same. + # TODO tolerate "left" and "right" substrings being in path names + pairs = [ + (left_file, right_folder / left_file.name) + for left_file in left_folder.glob(f'*{ext}') + ] + return _validate_pairs(pairs) + + +def _find_left_and_right_files(folder: Path, ext: str) -> Sequence[tuple[Path, Path]]: + left_files = _find_side_files(folder, ext, constants.LEFT_WORDS) + pairs = _find_right_files_for(left_files) + return _validate_pairs(pairs) + + +def _is_left_folder_containing_obj(folder: Path) -> bool: + return _contains_obj(folder) and _is_side_folder(folder, 'left') + + +def _is_unsided_subjects_folder(folder: Path) -> bool: + return _contains_obj(folder) and not any(_is_side_folder(folder, side) for side in constants.SIDES) + + +def _contains_obj(folder: Path) -> bool: + return next(folder.glob('*.obj'), None) is not None + + +def _is_side_folder(folder: Path, side: str) -> bool: + return side.lower() in folder.name.lower() + + +def _corresponding_right_path_to(path: Path) -> Optional[Path]: + possible_paths = map(lambda l, r: path.with_name(path.name.replace(l, r)), constants.LEFT_WORDS, constants.RIGHT_WORDS) + existing_right_paths = filter(lambda f: f.exists(), possible_paths) + return next(existing_right_paths, None) diff --git a/surfigures/inputs/groups.py b/surfigures/inputs/groups.py new file mode 100644 index 0000000..2993557 --- /dev/null +++ b/surfigures/inputs/groups.py @@ -0,0 +1,22 @@ +from dataclasses import dataclass +from pathlib import Path + + +@dataclass(frozen=True) +class Layer: + """ + A pair of left and right brain surfaces. + """ + caption: str + left: Path + right: Path + + +@dataclass(frozen=True) +class DataFiles: + """ + Vertex-wise data files for left and right brain surfaces. + """ + caption: str + left: Path + right: Path diff --git a/surfigures/inputs/subject.py b/surfigures/inputs/subject.py new file mode 100644 index 0000000..2aecb8d --- /dev/null +++ b/surfigures/inputs/subject.py @@ -0,0 +1,62 @@ +import dataclasses +import shlex +import subprocess +from dataclasses import dataclass +from pathlib import Path +from typing import Self, Iterable + +from loguru import logger + +from surfigures.inputs.err import InputError +from surfigures.inputs.groups import Layer, DataFiles +from surfigures.util.runnable import Runner + + +@dataclass(frozen=True) +class SubjectSet: + """ + Batch of input files for one subject. + """ + + title: str + src: tuple[Path, ...] + surfaces: list[Layer] + data_files: list[DataFiles] + + def sort(self) -> Self: + """ + Sort the surfaces from outer to inner. + """ + surfaces = self.surfaces.copy() + surfaces.sort(key=_surface_area_of_left, reverse=True) + return dataclasses.replace(self, surfaces=surfaces) + + def surfaces_left(self) -> Iterable[Path]: + return (layer.left for layer in self.surfaces) + + def surfaces_right(self) -> Iterable[Path]: + return (layer.right for layer in self.surfaces) + + def mid_surface_left(self, sp: Runner) -> Path: + return self._mid_surface_of(sp, 'left', self.surfaces_left()) + + def mid_surface_right(self, sp: Runner) -> Path: + return self._mid_surface_of(sp, 'right', self.surfaces_right()) + + def _mid_surface_of(self, sp: Runner, suffix: str, surfaces: Iterable[Path]) -> Path: + name = sp.tmp_dir / f'{self.title}_{suffix}.obj' + sp.run(('average_surfaces', name, 'none', 'none', '1', *surfaces)) + return name + +def _surface_area_of_left(layer: Layer) -> float: + cmd = ('surface-stats', '-face_area', layer.left) + str_cmd = shlex.join(map(str, cmd)) + p = subprocess.run(cmd, text=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) + if p.returncode != 0: + raise InputError(f'Command failed: {str_cmd}') + try: + area = float(p.stderr.rsplit('=', 1)[-1].strip()) + except ValueError: + raise InputError(f'Unable to parse output from command: {str_cmd} :::: output: {p.stderr}') + logger.info('running: {} => Total Surface Area = {}', str_cmd, area) + return area diff --git a/surfigures/options.py b/surfigures/options.py index d545d83..d579b41 100644 --- a/surfigures/options.py +++ b/surfigures/options.py @@ -1,22 +1,26 @@ from dataclasses import dataclass from pathlib import Path -from typing import Sequence +from typing import Self @dataclass(frozen=True) class Options: - abs: Sequence[str] range: dict[str, tuple[str, str]] min: str max: str + bg: str + font_color: str + color_map: str @classmethod - def from_args(cls, args) -> 'Options': + def from_args(cls, args) -> Self: return cls( - abs=args.abs.split(','), range={s: (a, b) for s, a, b in map(_parse_range_arg, args.range.split(','))}, min=args.min, - max=args.max + max=args.max, + bg=args.background_color, + font_color=args.font_color, + color_map=args.color_map, ) def range_for(self, data_file: Path) -> tuple[str, str]: diff --git a/surfigures/run.py b/surfigures/run.py index b7c52c7..e702309 100644 --- a/surfigures/run.py +++ b/surfigures/run.py @@ -1,36 +1,58 @@ +import os +import subprocess from pathlib import Path import shlex import subprocess as sp import time from tempfile import TemporaryDirectory -from typing import Optional +from typing import Optional, Sequence, TextIO from loguru import logger -from surfigures.inputs import InputSet +from surfigures.draw.fig import FigureCreator +from surfigures.inputs.subject import SubjectSet from surfigures.options import Options +from surfigures.util.runnable import Runner -def run_surfigures(input_set: InputSet, output_file: Path, options: Options) -> Optional[float]: +def run_surfigures(input_set: SubjectSet, output_file: Path, options: Options) -> Optional[float]: """ - Returns ``None`` if there was an error, or the time spent running ``verify_surface_all.pl`` in seconds. + :returns: ``None`` if there was an error, or the time spent running ``verify_surface_all.pl`` in seconds. """ - with TemporaryDirectory() as tmp_dir: - sorted_inputs = input_set.expand(options, Path(tmp_dir)) - cmd = sorted_inputs.to_cmd(output_file) - log_file = output_file.with_suffix('.log') - logger.info('running: {} > {}', shlex.join(map(str, cmd)), log_file) - start = time.monotonic_ns() - with log_file.open('wb') as f: - p = sp.run(cmd, stderr=sp.STDOUT, stdout=f) + start = time.monotonic_ns() + sorted_inputs = input_set.sort() + fig = FigureCreator(sorted_inputs, output_file, options) + log_path = output_file.with_suffix('.log') + with TemporaryDirectory() as tmp_dir, log_path.open('w') as log_handle: + runner = LoggedRunner(Path(tmp_dir), log_handle) + ok = True + try: + fig.run(runner) + except sp.CalledProcessError: + ok = False end = time.monotonic_ns() elapsed = (end - start) / 1e9 - ok = p.returncode == 0 msg = f'{tuple(map(str, input_set.src))} --> {output_file} took {elapsed:.1f}s' if ok: logger.info(msg) return elapsed else: - logger.error('{}, please check {}', msg, log_file) + logger.error('{}. !!!FAILED!!! please check {}', msg, log_path) return None + + +class LoggedRunner(Runner): + + def __init__(self, tmp_dir: Path, log_file: TextIO): + self.__tmp_dir = tmp_dir + self.__log_file = log_file + + @property + def tmp_dir(self) -> Path: + return self.__tmp_dir + + def run(self, cmd: Sequence[str | os.PathLike], stdout=sp.DEVNULL, stderr=sp.DEVNULL) -> sp.CompletedProcess: + self.__log_file.write(shlex.join(map(str, cmd))) + self.__log_file.write('\n') + return subprocess.run(cmd, stdout=stdout, stderr=stderr, check=True, text=True) diff --git a/surfigures/util/__init__.py b/surfigures/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/surfigures/util/runnable.py b/surfigures/util/runnable.py new file mode 100644 index 0000000..f09d7ef --- /dev/null +++ b/surfigures/util/runnable.py @@ -0,0 +1,45 @@ +import abc +import os +from pathlib import Path +from typing import Generic, TypeVar, Sequence +import subprocess as sp + +T = TypeVar('T') + + +class Runner(abc.ABC): + """ + For the most part, ``Runnable`` are wrappers to the ``subprocess`` module. + """ + + STDOUT = sp.STDOUT + DEVNULL = sp.DEVNULL + PIPE = sp.PIPE + + @property + @abc.abstractmethod + def tmp_dir(self) -> Path: + """ + A temporary directory, a suitable location for output files. + """ + ... + + @abc.abstractmethod + def run(self, cmd: Sequence[str | os.PathLike], stdout=sp.DEVNULL, stderr=sp.DEVNULL) -> sp.CompletedProcess: + ... + + +class Runnable(abc.ABC, Generic[T]): + """ + A ``Runnable`` is a set of inputs which can be transformed into outputs by running subprocesses. + + In retrospect, this abstraction was a bad idea. + """ + + @abc.abstractmethod + def run(self, sp: Runner) -> T: + """ + :param sp: can be used to run subcommands and capture subcommand output + :returns: an object which represents the created output + """ + ... diff --git a/tests/test_helpers.py b/tests/test_helpers.py index e398d02..148d429 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,7 +1,7 @@ import pytest from pathlib import Path -from surfigures.inputs import fname_without_side, name_tempfile_if_endswith +from surfigures.inputs.find import _fname_without_side @pytest.mark.parametrize( @@ -17,16 +17,4 @@ ] ) def test_replace_side(input_str, expected): - assert fname_without_side(Path(input_str)) == expected - - -@pytest.mark.parametrize( - 'p, expected', - [ - (Path('surf_81920.disterr.txt'), Path('/tmp/surf_81920.abs.disterr.txt')), - (Path('surf_81920.smtherr.txt'), None), - ] -) -def test_insert_before_suffix_if(p, expected): - actual = name_tempfile_if_endswith(p, '.disterr.txt', '.abs', Path('/tmp')) - assert actual == expected + assert _fname_without_side(Path(input_str)) == expected diff --git a/verify_surface_all.pl b/verify_surface_all.pl deleted file mode 100755 index 203b414..0000000 --- a/verify_surface_all.pl +++ /dev/null @@ -1,271 +0,0 @@ -#!/usr/bin/env perl - -# Plot surfaces and vertex-wise data. -# Modified from verify_clasp. -# -# I wrote this in a hurry. Parallelism and declarativeness would be nice, but ain't got time for that... -# -# N.B.: This script does not care to show good errors nor validate its arguments. -# My recommendation is to write a wrapper for this script, such as verify_surfaces.py -# -# TODO: it'd be nice to check flip orientation to make sure surface sides are correct. -# -# Copyright Alan C. Evans -# Professor of Neurology -# McGill University -# - -use strict; -use warnings "all"; -use List::Util qw( min max ); -use File::Temp qw/ tempdir /; - -my ($main_title, $output, @args) = @ARGV; - -unless($output) { - print "USAGE\n\n"; - print "\t$0 title output.png [inputs...]\n\n"; - print "Inputs to this program are 3-tuples indicating surfaces, and 5-tuples of values.\n"; - print "First, specify surfaces as: caption left.obj right.obj\n"; - print "After all surfaces arguments, specify data files: caption left.txt right.txt min max\n"; - print "\n"; - print "EXAMPLES\n\n"; - print "One layer, one dataset:\n\n"; - print "\t$0 \"WM Extraction\" wm_extraction_qc.png \\\n"; - print "\t\"Marching-cubes white matter surface\" wm_left_81920.obj wm_left_right_81920.obj \\\n"; - print "\t\"Distance error\" left_disterr.txt right_disterr.txt -3.0 3.0\n"; - print "\n\n"; - print "Inner and outer layers, multiple datasets:\n\n"; - print "\t$0 \"Inner subplate fitting\" spfit_qc.png \\\n"; - print "\t\"Outer SP\" spouter_left_81920.obj spouter_right_81920.obj \\\n"; - print "\t\"Inner SP\" spinner_left_81920.obj spinner_right_81920.obj \\\n"; - print "\t\"Distance error\" left_disterr.txt right_disterr.txt -3.0 3.0 \\\n"; - print "\t\"Smoothness error\" left_smtherr.txt right_smtherr.txt 0.0 2.0 \\\n"; - print "\t\"Tlink thickness\" left_tlink.txt right_tnear.txt 0.0 10.0\n"; - print "\n"; - exit(1); -} - -################################################################################ -# ARGUMENT PARSING -################################################################################ - -my @surface_args = (); -my @data_args = (); - -my @buffer = (); -my $arg_type = ""; - -while (my $arg = shift(@args)) { - my $current_len = scalar @buffer; - if ($current_len == 1) { - $arg_type = $arg =~ /.*\.obj/ ? "surface" : "data"; - } - push @buffer, $arg; - if ($current_len == 2 && $arg_type eq 'surface') { - push @surface_args, @buffer; - @buffer = (); - } elsif ($current_len == 4 && $arg_type eq 'data') { - push @data_args, @buffer; - @buffer = (); - } -} - -if (@buffer) { - print "Wrong number of arguments.\n"; - exit(1); -} - -################################################################################ -# GLOBAL VARIABLES -################################################################################ - -my $tmpdir = tempdir( CLEANUP => 1 ); -my $tilesize = 200; -my @mont_args = (); -my @DrawText = ( '-font', 'DejaVu-Sans' ); - -my $num_rows = 0; -my $xpos = 2*$tilesize; -my $ypos = 15; -my $xpos2 = 3.185*$tilesize; # position for L/R labels -my $xpos3 = 3.84*$tilesize; -my $xpos4 = 4.185*$tilesize; -my $xpos5 = 4.84*$tilesize; - -# draw main title -push( @DrawText, ( '-annotate', "0x0+${xpos}+${ypos}", "$main_title" ) ); - -################################################################################ -# CALCULATE MID SURFACES -################################################################################ - -# This step is performed early because average_surfaces will fail if the input -# is weird, e.g. accidentally provided a left surface where right was expected - -my $mid_left = "${tmpdir}/mid_surface_left.obj"; -my $mid_right = "${tmpdir}/mid_surface_right.obj"; - -my @all_left_surfaces = (); -my @all_right_surfaces = (); - -for ( my $i = 0; $i < @surface_args; $i += 3 ) { - push @all_left_surfaces, $surface_args[$i + 1]; - push @all_right_surfaces, $surface_args[$i + 2]; -} - -&run('average_surfaces', $mid_left, 'none', 'none', '1', @all_left_surfaces); -&run('average_surfaces', $mid_right, 'none', 'none', '1', @all_right_surfaces); - -################################################################################ -# DRAW SURFACES -################################################################################ - -for ( my $i = 0; $i < @surface_args; $i += 3 ) { - my ($caption_text, $surface_left, $surface_right) = @surface_args[$i .. $i + 2]; - - # ROW 1: white left hemi surfaces + white top and bottom views - $num_rows += 1; - $ypos += 0.065 * $tilesize; - push(@DrawText, ('-annotate', "0x0+${xpos2}+${ypos}", "L")); - push(@DrawText, ('-annotate', "0x0+${xpos3}+${ypos}", "R")); - push(@DrawText, ('-annotate', "0x0+${xpos4}+${ypos}", "R")); - push(@DrawText, ('-annotate', "0x0+${xpos5}+${ypos}", "L")); - $ypos -= 0.065 * $tilesize; - foreach my $pos ('default', 'left', 'right') { - make_hemi($surface_left, "${tmpdir}/${num_rows}_hemi_${pos}.rgb", $pos); - push(@mont_args, "${tmpdir}/${num_rows}_hemi_${pos}.rgb"); - } - foreach my $pos ('top', 'bottom') { - make_surface($surface_left, $surface_right, "${tmpdir}/${num_rows}_surf_${pos}.rgb", $pos); - push(@mont_args, "${tmpdir}/${num_rows}_surf_${pos}.rgb"); - } - - # ROW 2: white right hemi surfaces + white front and back views - $num_rows += 1; - $ypos += $tilesize; - $ypos += 0.045 * $tilesize; - push(@DrawText, ('-annotate', "0x0+${xpos2}+${ypos}", "R")); - push(@DrawText, ('-annotate', "0x0+${xpos3}+${ypos}", "L")); - push(@DrawText, ('-annotate', "0x0+${xpos4}+${ypos}", "L")); - push(@DrawText, ('-annotate', "0x0+${xpos5}+${ypos}", "R")); - $ypos -= 0.045 * $tilesize; - foreach my $pos ('flipped', 'right', 'left') { - make_hemi($surface_right, "${tmpdir}/${num_rows}_hemi_${pos}.rgb", $pos); - push(@mont_args, "${tmpdir}/${num_rows}_hemi_${pos}.rgb"); - } - foreach my $pos ('front', 'back') { - make_surface($surface_left, $surface_right, "${tmpdir}/${num_rows}_surf_${pos}.rgb", $pos); - push(@mont_args, "${tmpdir}/${num_rows}_surf_${pos}.rgb"); - } - - push( @DrawText, ( '-annotate', "0x0+${xpos}+${ypos}", "$caption_text" ) ); - # leftover stuff. It used to draw 3 lines of text and move them places... - $ypos += $tilesize - 0.10*$tilesize; - $xpos -= 0.2*$tilesize; - $xpos += 0.2*$tilesize; - $ypos += 0.10*$tilesize; - $ypos += 0.10*$tilesize; - $ypos -= 0.10*$tilesize; - $ypos += 0.065*$tilesize; -} - -################################################################################ -# DRAW DATA VALUES OVER MID SURFACES -################################################################################ - -for ( my $i = 0; $i < @data_args; $i += 5 ) { - my ($caption_text, $data_left, $data_right, $min, $max) = @data_args[$i .. $i + 4]; - - # these two files get overwritten in the next iteration - my $mid_rms_left = "${tmpdir}/${num_rows}_mid_rms_left.obj"; - my $mid_rms_right = "${tmpdir}/${num_rows}_mid_rms_right.obj"; - - my $old_xpos = $xpos; - $xpos = 0.85 * $xpos; - $ypos += $tilesize; - push( @DrawText, ( '-annotate', "0x0+${xpos}+${ypos}", $caption_text ) ); - $ypos += $tilesize; - $xpos = $old_xpos; - - &run( 'colour_object', $mid_left, $data_left, $mid_rms_left, 'spectral', $min, $max); - &run( 'colour_object', $mid_right, $data_right, $mid_rms_right, 'spectral', $min, $max); - - $num_rows += 1; - foreach my $pos ('default', 'left', 'right') { - my $tile_image = "${tmpdir}/${num_rows}_mid_left_$pos.rgb"; - make_hemi($mid_rms_left, $tile_image, $pos); - push(@mont_args, $tile_image); - } - foreach my $pos ('top', 'bottom') { - my $tile_image = "${tmpdir}/${num_rows}_mid_$pos.rgb"; - make_surface( $mid_rms_left, $mid_rms_right, $tile_image, $pos ); - push(@mont_args, $tile_image); - } - $num_rows += 1; - foreach my $pos ('flipped', 'right', 'left') { - my $tile_image = "${tmpdir}/${num_rows}_mid_right_$pos.rgb"; - make_hemi($mid_rms_right, $tile_image, $pos); - push(@mont_args, $tile_image); - } - foreach my $pos ('front', 'back') { - my $tile_image = "${tmpdir}/${num_rows}_mid_${pos}.rgb"; - make_surface( $mid_rms_left, $mid_rms_right, $tile_image, $pos ); - push(@mont_args, $tile_image); - } -} - -################################################################################ -# FINISH UP BY RUNNING MONTAGE -################################################################################ - -# do the montage -&run( 'montage', '-tile', "5x${num_rows}", '-background', 'white', - '-geometry', "${tilesize}x${tilesize}+1+1", @mont_args, - "${tmpdir}/mont.png" ); - -# Add the title -&run( 'convert', '-box', 'white', '-stroke', 'green', '-pointsize', 16, - @DrawText, "${tmpdir}/mont.png", ${output} ); - -# end of function - -sub make_hemi { - my ($surface, $temp_output, $pos) = @_; - - my @viewdir = (); - if ($pos eq 'default') { - push( @viewdir, qw( -view 0.77 -0.18 -0.6 0.55 0.6 0.55 ) ); - } else { - if ($pos eq 'flipped') { - push( @viewdir, qw( -view -0.77 -0.18 -0.6 -0.55 0.6 0.55 ) ); - } else { - push( @viewdir, "-$pos" ); - } - } - - &run( 'ray_trace', '-shadows', '-output', ${temp_output}, ${surface}, - '-bg', 'white', '-crop', @{viewdir} ); -} - -sub make_surface { - my ($left_hemi, $right_hemi, $temp_output, $pos) = @_; - - my $viewdir = ""; - if ($pos eq 'default') { - $viewdir = ""; - } else { - $viewdir = "-$pos"; - } - - &run( 'ray_trace', '-shadows', '-output', ${temp_output}, ${left_hemi}, - ${right_hemi}, '-bg', 'white', '-crop', ${viewdir} ); -} - - -#Execute a system call. - -sub run { - print "@_ \n"; - system(@_)==0 or die "Command @_ failed with status: $?"; -}