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
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-29 16:21 +0000
1"""Manipulate and plot fish outlines.
3## Fish shapes
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.
10All fish shapes of this module are accessible via these dictionaries:
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.
16These are the shapes of various fish species:
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.
23Helper function for selecting a particular fish shape:
25- `fish_shape()`: get a dictinary containing shapes of a fish.
27## Plotting
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.
34## Fish surface and normals from shapes
36- `fish_surface()`: generate meshgrid of one side of the fish from shape.
37- `surface_normals()`: normal vectors on a surface.
39## General path manipulations
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.
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.
57## Exporting fish outlines from pathes
59- `export_fish()`: serialize coordinates of fish outlines as a dictionary.
60- `export_fish_demo()`: code demonstrating how to export fish outlines from SVG.
62"""
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
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."""
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."""
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."""
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."""
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."""
327fish_top_shapes = dict(Alepto=Alepto_top,
328 Eigenmannia=Eigenmannia_top)
329"""Dictionary holding electric fish shapes viewed from top."""
331fish_side_shapes = dict(Alepto_male=Alepto_male_side,
332 Eigenmannia=Eigenmannia_side)
333"""Dictionary holding electric fish shapes viewed from the side."""
336def fish_shape(fish):
337 """Get a dictinary containing shapes of a fish.
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.
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
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.
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.
392 Returns
393 -------
394 bpatch: matplotlib.patches.PathPatch
395 The fish's body. Can be used for set_clip_path().
397 Example
398 -------
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
453def plot_object(ax, pos=(0, 0), radius=1.0, **kwargs):
454 """Plot circular object.
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))
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.
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.
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)
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
573def plot_pathes(ax, *vertices, **kwargs):
574 """Plot pathes.
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])
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.
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.
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
683def surface_normals(xx, yy, zz):
684 """Normal vectors on a surface.
686 Compute surface normals on a surface as returned by `fish_surface()`.
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.
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
717def extract_path(data):
718 """Convert SVG coordinates to numpy array with path coordinates.
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.
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.
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
761def bbox_pathes(*vertices):
762 """Common bounding box of pathes.
764 Parameters
765 ----------
766 vertices: one or more 2D arrays
767 The coordinates of pathes
768 (first column x-coordinates, second colum y-coordinates).
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
794def translate_pathes(dx, dy, *vertices):
795 """Translate pathes in place.
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
812def center_pathes(*vertices):
813 """Translate pathes to their common origin in place.
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]
828def rotate_pathes(theta, *vertices):
829 """Rotate pathes in place.
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)
849def flipx_pathes(*vertices):
850 """Flip pathes in x-direction in place.
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]
862def flipy_pathes(*vertices):
863 """Flip pathes in y-direction in place.
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]
875def mirror_path(vertices1):
876 """Complete path of half a fish outline by appending the mirrored path.
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.
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).
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
899def normalize_path(*vertices):
900 """Normalize and shift path in place.
902 The path extent in x direction is normalized to one and its center
903 is shifted to the origin.
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
919def bend_path(path, bend, size, size_fac=1.0):
920 """Bend and scale a path.
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.
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
952def export_path(vertices):
953 """Print coordinates of path for import as numpy array.
955 The variable name, a leading 'np.array([' and the closing '])'
956 are not printed.
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('')
973def export_fish(name, body, *fins):
974 """Serialize coordinates of fish outlines as a dictionary.
976 Writes a dictionary with name 'name' and keys 'body', 'fin0', 'fin1', ...
977 holding the pathes.
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().
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).
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
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()
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()
1086if __name__ == '__main__':
1087 #export_fish_demo()
1088 main()