@@ -63,6 +63,28 @@ def pol2cart(phi, rho):
63
63
return x , y
64
64
65
65
66
+ def circ_dist (a , b , degrees = True ):
67
+ """Compute angle between two angles w/ circular statistics
68
+
69
+ Parameters
70
+ ----------
71
+ a : scalar or array
72
+ angle(s) TO WHICH to compute angular (aka rotational) distance
73
+ b : scalar or array
74
+ angle(s) FROM WHICH to compute angular (aka rotational) distance
75
+ degrees : bool
76
+ Whether a and b are in degrees (defaults to True; False means they are in radians)
77
+ """
78
+ if degrees :
79
+ a = np .radians (a )
80
+ b = np .radians (b )
81
+ phase = np .e ** (1j * a ) / np .e ** (1j * b )
82
+ dist = np .arctan2 (phase .imag , phase .real )
83
+ if degrees :
84
+ dist = np .degrees (dist )
85
+ return dist
86
+
87
+
66
88
def nonzero_coords (coords ):
67
89
return coords [np .any (coords != 0 , axis = 1 )]
68
90
@@ -737,25 +759,38 @@ def resample_roi_outline(
737
759
distances ,
738
760
resolution = 100 ,
739
761
every_n = 5 ,
762
+ start_angle = np .pi / 2 ,
763
+ fixed_sampling_between = (0 , 90 , 180 , 270 ), # TO DO
740
764
even_sampling_over = 'polar angle' ,
741
765
):
742
766
"""resample roi outline to smooth outline
767
+
768
+ Also useful for creating average ROI outlines for different participants
769
+ that can be averaged in dartboard space to create an average ROI.
743
770
744
- Relies on angles being monotonically increasing
771
+ Relies on angles being monotonically increasing.
745
772
746
773
Parameters
747
774
----------
748
775
angles : _type_
749
- _description_
750
- even_sampling_over : str, optional
751
- _description_, by default 'polar angle'
776
+ angle to each ROI outline point. This is theta of (rho, theta)
777
+ [i.e. (radius, polar angle)] in polar coordinates.
778
+ even_sampling_over : str, or None, optional
779
+ method by which to sample along roi border, either 'path length',
780
+ 'polar angle', or None; by default 'path length'. Sampling over
781
+ polar angle from the center of the ROI is a sensible option if the
782
+ ROI is approximately circular, without drastic elongation or non-convex
783
+ regions along the border. For ROIs with non-convex regions,
784
+ sampling over path length (at even distances along the ROI border)
785
+ will give more sensible results. Given that the failure case for
786
+ sampling
752
787
resolution : int, optional
753
788
number of points to sample along the ROI border, by default 100
754
789
755
790
Returns
756
791
-------
757
- _type_
758
- _description_
792
+ angles, distances
793
+ returns angles and distances in polar coordinates.
759
794
"""
760
795
delta = 1e-5
761
796
# Periodic linear resample (upsample) to assure values from 0 to 2pi
@@ -777,10 +812,12 @@ def resample_roi_outline(
777
812
angles_out = np .linspace (0 , 2 * np .pi - delta , resolution )
778
813
dist_out = smooth (angles_out )
779
814
return angles_out , dist_out
780
-
781
- if even_sampling_over == 'path length' :
815
+ elif even_sampling_over == 'path length' :
782
816
## Resample
783
817
xy = np .array (pol2cart (angles , distances )).T
818
+ vertex_order = sort_roi_border (xy , angles , start_angle = start_angle , max_deg_from_zero = 5 , verbose = False )
819
+ xy = xy [vertex_order ]
820
+ angles = angles [vertex_order ]
784
821
path_dist = np .cumsum (
785
822
np .hstack ([0 , np .linalg .norm (np .diff (xy , axis = 0 ), axis = 1 )]))
786
823
max_dist = np .max (path_dist )
@@ -810,9 +847,69 @@ def resample_roi_outline(
810
847
# Convert back to angles
811
848
angles_out , dist_out = cart2pol (* xy_out .T )
812
849
return angles_out , dist_out
850
+ elif even_sampling_over is None :
851
+ return angles , distances
852
+
853
+ def sort_roi_border (xy , angles , start_angle = 0 , start_index = None , max_deg_from_zero = 5 , verbose = False ):
854
+ """start_index overrides start_angle"""
855
+ dsts = distance .squareform (distance .pdist (xy ))
856
+ if start_index is None :
857
+ # Choose max distance vertex that is approximately straight up
858
+ # from whatever center the angles were computed from.
859
+ # Best would be maybe to compute max distance from actual center
860
+ # that is the center from which angles were computed.
861
+ deg_from_zero = 2
862
+ while deg_from_zero <= max_deg_from_zero :
863
+ max_radians_from_up = np .radians (max_deg_from_zero )
864
+ close_to_straight_up , = np .nonzero (np .abs (circ_dist (angles , start_angle , degrees = False )) <= max_radians_from_up )
865
+ if len (close_to_straight_up ) > 0 :
866
+ # pick farthest away from approx. center that is straight up
867
+ dist_from_median = np .linalg .norm (xy - np .median (xy , axis = 0 ), axis = 1 )
868
+ jj = np .argmax (dist_from_median [close_to_straight_up ])
869
+ start_index = close_to_straight_up [jj ]
870
+ break
871
+ else :
872
+ deg_from_zero += 1
873
+ if start_index is None :
874
+ start_index = np .argmin (
875
+ np .abs (circ_dist (angles , start_angle , degrees = False )))
876
+ vertex_order = [start_index ]
877
+ ct = 0
878
+ for i_vert in range (len (xy )):
879
+ # Special case for first one: go clockwise
880
+ if i_vert == 0 :
881
+ new_verts = np .argsort (dsts [start_index ])[:5 ]
882
+ new_verts = np .array ([x for x in new_verts if x not in vertex_order ])
883
+ angles_from_origin = circ_dist (angles [new_verts ], angles [start_index ], degrees = False )
884
+ is_clockwise = angles_from_origin > 0
885
+ #if ~any(is_clockwise):
886
+ # plt.plot(*xy.T)
887
+ # plt.plot()
888
+ min_clockwise_angle = np .min (angles_from_origin [is_clockwise ])
889
+ j = new_verts [angles_from_origin == min_clockwise_angle ][0 ]
890
+ vertex_order .append (j )
891
+ else :
892
+ success = False
893
+ n = 3
894
+ while (not success ) and (n < 15 ):
895
+ new_verts = np .argsort (dsts [vertex_order [- 1 ]])[:n ]
896
+ new_verts = np .array ([x for x in new_verts if x not in vertex_order ])
897
+ success = len (new_verts == 1 )
898
+ n += 1
899
+ #if n > 4:
900
+ # print(n)
901
+ if len (new_verts ) > 0 :
902
+ vertex_order .append (new_verts [- 1 ])
903
+ else :
904
+ ct += 1
905
+ if verbose :
906
+ print (f"Skipped a vertex ({ ct } skipped)" )
907
+ #1/0
908
+ vertex_order .append (vertex_order [0 ])
909
+ return np .array (vertex_order )
813
910
814
911
815
- def _get_interpolated_outlines (overlay ,
912
+ def get_interpolated_outlines (overlay ,
816
913
outline_roi ,
817
914
geodesic_distances = True ,
818
915
resolution = 100 ,
@@ -898,9 +995,6 @@ def _get_interpolated_outlines(overlay,
898
995
# Convert to polar relative to these centroids
899
996
relative_phi , relative_rho = cart2pol (x ,y )
900
997
# Even angular interpolation
901
- ## CHANGED HERE: May require re-ordering of angles, may rest on assumption
902
- ## about what angle is first
903
- #relative_phi_interp, relative_rho_interp = interpolate_outlines(relative_phi, relative_rho, resolution=resolution)
904
998
relative_phi_interp , relative_rho_interp = resample_roi_outline (
905
999
relative_phi ,
906
1000
relative_rho ,
@@ -913,6 +1007,7 @@ def _get_interpolated_outlines(overlay,
913
1007
# Add back own centroid
914
1008
x_interp += centroid_x
915
1009
y_interp += centroid_y
1010
+ # Convert back to polar coordinates
916
1011
interpolated_outlines .append (np .stack (cart2pol (x_interp ,y_interp ), axis = 1 ))
917
1012
# cache result
918
1013
if verbose :
@@ -926,13 +1021,18 @@ def show_outlines_on_dartboard(
926
1021
** plot_kwargs ):
927
1022
"""Plot outlines of regions of interest on dartboard plots.
928
1023
1024
+ Plots all ROIs for one hemisphere to one axis.
1025
+
929
1026
Parameters
930
1027
----------
931
- outlines : _type_
932
- _description_
1028
+ outlines : array
1029
+ array of size (n_rois, n_points_along_boundary, coordinates), containing polar
1030
+ coordinates for outline of each ROI. Polar coordinates are expected to be
1031
+ (rho, theta), i.e. (radius, polar angle)
1032
+
933
1033
axis : matplotlib.axes._subplots.AxesSubplot, optional
934
1034
Matplotlib axis on which to plot.
935
- colors : _type_ , optional
1035
+ colors : matplotlib colorspec , optional
936
1036
List of colors to iterate through when plotting outlines. If None, will iterate through
937
1037
the default pyplot color cycle. Defaults to None.
938
1038
polar : bool, optional
@@ -1014,7 +1114,7 @@ def show_dartboard_pair(dartboard_data,
1014
1114
1015
1115
For lower-level functions, see:
1016
1116
`show_dartboard` #
1017
- `_get_interpolated_outlines ` # get roi outlines
1117
+ `get_interpolated_outlines ` # get roi outlines
1018
1118
`show_outlines_on_dartboard` # plot roi outlines
1019
1119
1020
1120
Parameters
@@ -1139,7 +1239,7 @@ def show_dartboard_pair(dartboard_data,
1139
1239
if not isinstance (roi_color , (list , tuple )):
1140
1240
roi_color = [roi_color ] * len (rois )
1141
1241
for roi in rois :
1142
- outlines [roi ] = _get_interpolated_outlines (overlay ,
1242
+ outlines [roi ] = get_interpolated_outlines (overlay ,
1143
1243
roi ,
1144
1244
geodesic_distances = geodesic_distances ,
1145
1245
resolution = path_resolution ,
@@ -1150,6 +1250,10 @@ def show_dartboard_pair(dartboard_data,
1150
1250
** dartboard_spec )
1151
1251
for hemi_index in range (2 ):
1152
1252
hemi_axis = axes [hemi_index ]
1253
+ # This may be somewhat confusing. Seems sensible to make the function plot
1254
+ # either one ROI at a time or to take a dict as input to obviate the need
1255
+ # for mapping to an array (and a totally different format than any other
1256
+ # function provides) here.
1153
1257
outlines_to_plot = np .array ([v for v in outlines .values ()])[:, hemi_index ]
1154
1258
if isinstance (dartboard_spec ['max_radii' ], tuple ):
1155
1259
rmax = dartboard_spec ['max_radii' ][hemi_index ]
@@ -1171,21 +1275,27 @@ def draw_mask_outlines(
1171
1275
outer_line = True , as_overlay = True , ** plot_kwargs ):
1172
1276
"""Given eccentricity-angle masks, fits and draws outlines of bins on the cortex.
1173
1277
1174
- Args:
1175
- masks np.ndarray: Vertex masks for each bin and hemisphere, such as the output of
1176
- get_eccentricity_angle_masks. Shape should be (2, eccentricities, angles, vertices).
1177
- axis (matplotlib.axes._subplots.AxesSubplot, optional): Matplotlb axis on which to plot.
1178
- eccentricities (bool, optional): Whether to draw outlines for separate eccentricities.
1179
- Defaults to True.
1180
- angles (bool, optional): Whether to draw outlines for separate angles.
1181
- Defaults to True.
1182
- outer_line (bool, optional): Whether to draw outline of outermost eccentricity.
1183
- Defaults to True.
1184
- as_overlay (bool, optional): Whether to create new axis on top of existing axis, to avoid
1185
- interfering with previously-plotted data. Defaults to False.
1186
-
1187
- Returns:
1188
- matplotlib.axes._subplots.AxesSubplot: Matplotlib axis in which data is plotted.
1278
+ Parameters
1279
+ ----------
1280
+ masks : np.ndarray
1281
+ Vertex masks for each bin and hemisphere, such as the output of get_eccentricity_angle_masks.
1282
+ Shape should be (2, eccentricities, angles, vertices).
1283
+ axis : matplotlib.axes._subplots.AxesSubplot, optional
1284
+ Matplotlib axis on which to plot.
1285
+ eccentricities : bool, optional
1286
+ Whether to draw outlines for separate eccentricities. Defaults to True.
1287
+ angles : bool, optional
1288
+ Whether to draw outlines for separate angles. Defaults to True.
1289
+ outer_line : bool, optional
1290
+ Whether to draw the outline of the outermost eccentricity. Defaults to True.
1291
+ as_overlay : bool, optional
1292
+ Whether to create a new axis on top of the existing axis to avoid interfering with
1293
+ previously-plotted data. Defaults to False.
1294
+
1295
+ Returns
1296
+ -------
1297
+ matplotlib.axes._subplots.AxesSubplot
1298
+ Matplotlib axis in which data is plotted.
1189
1299
"""
1190
1300
if axis is None :
1191
1301
_ , axis = plt .subplots ()
@@ -1526,8 +1636,9 @@ def get_dartboard_data(vertex_obj,
1526
1636
be used, e.g. ('STS', 'nearest') for the nearest point to the center_roi that
1527
1637
falls on STS.
1528
1638
1529
- Important: anchors must be specified counter clockwise from the RIGHT with
1530
- respect to the RIGHT hemisphere:
1639
+ Important: anchors must be specified counter clockwise from the RIGHT (per mathematical
1640
+ convention that rightward is zero degrees) with respect to the LEFT (alphabetically
1641
+ first) hemisphere:
1531
1642
2
1532
1643
/\
1533
1644
3 <- center -> 1
@@ -1675,8 +1786,9 @@ def dartboard_on_flatmap(vertex_data,
1675
1786
be used, e.g. ('STS', 'nearest') for the nearest point to the center_roi that
1676
1787
falls on STS.
1677
1788
1678
- Important: anchors must be specified counter clockwise from the RIGHT with
1679
- respect to the RIGHT hemisphere:
1789
+ Important: anchors must be specified counter clockwise from the RIGHT (per mathematical
1790
+ convention that rightward is zero degrees) with respect to the LEFT (alphabetically
1791
+ first) hemisphere:
1680
1792
2
1681
1793
/\
1682
1794
3 <- center -> 1
@@ -1842,3 +1954,17 @@ def dartboard_on_flatmap(vertex_data,
1842
1954
max_radii [0 ], max_radii [0 ]])
1843
1955
1844
1956
return fig
1957
+
1958
+
1959
+ def average_outlines (outlines ):
1960
+ outlines = np .array (outlines )
1961
+ for roi_i in range (outlines .shape [1 ]):
1962
+ for hemi in range (2 ):
1963
+ outlines [:, roi_i , hemi ] = np .array (
1964
+ pol2cart (* outlines [:, roi_i , hemi ].T )).T
1965
+ averaged_outlines = np .nanmean (outlines , axis = 0 )
1966
+ for roi_i in range (averaged_outlines .shape [0 ]):
1967
+ for hemi in range (2 ):
1968
+ averaged_outlines [roi_i , hemi ] = np .array (
1969
+ cart2pol (* averaged_outlines [roi_i , hemi ].T )).T
1970
+ return averaged_outlines
0 commit comments