Coverage for src/thunderfish/fishshapes.py: 98%

317 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-04-29 16:21 +0000

1"""Manipulate and plot fish outlines. 

2 

3## Fish shapes 

4 

5Fish shapes are dictionaries with the keys 'body', 'fin0', 'fin1' ..., 

6and 'eye'. The values are 2D arrays with x-y coordinates (first 

7dimension is points, second dimension coordinates) of the respective 

8pathes. 

9 

10All fish shapes of this module are accessible via these dictionaries: 

11 

12- `fish_shapes`: dictionary holding all electric fish shapes. 

13- `fish_top_shapes`: dictionary holding electric fish shapes viewed from top. 

14- `fish_side_shapes`: dictionary holding electric fish shapes viewed from the side. 

15 

16These are the shapes of various fish species: 

17 

18- `Alepto_top`: *Apteronotus leptorhynchus* viewed from top. 

19- `Alepto_male_side`: Male *Apteronotus leptorhynchus* viewed from the side. 

20- `Eigenmannia_top`: *Eigenmannia virescens* viewed from top. 

21- `Eigenmannia_side`: *Eigenmannia virescens* viewed from the side. 

22 

23Helper function for selecting a particular fish shape: 

24 

25- `fish_shape()`: get a dictinary containing shapes of a fish. 

26 

27## Plotting 

28 

29- `plot_fish()`: plot body, fins and eye of an electric fish. 

30- `plot_object()`: plot circular object. 

31- `plot_fishfinder()`: plot a fishfinder with electrodes and wires. 

32- `plot_pathes()`: plot pathes. 

33 

34## Fish surface and normals from shapes 

35 

36- `fish_surface()`: generate meshgrid of one side of the fish from shape. 

37- `surface_normals()`: normal vectors on a surface. 

38 

39## General path manipulations 

40 

41You may use these functions to extract and fine tune pathes from SVG 

42files in order to assemble fish shapes for this module. See 

43`export_fish_demo()` for a use case. 

44 

45- `extract_path()`: convert SVG coordinates to numpy array with path coordinates. 

46- `bbox_pathes()`: common bounding box of pathes. 

47- `translate_pathes()`: translate pathes in place. 

48- `center_pathes()`: translate pathes to their common origin in place. 

49- `rotate_pathes()`: rotate pathes in place. 

50- `flipy_pathes()`: flip pathes in y-direction in place. 

51- `flipx_pathes()`: flip pathes in x-direction in place. 

52- `export_path()`: print coordinates of path for import as numpy array. 

53- `mirror_path()`: complete path of half a fish outline by appending the mirrored path. 

54- `normalize_path()`: normalize fish outline to unit length. 

55- `bend_path()`: bend and scale a path. 

56 

57## Exporting fish outlines from pathes 

58 

59- `export_fish()`: serialize coordinates of fish outlines as a dictionary. 

60- `export_fish_demo()`: code demonstrating how to export fish outlines from SVG. 

61 

62""" 

63 

64import numpy as np 

65import matplotlib as mpl 

66import matplotlib.pyplot as plt 

67from matplotlib.path import Path 

68from matplotlib.patches import PathPatch, Circle, Rectangle 

69import matplotlib.transforms as mpt 

70 

71 

72Alepto_top = dict(body=np.array([ 

73 [-5.00000000e-01, 0.00000000e+00], [-4.99802704e-01, 1.23222860e-03], 

74 [-4.95374557e-01, 2.57983066e-03], [-4.84420392e-01, 3.29085947e-03], 

75 [-4.72487909e-01, 4.03497963e-03], [-4.13995354e-01, 4.39637211e-03], 

76 [-3.90529212e-01, 5.14049228e-03], [-3.67089631e-01, 5.88461244e-03], 

77 [-3.43596916e-01, 6.65006783e-03], [-3.20104187e-01, 7.39418800e-03], 

78 [-2.97063253e-01, 8.11708180e-03], [-2.79461930e-01, 8.49142780e-03], 

79 [-2.61664711e-01, 9.24382081e-03], [-2.38198570e-01, 1.03918950e-02], 

80 [-2.14732428e-01, 1.14974077e-02], [-1.91239699e-01, 1.26242555e-02], 

81 [-1.67773558e-01, 1.37511034e-02], [-1.44307403e-01, 1.52605701e-02], 

82 [-1.09307508e-01, 1.71314946e-02], [-8.58413531e-02, 1.86197349e-02], 

83 [-6.23486380e-02, 2.01079753e-02], [-3.88824966e-02, 2.12348231e-02], 

84 [-1.54429155e-02, 2.19789433e-02], [1.95835670e-02, 2.31057367e-02], 

85 [4.30231346e-02, 2.38498569e-02], [6.65424230e-02, 2.49767047e-02], 

86 [8.99820050e-02, 2.57421601e-02], [1.13448146e-01, 2.64862803e-02], 

87 [1.36914287e-01, 2.91013032e-02], [1.60380442e-01, 3.28431304e-02], 

88 [1.83873157e-01, 3.43101008e-02], [2.06914105e-01, 3.54369487e-02], 

89 [2.18819919e-01, 3.58196764e-02], [2.42339207e-01, 3.65850229e-02], 

90 [2.72903364e-01, 3.57933339e-02], [2.75411585e-01, 3.56061065e-02], 

91 [2.74126982e-01, 3.73081344e-02], [2.60251756e-01, 4.14908387e-02], 

92 [2.47930096e-01, 4.96419915e-02], [2.39119358e-01, 6.08413919e-02], 

93 [2.39547832e-01, 7.56059835e-02], [2.44279733e-01, 7.95534778e-02], 

94 [2.54298155e-01, 7.66782524e-02], [2.69627591e-01, 6.28724285e-02], 

95 [2.82177993e-01, 4.80249888e-02], [2.88316671e-01, 3.79294791e-02], 

96 [2.89271585e-01, 3.54368942e-02], [2.92213886e-01, 3.54205663e-02], 

97 [3.01203973e-01, 3.58192954e-02], [3.12737740e-01, 4.10493520e-02], 

98 [3.24670128e-01, 3.99438067e-02], [3.36177308e-01, 3.88170133e-02], 

99 [3.48109696e-01, 3.76902090e-02], [3.71177217e-01, 3.54366112e-02], 

100 [3.94643358e-01, 3.24601523e-02], [4.18136073e-01, 2.91010093e-02], 

101 [4.41602228e-01, 2.61033022e-02], [4.65041796e-01, 2.27229002e-02], 

102 [4.77000757e-01, 2.01078773e-02], [4.88938465e-01, 1.57516176e-02], 

103 [4.97051671e-01, 9.88149348e-03], [5.00000000e-01, 4.58499286e-03], 

104 [5.00000000e-01, -4.58499286e-03], [4.97051671e-01, -9.88149348e-03], 

105 [4.88938465e-01, -1.57516176e-02], [4.77000757e-01, -2.01078773e-02], 

106 [4.65041796e-01, -2.27229002e-02], [4.41602228e-01, -2.61033022e-02], 

107 [4.18136073e-01, -2.91010093e-02], [3.94643358e-01, -3.24601523e-02], 

108 [3.71177217e-01, -3.54366112e-02], [3.48109696e-01, -3.76902090e-02], 

109 [3.36177308e-01, -3.88170133e-02], [3.24670128e-01, -3.99438067e-02], 

110 [3.12737740e-01, -4.10493520e-02], [3.01203973e-01, -3.58192954e-02], 

111 [2.92213886e-01, -3.54205663e-02], [2.89271585e-01, -3.54368942e-02], 

112 [2.88316671e-01, -3.79294791e-02], [2.82177993e-01, -4.80249888e-02], 

113 [2.69627591e-01, -6.28724285e-02], [2.54298155e-01, -7.66782524e-02], 

114 [2.44279733e-01, -7.95534778e-02], [2.39547832e-01, -7.56059835e-02], 

115 [2.39119358e-01, -6.08413919e-02], [2.47930096e-01, -4.96419915e-02], 

116 [2.60251756e-01, -4.14908387e-02], [2.74126982e-01, -3.73081344e-02], 

117 [2.75411585e-01, -3.56061065e-02], [2.72903364e-01, -3.57933339e-02], 

118 [2.42339207e-01, -3.65850229e-02], [2.18819919e-01, -3.58196764e-02], 

119 [2.06914105e-01, -3.54369487e-02], [1.83873157e-01, -3.43101008e-02], 

120 [1.60380442e-01, -3.28431304e-02], [1.36914287e-01, -2.91013032e-02], 

121 [1.13448146e-01, -2.64862803e-02], [8.99820050e-02, -2.57421601e-02], 

122 [6.65424230e-02, -2.49767047e-02], [4.30231346e-02, -2.38498569e-02], 

123 [1.95835670e-02, -2.31057367e-02], [-1.54429155e-02, -2.19789433e-02], 

124 [-3.88824966e-02, -2.12348231e-02], [-6.23486380e-02, -2.01079753e-02], 

125 [-8.58413531e-02, -1.86197349e-02], [-1.09307508e-01, -1.71314946e-02], 

126 [-1.44307403e-01, -1.52605701e-02], [-1.67773558e-01, -1.37511034e-02], 

127 [-1.91239699e-01, -1.26242555e-02], [-2.14732428e-01, -1.14974077e-02], 

128 [-2.38198570e-01, -1.03918950e-02], [-2.61664711e-01, -9.24382081e-03], 

129 [-2.79461930e-01, -8.49142780e-03], [-2.97063253e-01, -8.11708180e-03], 

130 [-3.20104187e-01, -7.39418800e-03], [-3.43596916e-01, -6.65006783e-03], 

131 [-3.67089631e-01, -5.88461244e-03], [-3.90529212e-01, -5.14049228e-03], 

132 [-4.13995354e-01, -4.39637211e-03], [-4.72487909e-01, -4.03497963e-03], 

133 [-4.84420392e-01, -3.29085947e-03], [-4.95374557e-01, -2.57983066e-03], 

134 [-4.99802704e-01, -1.23222860e-03], [-5.00000000e-01, -0.00000000e+00],])) 

135"""Outline of an *Apteronotus leptorhynchus* viewed from top, modified from Krahe 2004.""" 

136 

137Alepto_male_side = dict(body=np.array([ 

138 [2.80332097e-01, 5.51361973e-02], [2.41127905e-01, 5.93460338e-02], 

139 [1.91463866e-01, 6.22667811e-02], [1.37379023e-01, 6.17716006e-02], 

140 [6.91234340e-02, 5.72953633e-02], [-1.36051588e-02, 4.74838393e-02], 

141 [-7.55221954e-02, 3.64211032e-02], [-1.60157310e-01, 2.45651115e-02], 

142 [-2.32035003e-01, 1.55421483e-02], [-2.99079447e-01, 9.70960800e-03], 

143 [-3.62251791e-01, 6.27265707e-03], [-4.20527920e-01, 4.22449025e-03], 

144 [-4.72735573e-01, 5.39606712e-03], [-4.80154179e-01, 5.86398206e-03], 

145 [-4.92605065e-01, 1.01411700e-02], [-4.97402289e-01, 5.91543079e-03], 

146 [-5.00000000e-01, -2.84973497e-03], [-4.97832769e-01, -1.17981289e-02], 

147 [-4.93106950e-01, -1.43380199e-02], [-4.81164618e-01, -8.19215843e-03], 

148 [-4.72578673e-01, -6.17623988e-03], [-4.45390092e-01, -5.96123217e-03], 

149 [-3.74805165e-01, -9.05994885e-03], [-3.33716813e-01, -1.08317142e-02], 

150 [-3.08099380e-01, -1.15017063e-02], [-2.82451613e-01, -1.30396176e-02], 

151 [-2.34498580e-01, -2.21834040e-02], [-1.86892658e-01, -3.26728000e-02], 

152 [-1.08738732e-01, -4.99024273e-02], [-3.50753879e-02, -5.94218882e-02], 

153 [3.28767168e-02, -6.58397526e-02], [1.25319086e-01, -7.21513968e-02], 

154 [1.99523049e-01, -7.99740378e-02], [2.37035792e-01, -8.44828747e-02], 

155 [2.74475366e-01, -8.68964223e-02], [3.12742824e-01, -8.34038539e-02], 

156 [3.36340505e-01, -7.82231053e-02], [3.55492327e-01, -7.21451373e-02], 

157 [3.74670470e-01, -6.45564453e-02], [3.82920881e-01, -6.06824741e-02], 

158 [3.84828678e-01, -5.92550189e-02], [3.86562866e-01, -5.99353293e-02], 

159 [3.90753372e-01, -6.01589140e-02], [4.03494946e-01, -5.90960625e-02], 

160 [4.38474761e-01, -6.13270959e-02], [4.61389913e-01, -6.47960654e-02], 

161 [4.77010163e-01, -6.86433853e-02], [4.84437594e-01, -6.89404377e-02], 

162 [4.90842798e-01, -6.82840746e-02], [4.94567181e-01, -6.58050993e-02], 

163 [4.95443985e-01, -6.30972916e-02], [4.94497789e-01, -6.10849673e-02], 

164 [4.91729699e-01, -6.00016418e-02], [4.84298546e-01, -5.78808424e-02], 

165 [4.93112897e-01, -5.45550751e-02], [4.97742360e-01, -5.12667865e-02], 

166 [5.00000000e-01, -4.73196051e-02], [4.99521047e-01, -4.36153642e-02], 

167 [4.96159278e-01, -3.87756472e-02], [4.86402575e-01, -3.18513601e-02], 

168 [4.67134496e-01, -2.06920393e-02], [4.39218141e-01, -5.92866768e-03], 

169 [4.25010402e-01, 4.45359743e-03], [4.14788070e-01, 1.39860522e-02], 

170 [3.93656086e-01, 2.44160739e-02], [3.75679976e-01, 2.94323719e-02], 

171 [3.61404254e-01, 3.69002336e-02], [3.37900061e-01, 4.40458301e-02], 

172 [3.11463577e-01, 4.97553861e-02],]), 

173 fin0=np.array([ 

174 [3.29593304e-01, -7.95912942e-02], [3.27561074e-01, -8.48367727e-02], 

175 [3.08709726e-01, -9.90609655e-02], [2.80934315e-01, -1.08062137e-01], 

176 [2.58017473e-01, -1.12878542e-01], [2.35142157e-01, -1.14467112e-01], 

177 [2.18081531e-01, -1.12354592e-01], [1.98185626e-01, -1.10721292e-01], 

178 [1.78099090e-01, -1.13640193e-01], [1.59752865e-01, -1.18762090e-01], 

179 [1.40752841e-01, -1.20266781e-01], [1.27904629e-01, -1.17712356e-01], 

180 [1.19134213e-01, -1.12284346e-01], [1.09580014e-01, -1.04436264e-01], 

181 [8.20184710e-02, -9.60992771e-02], [5.05598670e-02, -9.57289587e-02], 

182 [2.74790284e-02, -1.04021601e-01], [3.92704920e-03, -1.08834461e-01], 

183 [-3.12710137e-02, -1.08965162e-01], [-5.88865488e-02, -1.03820945e-01], 

184 [-7.82549598e-02, -9.45428978e-02], [-9.94601687e-02, -8.20174601e-02], 

185 [-1.29941640e-01, -7.01658118e-02], [-1.58259295e-01, -6.73695625e-02], 

186 [-1.86001442e-01, -7.01570717e-02], [-2.14339679e-01, -6.79007296e-02], 

187 [-2.38708971e-01, -5.78982409e-02], [-2.55168178e-01, -4.41230328e-02], 

188 [-2.71293058e-01, -3.28785160e-02], [-2.88416341e-01, -2.86291802e-02], 

189 [-3.06103856e-01, -2.82461534e-02], [-3.22345146e-01, -2.47128040e-02], 

190 [-3.38333410e-01, -1.44124470e-02], [-3.43264223e-01, -1.03691894e-02], 

191 [-3.08609907e-01, -1.12571357e-02], [-2.86088545e-01, -1.25633719e-02], 

192 [-2.59977440e-01, -1.65414204e-02], [-2.16119429e-01, -2.64072955e-02], 

193 [-1.68443229e-01, -3.68996138e-02], [-1.12717944e-01, -4.88585839e-02], 

194 [-7.07908982e-02, -5.51259999e-02], [-1.80906639e-02, -6.16068166e-02], 

195 [2.75299392e-02, -6.53080983e-02], [7.71390030e-02, -6.85205021e-02], 

196 [1.21071140e-01, -7.25104674e-02], [1.78723549e-01, -7.85286909e-02], 

197 [2.32100395e-01, -8.40268652e-02], [2.74938812e-01, -8.74456073e-02], 

198 [3.10041908e-01, -8.43007220e-02],]), 

199 eye=np.array([0.4, 0.0, 0.01])) 

200"""Outline of an *Apteronotus leptorhynchus* male viewed from the side.""" 

201 

202Eigenmannia_top = dict(body=np.array([ 

203 [-5.00000000e-01, 0.00000000e+00], [-4.84515329e-01, 4.41536208e-03], 

204 [-4.76913801e-01, 5.34924846e-03], [-3.94680346e-01, 8.25734868e-03], 

205 [-2.74106007e-01, 8.94059314e-03], [-1.35145770e-01, 1.09559947e-02], 

206 [2.36080412e-02, 1.40941342e-02], [1.36968804e-01, 1.51550643e-02], 

207 [2.15041020e-01, 1.96734219e-02], [2.83582110e-01, 2.36895289e-02], 

208 [3.20834553e-01, 2.63067663e-02], [3.46646908e-01, 2.77590937e-02], 

209 [3.68462758e-01, 2.97229886e-02], [3.62525174e-01, 3.12766064e-02], 

210 [3.57215426e-01, 3.25163153e-02], [3.51347983e-01, 3.44809486e-02], 

211 [3.46108357e-01, 3.83290703e-02], [3.44207747e-01, 4.53621620e-02], 

212 [3.46387987e-01, 5.39648157e-02], [3.54784122e-01, 6.69720204e-02], 

213 [3.67470562e-01, 8.11691502e-02], [3.80987875e-01, 9.13148567e-02], 

214 [3.90738756e-01, 9.39276818e-02], [3.95854520e-01, 9.06728175e-02], 

215 [3.99717109e-01, 8.49081236e-02], [3.96997843e-01, 6.54750599e-02], 

216 [3.89101023e-01, 4.11631100e-02], [3.86289062e-01, 3.71837960e-02], 

217 [3.94553267e-01, 3.78052325e-02], [4.03373690e-01, 3.72181278e-02], 

218 [4.20207675e-01, 3.56696607e-02], [4.37553246e-01, 3.46018748e-02], 

219 [4.59139056e-01, 3.15068918e-02], [4.79811600e-01, 2.68634593e-02], 

220 [4.92810472e-01, 1.97499259e-02], [4.98594784e-01, 1.11517021e-02], 

221 [5.00000000e-01, 5.62393850e-03], [5.00000000e-01, -5.62393850e-03], 

222 [4.98594784e-01, -1.11517021e-02], [4.92810472e-01, -1.97499259e-02], 

223 [4.79811600e-01, -2.68634593e-02], [4.59139056e-01, -3.15068918e-02], 

224 [4.37553246e-01, -3.46018748e-02], [4.20207675e-01, -3.56696607e-02], 

225 [4.03373690e-01, -3.72181278e-02], [3.94553267e-01, -3.78052325e-02], 

226 [3.86289062e-01, -3.71837960e-02], [3.89101023e-01, -4.11631100e-02], 

227 [3.96997843e-01, -6.54750599e-02], [3.99717109e-01, -8.49081236e-02], 

228 [3.95854520e-01, -9.06728175e-02], [3.90738756e-01, -9.39276818e-02], 

229 [3.80987875e-01, -9.13148567e-02], [3.67470562e-01, -8.11691502e-02], 

230 [3.54784122e-01, -6.69720204e-02], [3.46387987e-01, -5.39648157e-02], 

231 [3.44207747e-01, -4.53621620e-02], [3.46108357e-01, -3.83290703e-02], 

232 [3.51347983e-01, -3.44809486e-02], [3.57215426e-01, -3.25163153e-02], 

233 [3.62525174e-01, -3.12766064e-02], [3.68462758e-01, -2.97229886e-02], 

234 [3.46646908e-01, -2.77590937e-02], [3.20834553e-01, -2.63067663e-02], 

235 [2.83582110e-01, -2.36895289e-02], [2.15041020e-01, -1.96734219e-02], 

236 [1.36968804e-01, -1.51550643e-02], [2.36080412e-02, -1.40941342e-02], 

237 [-1.35145770e-01, -1.09559947e-02], [-2.74106007e-01, -8.94059314e-03], 

238 [-3.94680346e-01, -8.25734868e-03], [-4.76913801e-01, -5.34924846e-03], 

239 [-4.84515329e-01, -4.41536208e-03], [-5.00000000e-01, -0.00000000e+00],])) 

240"""Outline of an *Eigenmannia virescens* viewed from top.""" 

241 

242Eigenmannia_side = dict(body=np.array([ 

243 [7.39835590e-02, 4.57421567e-02], [1.36190672e-01, 5.20008556e-02], 

244 [1.88575637e-01, 5.31087788e-02], [2.55693889e-01, 4.90162062e-02], 

245 [2.91989388e-01, 4.57421567e-02], [3.30997244e-01, 4.08310609e-02], 

246 [3.60079352e-01, 3.50312357e-02], [3.86267547e-01, 2.72057399e-02], 

247 [4.09748495e-01, 1.88510343e-02], [4.30914243e-01, 1.02069720e-02], 

248 [4.43253678e-01, 5.18028074e-03], [4.61959655e-01, -3.75313831e-03], 

249 [4.82422519e-01, -1.50677197e-02], [4.93493046e-01, -2.26243878e-02], 

250 [4.97325280e-01, -2.75603439e-02], [5.00000000e-01, -3.36538136e-02], 

251 [4.99855343e-01, -3.81556262e-02], [4.97829629e-01, -4.26574388e-02], 

252 [4.95229403e-01, -4.49683083e-02], [4.93207934e-01, -4.68450344e-02], 

253 [4.90607707e-01, -4.83870578e-02], [4.92124870e-01, -5.04085273e-02], 

254 [4.93063234e-01, -5.27193968e-02], [4.93063190e-01, -5.47905000e-02], 

255 [4.91905677e-01, -5.65722031e-02], [4.87982621e-01, -5.83539496e-02], 

256 [4.81889151e-01, -5.99909526e-02], [4.72187579e-01, -6.31614903e-02], 

257 [4.57251469e-01, -6.96684443e-02], [4.42315315e-01, -7.44390846e-02], 

258 [4.31434877e-01, -7.64563096e-02], [4.21852452e-01, -8.03091592e-02], 

259 [4.12030260e-01, -8.11773161e-02], [3.97297016e-01, -8.61380457e-02], 

260 [3.84200775e-01, -9.05200184e-02], [3.71589870e-01, -9.38291926e-02], 

261 [3.58008292e-01, -9.50424035e-02], [3.33452813e-01, -9.34053571e-02], 

262 [2.99075185e-01, -8.68572582e-02], [2.70427177e-01, -8.11276391e-02], 

263 [2.32775500e-01, -7.31023958e-02], [2.00034918e-01, -6.81912999e-02], 

264 [1.71386866e-01, -6.43481085e-02], [1.37488988e-01, -5.96768656e-02], 

265 [8.87168470e-02, -5.53444400e-02], [3.71504052e-02, -5.08426274e-02], 

266 [-8.94935470e-03, -4.47741911e-02], [-6.68009664e-02, -3.60218095e-02], 

267 [-1.11819296e-01, -3.02864735e-02], [-1.55609841e-01, -2.46444281e-02], 

268 [-2.01855938e-01, -1.98208625e-02], [-2.61607520e-01, -1.41655641e-02], 

269 [-3.02124011e-01, -9.83500080e-03], [-3.47551590e-01, -8.19795443e-03], 

270 [-3.86021794e-01, -7.21576125e-03], [-4.19580907e-01, -5.90618477e-03], 

271 [-4.49047446e-01, -5.00584824e-03], [-4.82606558e-01, -4.29793979e-03], 

272 [-4.93367213e-01, -3.88865654e-03], [-4.96609514e-01, -3.33497643e-03], 

273 [-4.98599358e-01, -2.28352992e-03], [-5.00000000e-01, -4.13646830e-04], 

274 [-4.99911798e-01, 1.42799787e-03], [-4.97749085e-01, 3.02268669e-03], 

275 [-4.94153971e-01, 3.94706050e-03], [-4.48842818e-01, 5.27946155e-03], 

276 [-3.90932887e-01, 5.88974836e-03], [-3.04988822e-01, 7.10408527e-03], 

277 [-2.43785835e-01, 8.93052803e-03], [-1.87718481e-01, 1.20250559e-02], 

278 [-1.39987578e-01, 1.55534240e-02], [-9.58582596e-02, 1.92113768e-02], 

279 [-4.87936436e-02, 2.54739303e-02], [-1.20172913e-02, 3.11685979e-02], 

280 [3.65545828e-02, 3.98634200e-02],]), 

281 fin0=np.array([ 

282 [-3.23227396e-01, -8.73526322e-03], [-3.17729007e-01, -1.49720903e-02], 

283 [-3.11901320e-01, -2.06301173e-02], [-2.94537996e-01, -2.87329729e-02], 

284 [-2.73702014e-01, -3.62471102e-02], [-2.48814582e-01, -4.42901541e-02], 

285 [-2.26392044e-01, -4.89203820e-02], [-2.11413629e-01, -4.97652813e-02], 

286 [-1.97592770e-01, -4.71608105e-02], [-1.88292360e-01, -4.37113973e-02], 

287 [-1.77575020e-01, -4.26201918e-02], [-1.63230314e-01, -4.13425351e-02], 

288 [-1.45633053e-01, -4.58128611e-02], [-1.32102997e-01, -5.21132245e-02], 

289 [-1.22627830e-01, -5.98022925e-02], [-1.16274541e-01, -6.51393895e-02], 

290 [-1.01226326e-01, -6.99292162e-02], [-8.87826127e-02, -7.09420732e-02], 

291 [-7.63388990e-02, -7.02186163e-02], [-6.41845810e-02, -6.63566715e-02], 

292 [-4.99997329e-02, -6.35107453e-02], [-3.86044383e-02, -6.71556184e-02], 

293 [-2.83003535e-02, -7.56835222e-02], [-1.41203129e-02, -8.28817968e-02], 

294 [1.21728460e-03, -8.66205668e-02], [1.22140543e-02, -8.75385740e-02], 

295 [2.16240177e-02, -8.43285373e-02], [3.27836777e-02, -8.13081568e-02], 

296 [3.98554860e-02, -8.02952999e-02], [4.86770343e-02, -7.96350762e-02], 

297 [5.81904230e-02, -8.20450399e-02], [6.47198980e-02, -8.65937577e-02], 

298 [7.29857310e-02, -9.36024194e-02], [8.47509570e-02, -9.91141438e-02], 

299 [1.00477612e-01, -1.02776515e-01], [1.28258936e-01, -1.02826321e-01], 

300 [1.45605097e-01, -1.02460349e-01], [1.59342462e-01, -9.97657918e-02], 

301 [1.76140399e-01, -9.72111283e-02], [1.89366052e-01, -9.61800377e-02], 

302 [2.03938918e-01, -9.84587276e-02], [2.14786136e-01, -1.02170949e-01], 

303 [2.24046592e-01, -1.08953357e-01], [2.34464605e-01, -1.14112491e-01], 

304 [2.47925953e-01, -1.18114112e-01], [2.65013334e-01, -1.19108779e-01], 

305 [2.83520819e-01, -1.15835465e-01], [2.98329467e-01, -1.08650574e-01], 

306 [3.15014321e-01, -1.04499489e-01], [3.28805304e-01, -1.04273408e-01], 

307 [3.39387031e-01, -1.06211982e-01], [3.52278630e-01, -1.03431974e-01], 

308 [3.61896180e-01, -1.00567165e-01], [3.67032403e-01, -9.80662488e-02], 

309 [3.71589870e-01, -9.38289761e-02], [3.58008292e-01, -9.50421869e-02], 

310 [3.33452813e-01, -9.34051405e-02], [3.06441808e-01, -8.84940880e-02], 

311 [2.35043362e-01, -7.35981699e-02], [1.65011316e-01, -6.31802003e-02], 

312 [1.25654422e-01, -5.85499724e-02], [9.49792270e-02, -5.56561016e-02], 

313 [4.05741354e-02, -5.11056947e-02], [-1.24746680e-03, -4.58268936e-02], 

314 [-5.20302500e-02, -3.81131387e-02], [-1.01805114e-01, -3.16101258e-02], 

315 [-1.51874267e-01, -2.50855445e-02], [-2.01943420e-01, -2.02074944e-02], 

316 [-2.61607516e-01, -1.41653476e-02], [-3.02124016e-01, -9.83478430e-03], 

317 [-3.12840355e-01, -9.28491550e-03],]), 

318 eye=np.array([0.46, -0.03, 0.005])) 

319"""Outline of an *Eigenmannia virescens* viewed from the side.""" 

320 

321fish_shapes = dict(Alepto_top=Alepto_top, 

322 Alepto_male_side=Alepto_male_side, 

323 Eigenmannia_top=Eigenmannia_top, 

324 Eigenmannia_side=Eigenmannia_side) 

325"""Dictionary holding all electric fish shapes.""" 

326 

327fish_top_shapes = dict(Alepto=Alepto_top, 

328 Eigenmannia=Eigenmannia_top) 

329"""Dictionary holding electric fish shapes viewed from top.""" 

330 

331fish_side_shapes = dict(Alepto_male=Alepto_male_side, 

332 Eigenmannia=Eigenmannia_side) 

333"""Dictionary holding electric fish shapes viewed from the side.""" 

334 

335 

336def fish_shape(fish): 

337 """Get a dictinary containing shapes of a fish. 

338 

339 Parameters 

340 ---------- 

341 fish: string or tuple or dict 

342 Specifies a fish to show: 

343 - any of the strings defining a shape contained in the `fish_shapes` dictionary, 

344 - a tuple with the name of the fish as the first element and 'top' or 'side' as the second element, 

345 - a dictionary with at least a 'body' key holding pathes to be drawn. 

346 

347 Returns 

348 ------- 

349 fish: dict 

350 Dictionary with at least a 'body' key holding pathes to be drawn. 

351 """ 

352 if not isinstance(fish, dict): 

353 if isinstance(fish, (tuple, list)): 

354 if fish[1] == 'top': 

355 fish = fish_top_shapes[fish[0]] 

356 else: 

357 fish = fish_side_shapes[fish[0]] 

358 else: 

359 fish = fish_shapes[fish] 

360 return fish 

361 

362 

363def plot_fish(ax, fish, pos=(0, 0), direction=(1, 0), size=20.0, bend=0, scaley=1, 

364 bodykwargs={}, finkwargs={}, eyekwargs=None): 

365 """Plot body, fins and eye of an electric fish. 

366 

367 Parameters 

368 ---------- 

369 ax: matplotlib axes 

370 Axes where to draw the fish. 

371 fish: string or tuple or dict 

372 Specifies a fish to show: 

373 - any of the strings defining a shape contained in the `fish_shapes` dictionary, 

374 - a tuple with the name of the fish as the first element and 'top' or 'side' as the second element, 

375 - a dictionary with at least a 'body' key holding pathes to be drawn. 

376 pos: tuple of floats 

377 Coordinates of the fish's position (its center). 

378 direction: tuple of floats 

379 Coordinates of a vector defining the orientation of the fish. 

380 size: float 

381 Size of the fish. 

382 bend: float 

383 Bending angle of the fish's tail in degree. 

384 scaley: float 

385 Scale factor applied in y direction after bending and rotation to 

386 compensate for differently scaled axes. 

387 bodykwargs: dict 

388 Key-word arguments for PathPatch used to draw the fish's body. 

389 finkwargs: dict 

390 Key-word arguments for PathPatch used to draw the fish's fins. 

391 

392 Returns 

393 ------- 

394 bpatch: matplotlib.patches.PathPatch 

395 The fish's body. Can be used for set_clip_path(). 

396 

397 Example 

398 ------- 

399 

400 ``` 

401 fig, ax = plt.subplots() 

402 bodykwargs=dict(lw=1, edgecolor='k', facecolor='k') 

403 finkwargs=dict(lw=1, edgecolor='k', facecolor='grey') 

404 fish = (('Eigenmannia', 'side'), (0, 0), (1, 0), 20.0, -25) 

405 plot_fish(ax, *fish, bodykwargs=bodykwargs, finkwargs=finkwargs) 

406 ax.set_xlim(-15, 15) 

407 ax.set_ylim(-10, 10) 

408 plt.show() 

409 ``` 

410 """ 

411 fish = fish_shape(fish) 

412 bpatch = None 

413 size_fac = 1.1 

414 bbox = bbox_pathes(*fish.values()) 

415 trans = mpl.transforms.Affine2D() 

416 angle = np.arctan2(direction[1], direction[0]) 

417 trans.rotate(angle) 

418 #trans.scale(dxu/dyu, dyu/dxu) # what is the right scaling???? 

419 trans.scale(1, scaley) 

420 trans.translate(*pos) 

421 for part, verts in fish.items(): 

422 if part == 'eye': 

423 if eyekwargs is not None: 

424 verts = np.array(verts)*size*size_fac 

425 verts[:2] = trans.transform_point(verts[:2]) 

426 if not 'zorder' in eyekwargs: 

427 eyekwargs['zorder'] = 20 

428 ax.add_patch(Circle(verts[:2], verts[2], **eyekwargs)) 

429 continue 

430 verts = bend_path(verts, bend, size, size_fac) 

431 codes = np.zeros(len(verts)) 

432 codes[:] = Path.LINETO 

433 codes[0] = Path.MOVETO 

434 codes[-1] = Path.CLOSEPOLY 

435 path = Path(verts, codes) 

436 #pixelx = np.abs(np.diff(ax.get_window_extent().get_points()[:,0]))[0] 

437 #pixely = np.abs(np.diff(ax.get_window_extent().get_points()[:,1]))[0] 

438 #xmin, xmax = ax.get_xlim() 

439 #ymin, ymax = ax.get_ylim() 

440 #dxu = np.abs(xmax - xmin)/pixelx 

441 #dyu = np.abs(ymax - ymin)/pixely 

442 path = path.transformed(trans) 

443 kwargs = bodykwargs if part == 'body' else finkwargs 

444 if not 'zorder' in kwargs: 

445 kwargs['zorder'] = 0 if part == 'body' else 10 

446 patch = PathPatch(path, **kwargs) 

447 if part == 'body': 

448 bpatch = patch 

449 ax.add_patch(patch) 

450 return bpatch 

451 

452 

453def plot_object(ax, pos=(0, 0), radius=1.0, **kwargs): 

454 """Plot circular object. 

455 

456 Parameters 

457 ---------- 

458 ax: matplotlib axes 

459 Axes where to draw the object. 

460 pos: tuple of floats 

461 Coordinates of the objects's position (its center). 

462 radius: float 

463 Radius of the cirular object. 

464 kwargs: key word arguments 

465 Arguments for Circle used to draw the obkect. 

466 """ 

467 ax.add_patch(Circle(pos, radius, **kwargs)) 

468 

469 

470def plot_fishfinder(ax, pos, direction, length, handle=0.05, 

471 central_ground=False, wires=False, 

472 rodkwargs=dict(edgecolor='none', facecolor='gray'), 

473 poskwargs=dict(edgecolor='none', facecolor='red'), 

474 negkwargs=dict(edgecolor='none', facecolor='blue'), 

475 gndkwargs=dict(edgecolor='none', facecolor='black'), 

476 lw=1, zorder=50): 

477 """Plot a fishfinder with electrodes and wires. 

478 

479 Parameters 

480 ---------- 

481 ax: matplotlib axes 

482 Axes where to draw the fishfinder. 

483 pos: tuple of floats 

484 Coordinates of the fishfinder's position (its center). 

485 direction: tuple of floats 

486 Coordinates defining the orientation of the fishfinder. 

487 length: float 

488 Length of the fishfinder (center of positive electrode 

489 minus center of negative electrode). 

490 handle: float 

491 Length of handle (rod beyond the negative electrode) 

492 as a fraction of the `length` of fishfinder. 

493 central_ground: bool 

494 Add a central ground electrode. 

495 wires: bool, 'postop' or 'negtop' 

496 Draw wires for each electrode. 

497 - True or 'postop': draw wire of positive electrode on top. 

498 - 'negtop': draw wire of negative electrode on top. 

499 Return the coordinates of the endpoints of the wires. 

500 rodkwargs: dict 

501 Key-word arguments for Rectangle used to draw the rod. 

502 poskwargs: dict 

503 Key-word arguments for Rectangle used to draw the positive electrode. 

504 negkwargs: dict 

505 Key-word arguments for Rectangle used to draw the negative electrode. 

506 gndkwargs: dict 

507 Key-word arguments for Rectangle used to draw the ground electrode. 

508 lw: float 

509 Width of the lines used for drawing the wires. 

510 zorder: int 

511 zorder for the fishfinder. 

512 

513 Returns 

514 ------- 

515 negpos: tuple of floats 

516 Coordinates of center of negative electrode. 

517 pospos: tuple of floats 

518 Coordinates of center of positive electrode. 

519 negwirepos: tuple of floats 

520 If `wire`, the end of the wire of the negative electrode. 

521 poswirepos: tuple of floats 

522 If `wire`, the end of the wire of the positive electrode. 

523 gndwirepos: tuple of floats 

524 If `central_ground` and `wire`, the end of the wire of 

525 the ground electrode. 

526 """ 

527 width = 0.07*length 

528 transform = mpt.Affine2D().rotate(np.arctan2(direction[1], direction[0])).translate(*pos) 

529 

530 ax.add_patch(Rectangle((-(0.5+handle)*length, -0.5*width), 

531 (1+handle+0.05)*length, width, 

532 transform=transform + ax.transData, 

533 zorder=zorder, **rodkwargs)) 

534 ax.add_patch(Rectangle((0.5*length-0.4*width, -0.6*width), 

535 0.8*width, 1.2*width, 

536 transform=transform + ax.transData, 

537 zorder=zorder+2, **poskwargs)) 

538 ax.add_patch(Rectangle((-0.5*length-0.4*width, -0.6*width), 

539 0.8*width, 1.2*width, 

540 transform=transform + ax.transData, 

541 zorder=zorder+2, **negkwargs)) 

542 nodes = [(-0.5*length, 0), (0.5*length, 0)] 

543 if central_ground: 

544 ax.add_patch(Rectangle((-0.4*width, -0.6*width), 

545 0.8*width, 1.2*width, 

546 transform=transform + ax.transData, 

547 zorder=zorder+2, **gndkwargs)) 

548 if wires: 

549 offs = 0.03*width*lw 

550 if wires == 'negtop': 

551 offs *= -1 

552 if central_ground: 

553 offs *= 2 

554 color = negkwargs.get('facecolor') 

555 ax.plot((-0.5*length, -(0.5+handle)*length), (-offs, -offs), 

556 color=color, lw=lw, solid_capstyle='butt', 

557 transform=transform + ax.transData, zorder=zorder+1) 

558 color = poskwargs.get('facecolor') 

559 ax.plot((0.5*length, -(0.5+handle)*length), (offs, offs), 

560 color=color, lw=lw, solid_capstyle='butt', transform=transform + 

561 ax.transData, zorder=zorder+1) 

562 nodes.extend(((-(0.5+handle)*length, -offs), (-(0.5+handle)*length, offs))) 

563 if central_ground: 

564 color = gndkwargs.get('facecolor') 

565 ax.plot((0, -(0.5+handle)*length), (0, 0), 

566 color=color, lw=lw, solid_capstyle='butt', 

567 transform=transform + ax.transData, zorder=zorder+1) 

568 nodes.append((-(0.5+handle)*length, 0)) 

569 nodes = transform.transform(nodes) 

570 return nodes 

571 

572 

573def plot_pathes(ax, *vertices, **kwargs): 

574 """Plot pathes. 

575 

576 Parameters 

577 ---------- 

578 ax: matplotlib axes 

579 Axes where to draw the path. 

580 vertices: one or more 2D arrays 

581 The coordinates of pathes to be plotted 

582 (first column x-coordinates, second colum y-coordinates). 

583 kwargs: key word arguments 

584 Arguments for PathPatch used to draw the path. 

585 """ 

586 for verts in vertices: 

587 codes = np.zeros(len(verts)) 

588 codes[:] = Path.LINETO 

589 codes[0] = Path.MOVETO 

590 codes[-1] = Path.CLOSEPOLY 

591 path = Path(verts, codes) 

592 ax.add_patch(PathPatch(path, **kwargs)) 

593 bbox = bbox_pathes(*vertices) 

594 center = np.mean(bbox, axis=0) 

595 bbox -= center 

596 bbox *= 1.2 

597 bbox += center 

598 ax.set_xlim(*bbox[:,0]) 

599 ax.set_ylim(*bbox[:,1]) 

600 

601 

602def fish_surface(fish, pos=(0, 0), direction=(1, 0), size=20.0, bend=0, 

603 gamma=1.0): 

604 """Generate meshgrid of one side of the fish from shape. 

605  

606 Parameters 

607 ---------- 

608 fish: string or tuple or dict 

609 Specifies a fish to show: 

610 - any of the strings defining a shape contained in the `fish_shapes` dictionary, 

611 - a tuple with the name of the fish and 'top' or 'side', 

612 - a dictionary with at least a 'body' key holding pathes to be drawn. 

613 pos: tuple of floats 

614 Coordinates of the fish's position (its center). 

615 direction: tuple of floats 

616 Coordinates of a vector defining the orientation of the fish. 

617 size: float 

618 Size of the fish. 

619 bend: float 

620 Bending angle of the fish's tail in degree. 

621 gamma: float 

622 Gamma distortion of the ellipse. The ellipse equation is raised 

623 to the power of gamma before its smaller diameter is scaled up 

624 from one to the actual value. 

625 

626 Returns 

627 ------- 

628 xx: 2D array of floats 

629 x-coordinates in direction of body axis. 

630 yy: 2D array of floats 

631 y-coordinates in direction upwards from body axis. 

632 zz: 2D array of floats 

633 z-coordinates of fish surface, outside of fish NaN. 

634 """ 

635 if direction[1] != 0: 

636 raise ValueError('rotation not supported by fish_surface yet.') 

637 fish = fish_shape(fish) 

638 bbox = bbox_pathes(*fish.values()) 

639 size_fac = -1.05*0.5/bbox[0,0] 

640 path = bend_path(fish['body'], bend, size, size_fac) 

641 # split in top and bottom half: 

642 minxi = np.argmin(path[:,0]) 

643 maxxi = np.argmax(path[:,0]) 

644 i0 = min(minxi, maxxi) 

645 i1 = max(minxi, maxxi) 

646 path0 = path[i0:i1,:] 

647 path1 = np.vstack((path[i0::-1,:], path[:i1:-1,:])) 

648 if np.mean(path0[:,1]) < np.mean(path1[:,1]): 

649 path0, path1 = path1, path0 

650 # make sure x coordinates are monotonically increasing: 

651 pm = np.maximum.accumulate(path0[:,0]) 

652 path0 = np.delete(path0, np.where(path0[:,0] < pm)[0], axis=0) 

653 pm = np.maximum.accumulate(path1[:,0]) 

654 path1 = np.delete(path1, np.where(path1[:,0] < pm)[0], axis=0) 

655 # rotate: XXX 

656 # translate: 

657 minx = path[minxi,0] + pos[0] 

658 maxx = path[maxxi,0] + pos[0] 

659 path0 += pos[:2] 

660 path1 += pos[:2] 

661 # interpolate: 

662 n = 5*max(len(path0), len(path1)) 

663 #n = 200 

664 x = np.linspace(minx, maxx, n) 

665 upperpath = np.zeros((len(x), 2)) 

666 upperpath[:,0] = x 

667 upperpath[:,1] = np.interp(x, path0[:,0], path0[:,1]) 

668 lowerpath = np.zeros((len(x), 2)) 

669 lowerpath[:,0] = x 

670 lowerpath[:,1] = np.interp(x, path1[:,0], path1[:,1]) 

671 # ellipse origin and semi axes: 

672 midline = np.array(upperpath) 

673 midline[:,1] = np.mean(np.vstack((upperpath[:,1], lowerpath[:,1])), axis=0) 

674 diamy = upperpath[:,1] - midline[:,1] 

675 diamz = 0.3*diamy # take it from the top view! 

676 # apply ellipse: 

677 y = np.linspace(np.min(midline[:,1]-diamy), np.max(midline[:,1]+diamy), n//2) 

678 xx, yy = np.meshgrid(x ,y) 

679 zz = diamz * (np.sqrt(1.0 - ((yy-midline[:,1])/diamy)**2))**gamma 

680 return xx, yy, zz 

681 

682 

683def surface_normals(xx, yy, zz): 

684 """Normal vectors on a surface. 

685 

686 Compute surface normals on a surface as returned by `fish_surface()`. 

687 

688 Parameters 

689 ---------- 

690 xx: 2D array of floats 

691 Mesh grid of x coordinates. 

692 yy: 2D array of floats 

693 Mesh grid of y coordinates. 

694 zz: 2D array of floats 

695 z-coordinates of surface on the xx and yy coordinates. 

696 

697 Returns 

698 ------- 

699 nx: 2D array of floats 

700 x-coordinates of normal vectors for each point in xx and yy. 

701 ny: 2D array of floats 

702 y-coordinates of normal vectors for each point in xx and yy. 

703 nz: 2D array of floats 

704 z-coordinates of normal vectors for each point in xx and yy. 

705 """ 

706 dx = xx[0,1] - xx[0,0] 

707 dy = yy[1,0] - yy[0,0] 

708 nx = np.zeros(xx.shape) 

709 nx[:,:-1] = -np.diff(zz, axis=1)/dx 

710 ny = np.zeros(xx.shape) 

711 ny[:-1,:] = -np.diff(zz, axis=0)/dy 

712 nz = np.ones(xx.shape) 

713 norm = np.sqrt(nx*nx+ny*ny+1) 

714 return nx/norm, ny/norm, nz/norm 

715 

716 

717def extract_path(data): 

718 """Convert SVG coordinates to numpy array with path coordinates. 

719 

720 Draw a fish outline in inkscape. Open the XML Editor (shift+ctrl+x) 

721 and copy the value of the data field ('d') into a variable that you 

722 pass to this function. 

723 Alternatively, try the 'inkscape:original-d' variable. 

724 

725 Parameters 

726 ---------- 

727 data: string 

728 Space separated coordinate pairs describing the outline of a fish. 

729 The coordinates are separated by commas. Coordinate pairs without a comma are ignored. 

730 

731 Returns 

732 ------- 

733 vertices: 2D array 

734 The coordinates of the outline of a fish. 

735 """ 

736 coords = data.split(' ') 

737 vertices = [] 

738 relative = False 

739 xc = yc = 0 

740 for c in coords: 

741 if ',' in c: 

742 xs, ys = c.split(',') 

743 x = float(xs) 

744 y = float(ys) 

745 if relative: 

746 xc += x 

747 yc += y 

748 else: 

749 xc = x 

750 yc = y 

751 vertices.append((xc, yc)) 

752 else: 

753 if c in 'MLC': 

754 relative = False 

755 elif c in 'mlc': 

756 relative = True 

757 vertices = np.array(vertices) 

758 return vertices 

759 

760 

761def bbox_pathes(*vertices): 

762 """Common bounding box of pathes. 

763 

764 Parameters 

765 ---------- 

766 vertices: one or more 2D arrays 

767 The coordinates of pathes 

768 (first column x-coordinates, second colum y-coordinates). 

769 

770 Returns 

771 ------- 

772 bbox: 2D array 

773 Bounding box of the pathes: [[x0, y0], [x1, y1]] 

774 """ 

775 # get bounding box of all pathes: 

776 bbox = np.zeros((2, 2)) 

777 first = True 

778 for verts in vertices: 

779 if len(verts.shape) != 2: 

780 continue 

781 vbbox = np.array([[np.min(verts[:,0]), np.min(verts[:,1])], 

782 [np.max(verts[:,0]), np.max(verts[:,1])]]) 

783 if first: 

784 bbox = vbbox 

785 first = False 

786 else: 

787 bbox[0,0] = min(bbox[0,0], vbbox[0,0]) 

788 bbox[0,1] = min(bbox[0,1], vbbox[0,1]) 

789 bbox[1,0] = max(bbox[1,0], vbbox[1,0]) 

790 bbox[1,1] = max(bbox[1,1], vbbox[1,1]) 

791 return bbox 

792 

793 

794def translate_pathes(dx, dy, *vertices): 

795 """Translate pathes in place. 

796 

797 Parameters 

798 ---------- 

799 dx: float 

800 Shift in x direction. 

801 dy: float 

802 Shift in y direction. 

803 vertices: one or more 2D arrays 

804 The coordinates of pathes to be translated 

805 (first column x-coordinates, second colum y-coordinates). 

806 """ 

807 for verts in vertices: 

808 verts[:,0] += dx 

809 verts[:,1] += dy 

810 

811 

812def center_pathes(*vertices): 

813 """Translate pathes to their common origin in place. 

814 

815 Parameters 

816 ---------- 

817 vertices: one or more 2D arrays 

818 The coordinates of pathes to be centered 

819 (first column x-coordinates, second colum y-coordinates). 

820 """ 

821 center = np.mean(bbox_pathes(*vertices), axis=1) 

822 # shift: 

823 for verts in vertices: 

824 verts[:,0] -= center[0] 

825 verts[:,1] -= center[1] 

826 

827 

828def rotate_pathes(theta, *vertices): 

829 """Rotate pathes in place. 

830 

831 Parameters 

832 ---------- 

833 theta: float 

834 Rotation angle in degrees. 

835 vertices: one or more 2D arrays 

836 The coordinates of pathes to be rotated 

837 (first column x-coordinates, second colum y-coordinates). 

838 """ 

839 theta *= np.pi/180.0 

840 # rotation matrix: 

841 c = np.cos(theta) 

842 s = np.sin(theta) 

843 rm = np.array(((c, -s), (s, c))) 

844 # rotation: 

845 for verts in vertices: 

846 verts[:,:] = np.dot(verts, rm) 

847 

848 

849def flipx_pathes(*vertices): 

850 """Flip pathes in x-direction in place. 

851 

852 Parameters 

853 ---------- 

854 vertices: one or more 2D arrays 

855 The coordinates of pathes to be flipped 

856 (first column x-coordinates, second colum y-coordinates). 

857 """ 

858 for verts in vertices: 

859 verts[:,0] = -verts[:,0] 

860 

861 

862def flipy_pathes(*vertices): 

863 """Flip pathes in y-direction in place. 

864 

865 Parameters 

866 ---------- 

867 vertices: one or more 2D arrays 

868 The coordinates of pathes to be flipped 

869 (first column x-coordinates, second colum y-coordinates). 

870 """ 

871 for verts in vertices: 

872 verts[:,1] = -verts[:,1] 

873 

874 

875def mirror_path(vertices1): 

876 """Complete path of half a fish outline by appending the mirrored path. 

877 

878 It is sufficient to draw half of a top view of a fish. Import with 

879 extract_path() and use this function to add the missing half of the 

880 outline to the path. The outline is mirrored on the x-axis. 

881 

882 Parameters 

883 ---------- 

884 vertices1: 2D array 

885 The coordinates of one half of the outline of a fish 

886 (first column x-coordinates, second colum y-coordinates). 

887 

888 Returns 

889 ------- 

890 vertices: 2D array 

891 The coordinates of the complete outline of a fish. 

892 """ 

893 vertices2 = np.array(vertices1[::-1,:]) 

894 vertices2[:,1] *= -1 

895 vertices = np.concatenate((vertices1, vertices2)) 

896 return vertices 

897 

898 

899def normalize_path(*vertices): 

900 """Normalize and shift path in place. 

901 

902 The path extent in x direction is normalized to one and its center 

903 is shifted to the origin. 

904 

905 Parameters 

906 ---------- 

907 vertices: one or more 2D arrays 

908 The coordinates of the outline of a fish 

909 (first column x-coordinates, second colum y-coordinates). 

910 """ 

911 bbox = bbox_pathes(*vertices) 

912 for verts in vertices: 

913 verts[:,1] -= np.mean(bbox[:,1]) 

914 verts[:,0] -= bbox[0,0] 

915 verts /= bbox[1,0] - bbox[0,0] 

916 verts[:,0] -= 0.5 

917 

918 

919def bend_path(path, bend, size, size_fac=1.0): 

920 """Bend and scale a path. 

921 

922 Parameters 

923 ---------- 

924 path: 2D array 

925 The coordinates of a path. 

926 bend: float 

927 Angle for bending in degrees. 

928 size: float 

929 Scale path to this size. 

930 size_fac: float 

931 Scale path even more, but keep size for calculating the bending. 

932 

933 Returns 

934 ------- 

935 path: 2D array 

936 The coordinates of the bent and scaled path. 

937 """ 

938 path = np.array(path) 

939 path *= size_fac*size 

940 if np.abs(bend) > 1.e-8: 

941 sel = path[:,0]<0.0 

942 xp = path[sel,0] # all negative x coordinates of path 

943 yp = path[sel,1] # y coordinates of all negative x coordinates of path 

944 r = -180.0*0.5*size/bend/np.pi # radius of circle on which to bend the tail 

945 beta = xp/r # angle on circle for each y coordinate 

946 R = r-yp # radius of point 

947 path[sel,0] = -np.abs(R*np.sin(beta)) # transformed x coordinates 

948 path[sel,1] = r-R*np.cos(beta) # transformed y coordinates 

949 return path 

950 

951 

952def export_path(vertices): 

953 """Print coordinates of path for import as numpy array. 

954 

955 The variable name, a leading 'np.array([' and the closing '])' 

956 are not printed. 

957 

958 Parameters 

959 ---------- 

960 vertices: 2D array 

961 The coordinates of the path 

962 (first column x-coordinates, second colum y-coordinates). 

963 """ 

964 n = 2 

965 for k, v in enumerate(vertices): 

966 if k%n == 0: 

967 print(' ', end='') 

968 print(' [%.8e, %.8e],' % (v[0], v[1]), end='') 

969 if k%n == n-1 and k < len(vertices)-1: 

970 print('') 

971 

972 

973def export_fish(name, body, *fins): 

974 """Serialize coordinates of fish outlines as a dictionary. 

975 

976 Writes a dictionary with name 'name' and keys 'body', 'fin0', 'fin1', ... 

977 holding the pathes. 

978 

979 Copy these coordinates from the console and paste them into this module. 

980 Give it a proper name and don't forget to add it to the fish_shapes dictionary 

981 to make it know to plot_fish(). 

982 

983 Parameters 

984 ---------- 

985 name: string 

986 Name of the variable. 

987 body: 2D array 

988 The coordinates of fish's body 

989 (first column x-coordinates, second colum y-coordinates). 

990 fins: zero or more 2D arrays 

991 The coordinates of the fish's fins 

992 (first column x-coordinates, second colum y-coordinates). 

993 

994 Returns 

995 ------- 

996 fish: dict 

997 A dictionary holding the pathes that can be passed directly to plot_fish(). 

998 """ 

999 print('%s = dict(body=np.array([' % name) 

1000 export_path(body) 

1001 fish = dict(body=body) 

1002 for k, f in enumerate(fins): 

1003 print(']),') 

1004 print(' fin%d=np.array([' % k) 

1005 export_path(f) 

1006 fish['fin%d' % k] = f 

1007 print(']))') 

1008 return fish 

1009 

1010 

1011def export_fish_demo(): 

1012 """Code demonstrating how to export a fish outline from SVG. 

1013 """ 

1014 # copy the path specification from an SVG object: 

1015 data = "m 84.013672,21.597656 0.0082,83.002434 0.113201,-0.0145 0.1238,-0.32544 0.06532,-0.80506 0.06836,-0.87696 0.0332,-4.298823 v -8.625 l 0.06836,-1.724609 0.06836,-1.722657 0.07032,-1.726562 0.06836,-1.726563 0.06641,-1.693359 0.03439,-1.293583 0.06912,-1.30798 0.10547,-1.724609 0.10156,-1.724609 0.10352,-1.726563 0.10352,-1.724609 0.13867,-1.72461 0.171876,-2.572265 0.13672,-1.72461 0.13672,-1.726562 0.10352,-1.724609 0.06836,-1.722657 0.103515,-2.574219 0.06836,-1.722656 0.10352,-1.728515 0.07032,-1.722657 0.06836,-1.724609 0.240234,-1.724609 0.34375,-1.72461 0.134766,-1.726562 0.10352,-1.69336 0.03516,-0.875 0.07031,-1.728515 v -0.847657 l -0.07273,-2.246267 -0.0172,-0.184338 0.15636,0.09441 0.384252,1.019739 0.748821,0.905562 1.028854,0.647532 1.356377,-0.03149 0.362644,-0.347764 -0.264138,-0.736289 -1.268298,-1.126614 -1.363988,-0.922373 -0.927443,-0.451153 -0.228986,-0.07018 -0.0015,-0.21624 0.03663,-0.660713 0.480469,-0.847657 -0.101563,-0.876953 -0.103515,-0.845703 -0.103516,-0.876953 -0.207031,-1.695313 -0.273438,-1.724609 -0.308594,-1.726562 -0.27539,-1.72461 -0.310547,-1.722656 -0.240234,-0.878906 -0.400196,-0.877344 -0.53927,-0.596268 -0.486573,-0.216683 z" 

1016 verts = extract_path(data) 

1017 # look at the path: 

1018 fig, ax = plt.subplots() 

1019 plot_pathes(ax, verts) 

1020 ax.set_aspect('equal') 

1021 plt.show() 

1022 # fix path: 

1023 center_pathes(verts) 

1024 rotate_pathes(-90.0, verts) 

1025 verts[:,1] *= 0.8 # change aspect ratio 

1026 verts = verts[1:,:] # remove first point 

1027 translate_pathes(0.0, -np.min(verts[:,1]), verts) 

1028 # mirror, normalize and export path: 

1029 verts = mirror_path(verts) 

1030 normalize_path(verts) 

1031 fish = export_fish('Alepto_top', verts) 

1032 # plot outline: 

1033 fig, ax = plt.subplots() 

1034 plot_fish(ax, fish, size=1.0/1.1, 

1035 bodykwargs=dict(lw=1, edgecolor='k', facecolor='r'), 

1036 finkwargs=dict(lw=1, edgecolor='k', facecolor='b')) 

1037 ax.set_xlim(-1.5, 1.5) 

1038 ax.set_ylim(-1, 1) 

1039 plt.show() 

1040 

1041 

1042def main(): 

1043 """Plot some fish shapes and surface normals. 

1044 """ 

1045 bodykwargs = dict(lw=1, edgecolor='k', facecolor='none') 

1046 finkwargs = dict(lw=1, edgecolor='k', facecolor='grey') 

1047 eyekwargs = dict(lw=1, edgecolor='white', facecolor='grey') 

1048 var = ['zz', 'nx', 'ny', 'nz'] 

1049 fig, ax = plt.subplots() 

1050 for k in range(4): 

1051 y = (1.5-k)*9 

1052 fish = (('Alepto_male', 'side'), (0, y), (1, 0), 20.0, 0) 

1053 xx, yy, zz = fish_surface(*fish, gamma=0.5) 

1054 nx, ny, nz = surface_normals(xx, yy, zz) 

1055 a = [zz, nx, ny, nz] 

1056 th = np.nanmax(np.abs(a[k])) 

1057 ax.contourf(xx[0,:], yy[:,0], -a[k], 20, vmin=-th, vmax=th, cmap='RdYlBu') 

1058 plot_fish(ax, *fish, bodykwargs=bodykwargs, finkwargs=finkwargs, eyekwargs=eyekwargs) 

1059 ax.text(-11, y+2, var[k]) 

1060 fish = (('Alepto_male', 'side'), (20, -9), (1, 0), 23.0, 10) 

1061 xx, yy, zz = fish_surface(*fish, gamma=0.8) 

1062 nv = surface_normals(xx, yy, zz) 

1063 ilumn = [-0.05, 0.1, 1.0] 

1064 dv = np.zeros(nv[0].shape) 

1065 for nc, ic in zip(nv, ilumn): 

1066 dv += nc*ic 

1067 #ax.contourf(xx[0,:], yy[:,0], dv, 20, cmap='gist_gray') 

1068 ax.contourf(xx[0,:], yy[:,0], dv, levels=[np.nanmin(dv), np.nanmin(dv)+0.99*(np.nanmax(dv)-np.nanmin(dv)), np.nanmax(dv)], cmap='gist_gray') 

1069 plot_fish(ax, *fish, bodykwargs=bodykwargs, finkwargs=finkwargs, eyekwargs=eyekwargs) 

1070 bodykwargs = dict(lw=1, edgecolor='k', facecolor='k') 

1071 fish = (('Alepto', 'top'), (23, 0), (2, 1), 16.0, 25) 

1072 plot_fish(ax, *fish, bodykwargs=bodykwargs, finkwargs=finkwargs) 

1073 fish = (('Eigenmannia', 'top'), (23, 8), (1, 0.3), 16.0, -15) 

1074 plot_fish(ax, *fish, bodykwargs=bodykwargs, finkwargs=finkwargs) 

1075 fish = (('Eigenmannia', 'side'), (20, 18), (1, 0), 20.0, -25) 

1076 plot_fish(ax, *fish, bodykwargs=bodykwargs, finkwargs=finkwargs, eyekwargs=eyekwargs) 

1077 plot_fishfinder(ax, (38, 13), (1, 2), 18, handle=0.2, 

1078 central_ground=True, wires=True, lw=2) 

1079 plot_fishfinder(ax, (38, -8), (1, 2), 18, central_ground=False) 

1080 ax.set_xlim(-15, 45) 

1081 ax.set_ylim(-20, 24) 

1082 ax.set_aspect('equal') 

1083 plt.show() 

1084 

1085 

1086if __name__ == '__main__': 

1087 #export_fish_demo() 

1088 main() 

1089