Module thunderfish.fishshapes
Manipulate and plot fish outlines.
Fish shapes
Fish shapes are dictionaries with the keys 'body', 'fin0', 'fin1' …, and 'eye'. The values are 2D arrays with x-y coordinates (first dimension is points, second dimension coordinates) of the respective pathes.
All fish shapes of this module are accessible via these dictionaries:
fish_shapes
: dictionary holding all electric fish shapes.fish_top_shapes
: dictionary holding electric fish shapes viewed from top.fish_side_shapes
: dictionary holding electric fish shapes viewed from the side.
These are the shapes of various fish species:
Alepto_top
: Apteronotus leptorhynchus viewed from top.Alepto_male_side
: Male Apteronotus leptorhynchus viewed from the side.Eigenmannia_top
: Eigenmannia virescens viewed from top.Eigenmannia_side
: Eigenmannia virescens viewed from the side.
Helper function for selecting a particular fish shape:
fish_shape()
: get a dictinary containing shapes of a fish.
Plotting
plot_fish()
: plot body, fins and eye of an electric fish.plot_object()
: plot circular object.plot_fishfinder()
: plot a fishfinder with electrodes and wires.plot_pathes()
: plot pathes.
Fish surface and normals from shapes
fish_surface()
: generate meshgrid of one side of the fish from shape.surface_normals()
: normal vectors on a surface.
General path manipulations
You may use these functions to extract and fine tune pathes from SVG
files in order to assemble fish shapes for this module. See
export_fish_demo()
for a use case.
extract_path()
: convert SVG coordinates to numpy array with path coordinates.bbox_pathes()
: common bounding box of pathes.translate_pathes()
: translate pathes in place.center_pathes()
: translate pathes to their common origin in place.rotate_pathes()
: rotate pathes in place.flipy_pathes()
: flip pathes in y-direction in place.flipx_pathes()
: flip pathes in x-direction in place.export_path()
: print coordinates of path for import as numpy array.mirror_path()
: complete path of half a fish outline by appending the mirrored path.normalize_path()
: normalize fish outline to unit length.bend_path()
: bend and scale a path.
Exporting fish outlines from pathes
export_fish()
: serialize coordinates of fish outlines as a dictionary.export_fish_demo()
: code demonstrating how to export fish outlines from SVG.
Expand source code
"""Manipulate and plot fish outlines.
## Fish shapes
Fish shapes are dictionaries with the keys 'body', 'fin0', 'fin1' ...,
and 'eye'. The values are 2D arrays with x-y coordinates (first
dimension is points, second dimension coordinates) of the respective
pathes.
All fish shapes of this module are accessible via these dictionaries:
- `fish_shapes`: dictionary holding all electric fish shapes.
- `fish_top_shapes`: dictionary holding electric fish shapes viewed from top.
- `fish_side_shapes`: dictionary holding electric fish shapes viewed from the side.
These are the shapes of various fish species:
- `Alepto_top`: *Apteronotus leptorhynchus* viewed from top.
- `Alepto_male_side`: Male *Apteronotus leptorhynchus* viewed from the side.
- `Eigenmannia_top`: *Eigenmannia virescens* viewed from top.
- `Eigenmannia_side`: *Eigenmannia virescens* viewed from the side.
Helper function for selecting a particular fish shape:
- `fish_shape()`: get a dictinary containing shapes of a fish.
## Plotting
- `plot_fish()`: plot body, fins and eye of an electric fish.
- `plot_object()`: plot circular object.
- `plot_fishfinder()`: plot a fishfinder with electrodes and wires.
- `plot_pathes()`: plot pathes.
## Fish surface and normals from shapes
- `fish_surface()`: generate meshgrid of one side of the fish from shape.
- `surface_normals()`: normal vectors on a surface.
## General path manipulations
You may use these functions to extract and fine tune pathes from SVG
files in order to assemble fish shapes for this module. See
`export_fish_demo()` for a use case.
- `extract_path()`: convert SVG coordinates to numpy array with path coordinates.
- `bbox_pathes()`: common bounding box of pathes.
- `translate_pathes()`: translate pathes in place.
- `center_pathes()`: translate pathes to their common origin in place.
- `rotate_pathes()`: rotate pathes in place.
- `flipy_pathes()`: flip pathes in y-direction in place.
- `flipx_pathes()`: flip pathes in x-direction in place.
- `export_path()`: print coordinates of path for import as numpy array.
- `mirror_path()`: complete path of half a fish outline by appending the mirrored path.
- `normalize_path()`: normalize fish outline to unit length.
- `bend_path()`: bend and scale a path.
## Exporting fish outlines from pathes
- `export_fish()`: serialize coordinates of fish outlines as a dictionary.
- `export_fish_demo()`: code demonstrating how to export fish outlines from SVG.
"""
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib.path import Path
from matplotlib.patches import PathPatch, Circle, Rectangle
import matplotlib.transforms as mpt
Alepto_top = dict(body=np.array([
[-5.00000000e-01, 0.00000000e+00], [-4.99802704e-01, 1.23222860e-03],
[-4.95374557e-01, 2.57983066e-03], [-4.84420392e-01, 3.29085947e-03],
[-4.72487909e-01, 4.03497963e-03], [-4.13995354e-01, 4.39637211e-03],
[-3.90529212e-01, 5.14049228e-03], [-3.67089631e-01, 5.88461244e-03],
[-3.43596916e-01, 6.65006783e-03], [-3.20104187e-01, 7.39418800e-03],
[-2.97063253e-01, 8.11708180e-03], [-2.79461930e-01, 8.49142780e-03],
[-2.61664711e-01, 9.24382081e-03], [-2.38198570e-01, 1.03918950e-02],
[-2.14732428e-01, 1.14974077e-02], [-1.91239699e-01, 1.26242555e-02],
[-1.67773558e-01, 1.37511034e-02], [-1.44307403e-01, 1.52605701e-02],
[-1.09307508e-01, 1.71314946e-02], [-8.58413531e-02, 1.86197349e-02],
[-6.23486380e-02, 2.01079753e-02], [-3.88824966e-02, 2.12348231e-02],
[-1.54429155e-02, 2.19789433e-02], [1.95835670e-02, 2.31057367e-02],
[4.30231346e-02, 2.38498569e-02], [6.65424230e-02, 2.49767047e-02],
[8.99820050e-02, 2.57421601e-02], [1.13448146e-01, 2.64862803e-02],
[1.36914287e-01, 2.91013032e-02], [1.60380442e-01, 3.28431304e-02],
[1.83873157e-01, 3.43101008e-02], [2.06914105e-01, 3.54369487e-02],
[2.18819919e-01, 3.58196764e-02], [2.42339207e-01, 3.65850229e-02],
[2.72903364e-01, 3.57933339e-02], [2.75411585e-01, 3.56061065e-02],
[2.74126982e-01, 3.73081344e-02], [2.60251756e-01, 4.14908387e-02],
[2.47930096e-01, 4.96419915e-02], [2.39119358e-01, 6.08413919e-02],
[2.39547832e-01, 7.56059835e-02], [2.44279733e-01, 7.95534778e-02],
[2.54298155e-01, 7.66782524e-02], [2.69627591e-01, 6.28724285e-02],
[2.82177993e-01, 4.80249888e-02], [2.88316671e-01, 3.79294791e-02],
[2.89271585e-01, 3.54368942e-02], [2.92213886e-01, 3.54205663e-02],
[3.01203973e-01, 3.58192954e-02], [3.12737740e-01, 4.10493520e-02],
[3.24670128e-01, 3.99438067e-02], [3.36177308e-01, 3.88170133e-02],
[3.48109696e-01, 3.76902090e-02], [3.71177217e-01, 3.54366112e-02],
[3.94643358e-01, 3.24601523e-02], [4.18136073e-01, 2.91010093e-02],
[4.41602228e-01, 2.61033022e-02], [4.65041796e-01, 2.27229002e-02],
[4.77000757e-01, 2.01078773e-02], [4.88938465e-01, 1.57516176e-02],
[4.97051671e-01, 9.88149348e-03], [5.00000000e-01, 4.58499286e-03],
[5.00000000e-01, -4.58499286e-03], [4.97051671e-01, -9.88149348e-03],
[4.88938465e-01, -1.57516176e-02], [4.77000757e-01, -2.01078773e-02],
[4.65041796e-01, -2.27229002e-02], [4.41602228e-01, -2.61033022e-02],
[4.18136073e-01, -2.91010093e-02], [3.94643358e-01, -3.24601523e-02],
[3.71177217e-01, -3.54366112e-02], [3.48109696e-01, -3.76902090e-02],
[3.36177308e-01, -3.88170133e-02], [3.24670128e-01, -3.99438067e-02],
[3.12737740e-01, -4.10493520e-02], [3.01203973e-01, -3.58192954e-02],
[2.92213886e-01, -3.54205663e-02], [2.89271585e-01, -3.54368942e-02],
[2.88316671e-01, -3.79294791e-02], [2.82177993e-01, -4.80249888e-02],
[2.69627591e-01, -6.28724285e-02], [2.54298155e-01, -7.66782524e-02],
[2.44279733e-01, -7.95534778e-02], [2.39547832e-01, -7.56059835e-02],
[2.39119358e-01, -6.08413919e-02], [2.47930096e-01, -4.96419915e-02],
[2.60251756e-01, -4.14908387e-02], [2.74126982e-01, -3.73081344e-02],
[2.75411585e-01, -3.56061065e-02], [2.72903364e-01, -3.57933339e-02],
[2.42339207e-01, -3.65850229e-02], [2.18819919e-01, -3.58196764e-02],
[2.06914105e-01, -3.54369487e-02], [1.83873157e-01, -3.43101008e-02],
[1.60380442e-01, -3.28431304e-02], [1.36914287e-01, -2.91013032e-02],
[1.13448146e-01, -2.64862803e-02], [8.99820050e-02, -2.57421601e-02],
[6.65424230e-02, -2.49767047e-02], [4.30231346e-02, -2.38498569e-02],
[1.95835670e-02, -2.31057367e-02], [-1.54429155e-02, -2.19789433e-02],
[-3.88824966e-02, -2.12348231e-02], [-6.23486380e-02, -2.01079753e-02],
[-8.58413531e-02, -1.86197349e-02], [-1.09307508e-01, -1.71314946e-02],
[-1.44307403e-01, -1.52605701e-02], [-1.67773558e-01, -1.37511034e-02],
[-1.91239699e-01, -1.26242555e-02], [-2.14732428e-01, -1.14974077e-02],
[-2.38198570e-01, -1.03918950e-02], [-2.61664711e-01, -9.24382081e-03],
[-2.79461930e-01, -8.49142780e-03], [-2.97063253e-01, -8.11708180e-03],
[-3.20104187e-01, -7.39418800e-03], [-3.43596916e-01, -6.65006783e-03],
[-3.67089631e-01, -5.88461244e-03], [-3.90529212e-01, -5.14049228e-03],
[-4.13995354e-01, -4.39637211e-03], [-4.72487909e-01, -4.03497963e-03],
[-4.84420392e-01, -3.29085947e-03], [-4.95374557e-01, -2.57983066e-03],
[-4.99802704e-01, -1.23222860e-03], [-5.00000000e-01, -0.00000000e+00],]))
"""Outline of an *Apteronotus leptorhynchus* viewed from top, modified from Krahe 2004."""
Alepto_male_side = dict(body=np.array([
[2.80332097e-01, 5.51361973e-02], [2.41127905e-01, 5.93460338e-02],
[1.91463866e-01, 6.22667811e-02], [1.37379023e-01, 6.17716006e-02],
[6.91234340e-02, 5.72953633e-02], [-1.36051588e-02, 4.74838393e-02],
[-7.55221954e-02, 3.64211032e-02], [-1.60157310e-01, 2.45651115e-02],
[-2.32035003e-01, 1.55421483e-02], [-2.99079447e-01, 9.70960800e-03],
[-3.62251791e-01, 6.27265707e-03], [-4.20527920e-01, 4.22449025e-03],
[-4.72735573e-01, 5.39606712e-03], [-4.80154179e-01, 5.86398206e-03],
[-4.92605065e-01, 1.01411700e-02], [-4.97402289e-01, 5.91543079e-03],
[-5.00000000e-01, -2.84973497e-03], [-4.97832769e-01, -1.17981289e-02],
[-4.93106950e-01, -1.43380199e-02], [-4.81164618e-01, -8.19215843e-03],
[-4.72578673e-01, -6.17623988e-03], [-4.45390092e-01, -5.96123217e-03],
[-3.74805165e-01, -9.05994885e-03], [-3.33716813e-01, -1.08317142e-02],
[-3.08099380e-01, -1.15017063e-02], [-2.82451613e-01, -1.30396176e-02],
[-2.34498580e-01, -2.21834040e-02], [-1.86892658e-01, -3.26728000e-02],
[-1.08738732e-01, -4.99024273e-02], [-3.50753879e-02, -5.94218882e-02],
[3.28767168e-02, -6.58397526e-02], [1.25319086e-01, -7.21513968e-02],
[1.99523049e-01, -7.99740378e-02], [2.37035792e-01, -8.44828747e-02],
[2.74475366e-01, -8.68964223e-02], [3.12742824e-01, -8.34038539e-02],
[3.36340505e-01, -7.82231053e-02], [3.55492327e-01, -7.21451373e-02],
[3.74670470e-01, -6.45564453e-02], [3.82920881e-01, -6.06824741e-02],
[3.84828678e-01, -5.92550189e-02], [3.86562866e-01, -5.99353293e-02],
[3.90753372e-01, -6.01589140e-02], [4.03494946e-01, -5.90960625e-02],
[4.38474761e-01, -6.13270959e-02], [4.61389913e-01, -6.47960654e-02],
[4.77010163e-01, -6.86433853e-02], [4.84437594e-01, -6.89404377e-02],
[4.90842798e-01, -6.82840746e-02], [4.94567181e-01, -6.58050993e-02],
[4.95443985e-01, -6.30972916e-02], [4.94497789e-01, -6.10849673e-02],
[4.91729699e-01, -6.00016418e-02], [4.84298546e-01, -5.78808424e-02],
[4.93112897e-01, -5.45550751e-02], [4.97742360e-01, -5.12667865e-02],
[5.00000000e-01, -4.73196051e-02], [4.99521047e-01, -4.36153642e-02],
[4.96159278e-01, -3.87756472e-02], [4.86402575e-01, -3.18513601e-02],
[4.67134496e-01, -2.06920393e-02], [4.39218141e-01, -5.92866768e-03],
[4.25010402e-01, 4.45359743e-03], [4.14788070e-01, 1.39860522e-02],
[3.93656086e-01, 2.44160739e-02], [3.75679976e-01, 2.94323719e-02],
[3.61404254e-01, 3.69002336e-02], [3.37900061e-01, 4.40458301e-02],
[3.11463577e-01, 4.97553861e-02],]),
fin0=np.array([
[3.29593304e-01, -7.95912942e-02], [3.27561074e-01, -8.48367727e-02],
[3.08709726e-01, -9.90609655e-02], [2.80934315e-01, -1.08062137e-01],
[2.58017473e-01, -1.12878542e-01], [2.35142157e-01, -1.14467112e-01],
[2.18081531e-01, -1.12354592e-01], [1.98185626e-01, -1.10721292e-01],
[1.78099090e-01, -1.13640193e-01], [1.59752865e-01, -1.18762090e-01],
[1.40752841e-01, -1.20266781e-01], [1.27904629e-01, -1.17712356e-01],
[1.19134213e-01, -1.12284346e-01], [1.09580014e-01, -1.04436264e-01],
[8.20184710e-02, -9.60992771e-02], [5.05598670e-02, -9.57289587e-02],
[2.74790284e-02, -1.04021601e-01], [3.92704920e-03, -1.08834461e-01],
[-3.12710137e-02, -1.08965162e-01], [-5.88865488e-02, -1.03820945e-01],
[-7.82549598e-02, -9.45428978e-02], [-9.94601687e-02, -8.20174601e-02],
[-1.29941640e-01, -7.01658118e-02], [-1.58259295e-01, -6.73695625e-02],
[-1.86001442e-01, -7.01570717e-02], [-2.14339679e-01, -6.79007296e-02],
[-2.38708971e-01, -5.78982409e-02], [-2.55168178e-01, -4.41230328e-02],
[-2.71293058e-01, -3.28785160e-02], [-2.88416341e-01, -2.86291802e-02],
[-3.06103856e-01, -2.82461534e-02], [-3.22345146e-01, -2.47128040e-02],
[-3.38333410e-01, -1.44124470e-02], [-3.43264223e-01, -1.03691894e-02],
[-3.08609907e-01, -1.12571357e-02], [-2.86088545e-01, -1.25633719e-02],
[-2.59977440e-01, -1.65414204e-02], [-2.16119429e-01, -2.64072955e-02],
[-1.68443229e-01, -3.68996138e-02], [-1.12717944e-01, -4.88585839e-02],
[-7.07908982e-02, -5.51259999e-02], [-1.80906639e-02, -6.16068166e-02],
[2.75299392e-02, -6.53080983e-02], [7.71390030e-02, -6.85205021e-02],
[1.21071140e-01, -7.25104674e-02], [1.78723549e-01, -7.85286909e-02],
[2.32100395e-01, -8.40268652e-02], [2.74938812e-01, -8.74456073e-02],
[3.10041908e-01, -8.43007220e-02],]),
eye=np.array([0.4, 0.0, 0.01]))
"""Outline of an *Apteronotus leptorhynchus* male viewed from the side."""
Eigenmannia_top = dict(body=np.array([
[-5.00000000e-01, 0.00000000e+00], [-4.84515329e-01, 4.41536208e-03],
[-4.76913801e-01, 5.34924846e-03], [-3.94680346e-01, 8.25734868e-03],
[-2.74106007e-01, 8.94059314e-03], [-1.35145770e-01, 1.09559947e-02],
[2.36080412e-02, 1.40941342e-02], [1.36968804e-01, 1.51550643e-02],
[2.15041020e-01, 1.96734219e-02], [2.83582110e-01, 2.36895289e-02],
[3.20834553e-01, 2.63067663e-02], [3.46646908e-01, 2.77590937e-02],
[3.68462758e-01, 2.97229886e-02], [3.62525174e-01, 3.12766064e-02],
[3.57215426e-01, 3.25163153e-02], [3.51347983e-01, 3.44809486e-02],
[3.46108357e-01, 3.83290703e-02], [3.44207747e-01, 4.53621620e-02],
[3.46387987e-01, 5.39648157e-02], [3.54784122e-01, 6.69720204e-02],
[3.67470562e-01, 8.11691502e-02], [3.80987875e-01, 9.13148567e-02],
[3.90738756e-01, 9.39276818e-02], [3.95854520e-01, 9.06728175e-02],
[3.99717109e-01, 8.49081236e-02], [3.96997843e-01, 6.54750599e-02],
[3.89101023e-01, 4.11631100e-02], [3.86289062e-01, 3.71837960e-02],
[3.94553267e-01, 3.78052325e-02], [4.03373690e-01, 3.72181278e-02],
[4.20207675e-01, 3.56696607e-02], [4.37553246e-01, 3.46018748e-02],
[4.59139056e-01, 3.15068918e-02], [4.79811600e-01, 2.68634593e-02],
[4.92810472e-01, 1.97499259e-02], [4.98594784e-01, 1.11517021e-02],
[5.00000000e-01, 5.62393850e-03], [5.00000000e-01, -5.62393850e-03],
[4.98594784e-01, -1.11517021e-02], [4.92810472e-01, -1.97499259e-02],
[4.79811600e-01, -2.68634593e-02], [4.59139056e-01, -3.15068918e-02],
[4.37553246e-01, -3.46018748e-02], [4.20207675e-01, -3.56696607e-02],
[4.03373690e-01, -3.72181278e-02], [3.94553267e-01, -3.78052325e-02],
[3.86289062e-01, -3.71837960e-02], [3.89101023e-01, -4.11631100e-02],
[3.96997843e-01, -6.54750599e-02], [3.99717109e-01, -8.49081236e-02],
[3.95854520e-01, -9.06728175e-02], [3.90738756e-01, -9.39276818e-02],
[3.80987875e-01, -9.13148567e-02], [3.67470562e-01, -8.11691502e-02],
[3.54784122e-01, -6.69720204e-02], [3.46387987e-01, -5.39648157e-02],
[3.44207747e-01, -4.53621620e-02], [3.46108357e-01, -3.83290703e-02],
[3.51347983e-01, -3.44809486e-02], [3.57215426e-01, -3.25163153e-02],
[3.62525174e-01, -3.12766064e-02], [3.68462758e-01, -2.97229886e-02],
[3.46646908e-01, -2.77590937e-02], [3.20834553e-01, -2.63067663e-02],
[2.83582110e-01, -2.36895289e-02], [2.15041020e-01, -1.96734219e-02],
[1.36968804e-01, -1.51550643e-02], [2.36080412e-02, -1.40941342e-02],
[-1.35145770e-01, -1.09559947e-02], [-2.74106007e-01, -8.94059314e-03],
[-3.94680346e-01, -8.25734868e-03], [-4.76913801e-01, -5.34924846e-03],
[-4.84515329e-01, -4.41536208e-03], [-5.00000000e-01, -0.00000000e+00],]))
"""Outline of an *Eigenmannia virescens* viewed from top."""
Eigenmannia_side = dict(body=np.array([
[7.39835590e-02, 4.57421567e-02], [1.36190672e-01, 5.20008556e-02],
[1.88575637e-01, 5.31087788e-02], [2.55693889e-01, 4.90162062e-02],
[2.91989388e-01, 4.57421567e-02], [3.30997244e-01, 4.08310609e-02],
[3.60079352e-01, 3.50312357e-02], [3.86267547e-01, 2.72057399e-02],
[4.09748495e-01, 1.88510343e-02], [4.30914243e-01, 1.02069720e-02],
[4.43253678e-01, 5.18028074e-03], [4.61959655e-01, -3.75313831e-03],
[4.82422519e-01, -1.50677197e-02], [4.93493046e-01, -2.26243878e-02],
[4.97325280e-01, -2.75603439e-02], [5.00000000e-01, -3.36538136e-02],
[4.99855343e-01, -3.81556262e-02], [4.97829629e-01, -4.26574388e-02],
[4.95229403e-01, -4.49683083e-02], [4.93207934e-01, -4.68450344e-02],
[4.90607707e-01, -4.83870578e-02], [4.92124870e-01, -5.04085273e-02],
[4.93063234e-01, -5.27193968e-02], [4.93063190e-01, -5.47905000e-02],
[4.91905677e-01, -5.65722031e-02], [4.87982621e-01, -5.83539496e-02],
[4.81889151e-01, -5.99909526e-02], [4.72187579e-01, -6.31614903e-02],
[4.57251469e-01, -6.96684443e-02], [4.42315315e-01, -7.44390846e-02],
[4.31434877e-01, -7.64563096e-02], [4.21852452e-01, -8.03091592e-02],
[4.12030260e-01, -8.11773161e-02], [3.97297016e-01, -8.61380457e-02],
[3.84200775e-01, -9.05200184e-02], [3.71589870e-01, -9.38291926e-02],
[3.58008292e-01, -9.50424035e-02], [3.33452813e-01, -9.34053571e-02],
[2.99075185e-01, -8.68572582e-02], [2.70427177e-01, -8.11276391e-02],
[2.32775500e-01, -7.31023958e-02], [2.00034918e-01, -6.81912999e-02],
[1.71386866e-01, -6.43481085e-02], [1.37488988e-01, -5.96768656e-02],
[8.87168470e-02, -5.53444400e-02], [3.71504052e-02, -5.08426274e-02],
[-8.94935470e-03, -4.47741911e-02], [-6.68009664e-02, -3.60218095e-02],
[-1.11819296e-01, -3.02864735e-02], [-1.55609841e-01, -2.46444281e-02],
[-2.01855938e-01, -1.98208625e-02], [-2.61607520e-01, -1.41655641e-02],
[-3.02124011e-01, -9.83500080e-03], [-3.47551590e-01, -8.19795443e-03],
[-3.86021794e-01, -7.21576125e-03], [-4.19580907e-01, -5.90618477e-03],
[-4.49047446e-01, -5.00584824e-03], [-4.82606558e-01, -4.29793979e-03],
[-4.93367213e-01, -3.88865654e-03], [-4.96609514e-01, -3.33497643e-03],
[-4.98599358e-01, -2.28352992e-03], [-5.00000000e-01, -4.13646830e-04],
[-4.99911798e-01, 1.42799787e-03], [-4.97749085e-01, 3.02268669e-03],
[-4.94153971e-01, 3.94706050e-03], [-4.48842818e-01, 5.27946155e-03],
[-3.90932887e-01, 5.88974836e-03], [-3.04988822e-01, 7.10408527e-03],
[-2.43785835e-01, 8.93052803e-03], [-1.87718481e-01, 1.20250559e-02],
[-1.39987578e-01, 1.55534240e-02], [-9.58582596e-02, 1.92113768e-02],
[-4.87936436e-02, 2.54739303e-02], [-1.20172913e-02, 3.11685979e-02],
[3.65545828e-02, 3.98634200e-02],]),
fin0=np.array([
[-3.23227396e-01, -8.73526322e-03], [-3.17729007e-01, -1.49720903e-02],
[-3.11901320e-01, -2.06301173e-02], [-2.94537996e-01, -2.87329729e-02],
[-2.73702014e-01, -3.62471102e-02], [-2.48814582e-01, -4.42901541e-02],
[-2.26392044e-01, -4.89203820e-02], [-2.11413629e-01, -4.97652813e-02],
[-1.97592770e-01, -4.71608105e-02], [-1.88292360e-01, -4.37113973e-02],
[-1.77575020e-01, -4.26201918e-02], [-1.63230314e-01, -4.13425351e-02],
[-1.45633053e-01, -4.58128611e-02], [-1.32102997e-01, -5.21132245e-02],
[-1.22627830e-01, -5.98022925e-02], [-1.16274541e-01, -6.51393895e-02],
[-1.01226326e-01, -6.99292162e-02], [-8.87826127e-02, -7.09420732e-02],
[-7.63388990e-02, -7.02186163e-02], [-6.41845810e-02, -6.63566715e-02],
[-4.99997329e-02, -6.35107453e-02], [-3.86044383e-02, -6.71556184e-02],
[-2.83003535e-02, -7.56835222e-02], [-1.41203129e-02, -8.28817968e-02],
[1.21728460e-03, -8.66205668e-02], [1.22140543e-02, -8.75385740e-02],
[2.16240177e-02, -8.43285373e-02], [3.27836777e-02, -8.13081568e-02],
[3.98554860e-02, -8.02952999e-02], [4.86770343e-02, -7.96350762e-02],
[5.81904230e-02, -8.20450399e-02], [6.47198980e-02, -8.65937577e-02],
[7.29857310e-02, -9.36024194e-02], [8.47509570e-02, -9.91141438e-02],
[1.00477612e-01, -1.02776515e-01], [1.28258936e-01, -1.02826321e-01],
[1.45605097e-01, -1.02460349e-01], [1.59342462e-01, -9.97657918e-02],
[1.76140399e-01, -9.72111283e-02], [1.89366052e-01, -9.61800377e-02],
[2.03938918e-01, -9.84587276e-02], [2.14786136e-01, -1.02170949e-01],
[2.24046592e-01, -1.08953357e-01], [2.34464605e-01, -1.14112491e-01],
[2.47925953e-01, -1.18114112e-01], [2.65013334e-01, -1.19108779e-01],
[2.83520819e-01, -1.15835465e-01], [2.98329467e-01, -1.08650574e-01],
[3.15014321e-01, -1.04499489e-01], [3.28805304e-01, -1.04273408e-01],
[3.39387031e-01, -1.06211982e-01], [3.52278630e-01, -1.03431974e-01],
[3.61896180e-01, -1.00567165e-01], [3.67032403e-01, -9.80662488e-02],
[3.71589870e-01, -9.38289761e-02], [3.58008292e-01, -9.50421869e-02],
[3.33452813e-01, -9.34051405e-02], [3.06441808e-01, -8.84940880e-02],
[2.35043362e-01, -7.35981699e-02], [1.65011316e-01, -6.31802003e-02],
[1.25654422e-01, -5.85499724e-02], [9.49792270e-02, -5.56561016e-02],
[4.05741354e-02, -5.11056947e-02], [-1.24746680e-03, -4.58268936e-02],
[-5.20302500e-02, -3.81131387e-02], [-1.01805114e-01, -3.16101258e-02],
[-1.51874267e-01, -2.50855445e-02], [-2.01943420e-01, -2.02074944e-02],
[-2.61607516e-01, -1.41653476e-02], [-3.02124016e-01, -9.83478430e-03],
[-3.12840355e-01, -9.28491550e-03],]),
eye=np.array([0.46, -0.03, 0.005]))
"""Outline of an *Eigenmannia virescens* viewed from the side."""
fish_shapes = dict(Alepto_top=Alepto_top,
Alepto_male_side=Alepto_male_side,
Eigenmannia_top=Eigenmannia_top,
Eigenmannia_side=Eigenmannia_side)
"""Dictionary holding all electric fish shapes."""
fish_top_shapes = dict(Alepto=Alepto_top,
Eigenmannia=Eigenmannia_top)
"""Dictionary holding electric fish shapes viewed from top."""
fish_side_shapes = dict(Alepto_male=Alepto_male_side,
Eigenmannia=Eigenmannia_side)
"""Dictionary holding electric fish shapes viewed from the side."""
def fish_shape(fish):
"""Get a dictinary containing shapes of a fish.
Parameters
----------
fish: string or tuple or dict
Specifies a fish to show:
- any of the strings defining a shape contained in the `fish_shapes` dictionary,
- a tuple with the name of the fish as the first element and 'top' or 'side' as the second element,
- a dictionary with at least a 'body' key holding pathes to be drawn.
Returns
-------
fish: dict
Dictionary with at least a 'body' key holding pathes to be drawn.
"""
if not isinstance(fish, dict):
if isinstance(fish, (tuple, list)):
if fish[1] == 'top':
fish = fish_top_shapes[fish[0]]
else:
fish = fish_side_shapes[fish[0]]
else:
fish = fish_shapes[fish]
return fish
def plot_fish(ax, fish, pos=(0, 0), direction=(1, 0), size=20.0, bend=0, scaley=1,
bodykwargs={}, finkwargs={}, eyekwargs=None):
"""Plot body, fins and eye of an electric fish.
Parameters
----------
ax: matplotlib axes
Axes where to draw the fish.
fish: string or tuple or dict
Specifies a fish to show:
- any of the strings defining a shape contained in the `fish_shapes` dictionary,
- a tuple with the name of the fish as the first element and 'top' or 'side' as the second element,
- a dictionary with at least a 'body' key holding pathes to be drawn.
pos: tuple of floats
Coordinates of the fish's position (its center).
direction: tuple of floats
Coordinates of a vector defining the orientation of the fish.
size: float
Size of the fish.
bend: float
Bending angle of the fish's tail in degree.
scaley: float
Scale factor applied in y direction after bending and rotation to
compensate for differently scaled axes.
bodykwargs: dict
Key-word arguments for PathPatch used to draw the fish's body.
finkwargs: dict
Key-word arguments for PathPatch used to draw the fish's fins.
Returns
-------
bpatch: matplotlib.patches.PathPatch
The fish's body. Can be used for set_clip_path().
Example
-------
```
fig, ax = plt.subplots()
bodykwargs=dict(lw=1, edgecolor='k', facecolor='k')
finkwargs=dict(lw=1, edgecolor='k', facecolor='grey')
fish = (('Eigenmannia', 'side'), (0, 0), (1, 0), 20.0, -25)
plot_fish(ax, *fish, bodykwargs=bodykwargs, finkwargs=finkwargs)
ax.set_xlim(-15, 15)
ax.set_ylim(-10, 10)
plt.show()
```
"""
fish = fish_shape(fish)
bpatch = None
size_fac = 1.1
bbox = bbox_pathes(*fish.values())
trans = mpl.transforms.Affine2D()
angle = np.arctan2(direction[1], direction[0])
trans.rotate(angle)
#trans.scale(dxu/dyu, dyu/dxu) # what is the right scaling????
trans.scale(1, scaley)
trans.translate(*pos)
for part, verts in fish.items():
if part == 'eye':
if eyekwargs is not None:
verts = np.array(verts)*size*size_fac
verts[:2] = trans.transform_point(verts[:2])
if not 'zorder' in eyekwargs:
eyekwargs['zorder'] = 20
ax.add_patch(Circle(verts[:2], verts[2], **eyekwargs))
continue
verts = bend_path(verts, bend, size, size_fac)
codes = np.zeros(len(verts))
codes[:] = Path.LINETO
codes[0] = Path.MOVETO
codes[-1] = Path.CLOSEPOLY
path = Path(verts, codes)
#pixelx = np.abs(np.diff(ax.get_window_extent().get_points()[:,0]))[0]
#pixely = np.abs(np.diff(ax.get_window_extent().get_points()[:,1]))[0]
#xmin, xmax = ax.get_xlim()
#ymin, ymax = ax.get_ylim()
#dxu = np.abs(xmax - xmin)/pixelx
#dyu = np.abs(ymax - ymin)/pixely
path = path.transformed(trans)
kwargs = bodykwargs if part == 'body' else finkwargs
if not 'zorder' in kwargs:
kwargs['zorder'] = 0 if part == 'body' else 10
patch = PathPatch(path, **kwargs)
if part == 'body':
bpatch = patch
ax.add_patch(patch)
return bpatch
def plot_object(ax, pos=(0, 0), radius=1.0, **kwargs):
"""Plot circular object.
Parameters
----------
ax: matplotlib axes
Axes where to draw the object.
pos: tuple of floats
Coordinates of the objects's position (its center).
radius: float
Radius of the cirular object.
kwargs: key word arguments
Arguments for Circle used to draw the obkect.
"""
ax.add_patch(Circle(pos, radius, **kwargs))
def plot_fishfinder(ax, pos, direction, length, handle=0.05,
central_ground=False, wires=False,
rodkwargs=dict(edgecolor='none', facecolor='gray'),
poskwargs=dict(edgecolor='none', facecolor='red'),
negkwargs=dict(edgecolor='none', facecolor='blue'),
gndkwargs=dict(edgecolor='none', facecolor='black'),
lw=1, zorder=50):
"""Plot a fishfinder with electrodes and wires.
Parameters
----------
ax: matplotlib axes
Axes where to draw the fishfinder.
pos: tuple of floats
Coordinates of the fishfinder's position (its center).
direction: tuple of floats
Coordinates defining the orientation of the fishfinder.
length: float
Length of the fishfinder (center of positive electrode
minus center of negative electrode).
handle: float
Length of handle (rod beyond the negative electrode)
as a fraction of the `length` of fishfinder.
central_ground: bool
Add a central ground electrode.
wires: bool, 'postop' or 'negtop'
Draw wires for each electrode.
- True or 'postop': draw wire of positive electrode on top.
- 'negtop': draw wire of negative electrode on top.
Return the coordinates of the endpoints of the wires.
rodkwargs: dict
Key-word arguments for Rectangle used to draw the rod.
poskwargs: dict
Key-word arguments for Rectangle used to draw the positive electrode.
negkwargs: dict
Key-word arguments for Rectangle used to draw the negative electrode.
gndkwargs: dict
Key-word arguments for Rectangle used to draw the ground electrode.
lw: float
Width of the lines used for drawing the wires.
zorder: int
zorder for the fishfinder.
Returns
-------
negpos: tuple of floats
Coordinates of center of negative electrode.
pospos: tuple of floats
Coordinates of center of positive electrode.
negwirepos: tuple of floats
If `wire`, the end of the wire of the negative electrode.
poswirepos: tuple of floats
If `wire`, the end of the wire of the positive electrode.
gndwirepos: tuple of floats
If `central_ground` and `wire`, the end of the wire of
the ground electrode.
"""
width = 0.07*length
transform = mpt.Affine2D().rotate(np.arctan2(direction[1], direction[0])).translate(*pos)
ax.add_patch(Rectangle((-(0.5+handle)*length, -0.5*width),
(1+handle+0.05)*length, width,
transform=transform + ax.transData,
zorder=zorder, **rodkwargs))
ax.add_patch(Rectangle((0.5*length-0.4*width, -0.6*width),
0.8*width, 1.2*width,
transform=transform + ax.transData,
zorder=zorder+2, **poskwargs))
ax.add_patch(Rectangle((-0.5*length-0.4*width, -0.6*width),
0.8*width, 1.2*width,
transform=transform + ax.transData,
zorder=zorder+2, **negkwargs))
nodes = [(-0.5*length, 0), (0.5*length, 0)]
if central_ground:
ax.add_patch(Rectangle((-0.4*width, -0.6*width),
0.8*width, 1.2*width,
transform=transform + ax.transData,
zorder=zorder+2, **gndkwargs))
if wires:
offs = 0.03*width*lw
if wires == 'negtop':
offs *= -1
if central_ground:
offs *= 2
color = negkwargs.get('facecolor')
ax.plot((-0.5*length, -(0.5+handle)*length), (-offs, -offs),
color=color, lw=lw, solid_capstyle='butt',
transform=transform + ax.transData, zorder=zorder+1)
color = poskwargs.get('facecolor')
ax.plot((0.5*length, -(0.5+handle)*length), (offs, offs),
color=color, lw=lw, solid_capstyle='butt', transform=transform +
ax.transData, zorder=zorder+1)
nodes.extend(((-(0.5+handle)*length, -offs), (-(0.5+handle)*length, offs)))
if central_ground:
color = gndkwargs.get('facecolor')
ax.plot((0, -(0.5+handle)*length), (0, 0),
color=color, lw=lw, solid_capstyle='butt',
transform=transform + ax.transData, zorder=zorder+1)
nodes.append((-(0.5+handle)*length, 0))
nodes = transform.transform(nodes)
return nodes
def plot_pathes(ax, *vertices, **kwargs):
"""Plot pathes.
Parameters
----------
ax: matplotlib axes
Axes where to draw the path.
vertices: one or more 2D arrays
The coordinates of pathes to be plotted
(first column x-coordinates, second colum y-coordinates).
kwargs: key word arguments
Arguments for PathPatch used to draw the path.
"""
for verts in vertices:
codes = np.zeros(len(verts))
codes[:] = Path.LINETO
codes[0] = Path.MOVETO
codes[-1] = Path.CLOSEPOLY
path = Path(verts, codes)
ax.add_patch(PathPatch(path, **kwargs))
bbox = bbox_pathes(*vertices)
center = np.mean(bbox, axis=0)
bbox -= center
bbox *= 1.2
bbox += center
ax.set_xlim(*bbox[:,0])
ax.set_ylim(*bbox[:,1])
def fish_surface(fish, pos=(0, 0), direction=(1, 0), size=20.0, bend=0,
gamma=1.0):
"""Generate meshgrid of one side of the fish from shape.
Parameters
----------
fish: string or tuple or dict
Specifies a fish to show:
- any of the strings defining a shape contained in the `fish_shapes` dictionary,
- a tuple with the name of the fish and 'top' or 'side',
- a dictionary with at least a 'body' key holding pathes to be drawn.
pos: tuple of floats
Coordinates of the fish's position (its center).
direction: tuple of floats
Coordinates of a vector defining the orientation of the fish.
size: float
Size of the fish.
bend: float
Bending angle of the fish's tail in degree.
gamma: float
Gamma distortion of the ellipse. The ellipse equation is raised
to the power of gamma before its smaller diameter is scaled up
from one to the actual value.
Returns
-------
xx: 2D array of floats
x-coordinates in direction of body axis.
yy: 2D array of floats
y-coordinates in direction upwards from body axis.
zz: 2D array of floats
z-coordinates of fish surface, outside of fish NaN.
"""
if direction[1] != 0:
raise ValueError('rotation not supported by fish_surface yet.')
fish = fish_shape(fish)
bbox = bbox_pathes(*fish.values())
size_fac = -1.05*0.5/bbox[0,0]
path = bend_path(fish['body'], bend, size, size_fac)
# split in top and bottom half:
minxi = np.argmin(path[:,0])
maxxi = np.argmax(path[:,0])
i0 = min(minxi, maxxi)
i1 = max(minxi, maxxi)
path0 = path[i0:i1,:]
path1 = np.vstack((path[i0::-1,:], path[:i1:-1,:]))
if np.mean(path0[:,1]) < np.mean(path1[:,1]):
path0, path1 = path1, path0
# make sure x coordinates are monotonically increasing:
pm = np.maximum.accumulate(path0[:,0])
path0 = np.delete(path0, np.where(path0[:,0] < pm)[0], axis=0)
pm = np.maximum.accumulate(path1[:,0])
path1 = np.delete(path1, np.where(path1[:,0] < pm)[0], axis=0)
# rotate: XXX
# translate:
minx = path[minxi,0] + pos[0]
maxx = path[maxxi,0] + pos[0]
path0 += pos[:2]
path1 += pos[:2]
# interpolate:
n = 5*max(len(path0), len(path1))
#n = 200
x = np.linspace(minx, maxx, n)
upperpath = np.zeros((len(x), 2))
upperpath[:,0] = x
upperpath[:,1] = np.interp(x, path0[:,0], path0[:,1])
lowerpath = np.zeros((len(x), 2))
lowerpath[:,0] = x
lowerpath[:,1] = np.interp(x, path1[:,0], path1[:,1])
# ellipse origin and semi axes:
midline = np.array(upperpath)
midline[:,1] = np.mean(np.vstack((upperpath[:,1], lowerpath[:,1])), axis=0)
diamy = upperpath[:,1] - midline[:,1]
diamz = 0.3*diamy # take it from the top view!
# apply ellipse:
y = np.linspace(np.min(midline[:,1]-diamy), np.max(midline[:,1]+diamy), n//2)
xx, yy = np.meshgrid(x ,y)
zz = diamz * (np.sqrt(1.0 - ((yy-midline[:,1])/diamy)**2))**gamma
return xx, yy, zz
def surface_normals(xx, yy, zz):
"""Normal vectors on a surface.
Compute surface normals on a surface as returned by `fish_surface()`.
Parameters
----------
xx: 2D array of floats
Mesh grid of x coordinates.
yy: 2D array of floats
Mesh grid of y coordinates.
zz: 2D array of floats
z-coordinates of surface on the xx and yy coordinates.
Returns
-------
nx: 2D array of floats
x-coordinates of normal vectors for each point in xx and yy.
ny: 2D array of floats
y-coordinates of normal vectors for each point in xx and yy.
nz: 2D array of floats
z-coordinates of normal vectors for each point in xx and yy.
"""
dx = xx[0,1] - xx[0,0]
dy = yy[1,0] - yy[0,0]
nx = np.zeros(xx.shape)
nx[:,:-1] = -np.diff(zz, axis=1)/dx
ny = np.zeros(xx.shape)
ny[:-1,:] = -np.diff(zz, axis=0)/dy
nz = np.ones(xx.shape)
norm = np.sqrt(nx*nx+ny*ny+1)
return nx/norm, ny/norm, nz/norm
def extract_path(data):
"""Convert SVG coordinates to numpy array with path coordinates.
Draw a fish outline in inkscape. Open the XML Editor (shift+ctrl+x)
and copy the value of the data field ('d') into a variable that you
pass to this function.
Alternatively, try the 'inkscape:original-d' variable.
Parameters
----------
data: string
Space separated coordinate pairs describing the outline of a fish.
The coordinates are separated by commas. Coordinate pairs without a comma are ignored.
Returns
-------
vertices: 2D array
The coordinates of the outline of a fish.
"""
coords = data.split(' ')
vertices = []
relative = False
xc = yc = 0
for c in coords:
if ',' in c:
xs, ys = c.split(',')
x = float(xs)
y = float(ys)
if relative:
xc += x
yc += y
else:
xc = x
yc = y
vertices.append((xc, yc))
else:
if c in 'MLC':
relative = False
elif c in 'mlc':
relative = True
vertices = np.array(vertices)
return vertices
def bbox_pathes(*vertices):
"""Common bounding box of pathes.
Parameters
----------
vertices: one or more 2D arrays
The coordinates of pathes
(first column x-coordinates, second colum y-coordinates).
Returns
-------
bbox: 2D array
Bounding box of the pathes: [[x0, y0], [x1, y1]]
"""
# get bounding box of all pathes:
bbox = np.zeros((2, 2))
first = True
for verts in vertices:
if len(verts.shape) != 2:
continue
vbbox = np.array([[np.min(verts[:,0]), np.min(verts[:,1])],
[np.max(verts[:,0]), np.max(verts[:,1])]])
if first:
bbox = vbbox
first = False
else:
bbox[0,0] = min(bbox[0,0], vbbox[0,0])
bbox[0,1] = min(bbox[0,1], vbbox[0,1])
bbox[1,0] = max(bbox[1,0], vbbox[1,0])
bbox[1,1] = max(bbox[1,1], vbbox[1,1])
return bbox
def translate_pathes(dx, dy, *vertices):
"""Translate pathes in place.
Parameters
----------
dx: float
Shift in x direction.
dy: float
Shift in y direction.
vertices: one or more 2D arrays
The coordinates of pathes to be translated
(first column x-coordinates, second colum y-coordinates).
"""
for verts in vertices:
verts[:,0] += dx
verts[:,1] += dy
def center_pathes(*vertices):
"""Translate pathes to their common origin in place.
Parameters
----------
vertices: one or more 2D arrays
The coordinates of pathes to be centered
(first column x-coordinates, second colum y-coordinates).
"""
center = np.mean(bbox_pathes(*vertices), axis=1)
# shift:
for verts in vertices:
verts[:,0] -= center[0]
verts[:,1] -= center[1]
def rotate_pathes(theta, *vertices):
"""Rotate pathes in place.
Parameters
----------
theta: float
Rotation angle in degrees.
vertices: one or more 2D arrays
The coordinates of pathes to be rotated
(first column x-coordinates, second colum y-coordinates).
"""
theta *= np.pi/180.0
# rotation matrix:
c = np.cos(theta)
s = np.sin(theta)
rm = np.array(((c, -s), (s, c)))
# rotation:
for verts in vertices:
verts[:,:] = np.dot(verts, rm)
def flipx_pathes(*vertices):
"""Flip pathes in x-direction in place.
Parameters
----------
vertices: one or more 2D arrays
The coordinates of pathes to be flipped
(first column x-coordinates, second colum y-coordinates).
"""
for verts in vertices:
verts[:,0] = -verts[:,0]
def flipy_pathes(*vertices):
"""Flip pathes in y-direction in place.
Parameters
----------
vertices: one or more 2D arrays
The coordinates of pathes to be flipped
(first column x-coordinates, second colum y-coordinates).
"""
for verts in vertices:
verts[:,1] = -verts[:,1]
def mirror_path(vertices1):
"""Complete path of half a fish outline by appending the mirrored path.
It is sufficient to draw half of a top view of a fish. Import with
extract_path() and use this function to add the missing half of the
outline to the path. The outline is mirrored on the x-axis.
Parameters
----------
vertices1: 2D array
The coordinates of one half of the outline of a fish
(first column x-coordinates, second colum y-coordinates).
Returns
-------
vertices: 2D array
The coordinates of the complete outline of a fish.
"""
vertices2 = np.array(vertices1[::-1,:])
vertices2[:,1] *= -1
vertices = np.concatenate((vertices1, vertices2))
return vertices
def normalize_path(*vertices):
"""Normalize and shift path in place.
The path extent in x direction is normalized to one and its center
is shifted to the origin.
Parameters
----------
vertices: one or more 2D arrays
The coordinates of the outline of a fish
(first column x-coordinates, second colum y-coordinates).
"""
bbox = bbox_pathes(*vertices)
for verts in vertices:
verts[:,1] -= np.mean(bbox[:,1])
verts[:,0] -= bbox[0,0]
verts /= bbox[1,0] - bbox[0,0]
verts[:,0] -= 0.5
def bend_path(path, bend, size, size_fac=1.0):
"""Bend and scale a path.
Parameters
----------
path: 2D array
The coordinates of a path.
bend: float
Angle for bending in degrees.
size: float
Scale path to this size.
size_fac: float
Scale path even more, but keep size for calculating the bending.
Returns
-------
path: 2D array
The coordinates of the bent and scaled path.
"""
path = np.array(path)
path *= size_fac*size
if np.abs(bend) > 1.e-8:
sel = path[:,0]<0.0
xp = path[sel,0] # all negative x coordinates of path
yp = path[sel,1] # y coordinates of all negative x coordinates of path
r = -180.0*0.5*size/bend/np.pi # radius of circle on which to bend the tail
beta = xp/r # angle on circle for each y coordinate
R = r-yp # radius of point
path[sel,0] = -np.abs(R*np.sin(beta)) # transformed x coordinates
path[sel,1] = r-R*np.cos(beta) # transformed y coordinates
return path
def export_path(vertices):
"""Print coordinates of path for import as numpy array.
The variable name, a leading 'np.array([' and the closing '])'
are not printed.
Parameters
----------
vertices: 2D array
The coordinates of the path
(first column x-coordinates, second colum y-coordinates).
"""
n = 2
for k, v in enumerate(vertices):
if k%n == 0:
print(' ', end='')
print(' [%.8e, %.8e],' % (v[0], v[1]), end='')
if k%n == n-1 and k < len(vertices)-1:
print('')
def export_fish(name, body, *fins):
"""Serialize coordinates of fish outlines as a dictionary.
Writes a dictionary with name 'name' and keys 'body', 'fin0', 'fin1', ...
holding the pathes.
Copy these coordinates from the console and paste them into this module.
Give it a proper name and don't forget to add it to the fish_shapes dictionary
to make it know to plot_fish().
Parameters
----------
name: string
Name of the variable.
body: 2D array
The coordinates of fish's body
(first column x-coordinates, second colum y-coordinates).
fins: zero or more 2D arrays
The coordinates of the fish's fins
(first column x-coordinates, second colum y-coordinates).
Returns
-------
fish: dict
A dictionary holding the pathes that can be passed directly to plot_fish().
"""
print('%s = dict(body=np.array([' % name)
export_path(body)
fish = dict(body=body)
for k, f in enumerate(fins):
print(']),')
print(' fin%d=np.array([' % k)
export_path(f)
fish['fin%d' % k] = f
print(']))')
return fish
def export_fish_demo():
"""Code demonstrating how to export a fish outline from SVG.
"""
# copy the path specification from an SVG object:
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"
verts = extract_path(data)
# look at the path:
fig, ax = plt.subplots()
plot_pathes(ax, verts)
ax.set_aspect('equal')
plt.show()
# fix path:
center_pathes(verts)
rotate_pathes(-90.0, verts)
verts[:,1] *= 0.8 # change aspect ratio
verts = verts[1:,:] # remove first point
translate_pathes(0.0, -np.min(verts[:,1]), verts)
# mirror, normalize and export path:
verts = mirror_path(verts)
normalize_path(verts)
fish = export_fish('Alepto_top', verts)
# plot outline:
fig, ax = plt.subplots()
plot_fish(ax, fish, size=1.0/1.1,
bodykwargs=dict(lw=1, edgecolor='k', facecolor='r'),
finkwargs=dict(lw=1, edgecolor='k', facecolor='b'))
ax.set_xlim(-1.5, 1.5)
ax.set_ylim(-1, 1)
plt.show()
def main():
"""Plot some fish shapes and surface normals.
"""
bodykwargs = dict(lw=1, edgecolor='k', facecolor='none')
finkwargs = dict(lw=1, edgecolor='k', facecolor='grey')
eyekwargs = dict(lw=1, edgecolor='white', facecolor='grey')
var = ['zz', 'nx', 'ny', 'nz']
fig, ax = plt.subplots()
for k in range(4):
y = (1.5-k)*9
fish = (('Alepto_male', 'side'), (0, y), (1, 0), 20.0, 0)
xx, yy, zz = fish_surface(*fish, gamma=0.5)
nx, ny, nz = surface_normals(xx, yy, zz)
a = [zz, nx, ny, nz]
th = np.nanmax(np.abs(a[k]))
ax.contourf(xx[0,:], yy[:,0], -a[k], 20, vmin=-th, vmax=th, cmap='RdYlBu')
plot_fish(ax, *fish, bodykwargs=bodykwargs, finkwargs=finkwargs, eyekwargs=eyekwargs)
ax.text(-11, y+2, var[k])
fish = (('Alepto_male', 'side'), (20, -9), (1, 0), 23.0, 10)
xx, yy, zz = fish_surface(*fish, gamma=0.8)
nv = surface_normals(xx, yy, zz)
ilumn = [-0.05, 0.1, 1.0]
dv = np.zeros(nv[0].shape)
for nc, ic in zip(nv, ilumn):
dv += nc*ic
#ax.contourf(xx[0,:], yy[:,0], dv, 20, cmap='gist_gray')
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')
plot_fish(ax, *fish, bodykwargs=bodykwargs, finkwargs=finkwargs, eyekwargs=eyekwargs)
bodykwargs = dict(lw=1, edgecolor='k', facecolor='k')
fish = (('Alepto', 'top'), (23, 0), (2, 1), 16.0, 25)
plot_fish(ax, *fish, bodykwargs=bodykwargs, finkwargs=finkwargs)
fish = (('Eigenmannia', 'top'), (23, 8), (1, 0.3), 16.0, -15)
plot_fish(ax, *fish, bodykwargs=bodykwargs, finkwargs=finkwargs)
fish = (('Eigenmannia', 'side'), (20, 18), (1, 0), 20.0, -25)
plot_fish(ax, *fish, bodykwargs=bodykwargs, finkwargs=finkwargs, eyekwargs=eyekwargs)
plot_fishfinder(ax, (38, 13), (1, 2), 18, handle=0.2,
central_ground=True, wires=True, lw=2)
plot_fishfinder(ax, (38, -8), (1, 2), 18, central_ground=False)
ax.set_xlim(-15, 45)
ax.set_ylim(-20, 24)
ax.set_aspect('equal')
plt.show()
if __name__ == '__main__':
#export_fish_demo()
main()
Global variables
var Alepto_top
-
Outline of an Apteronotus leptorhynchus viewed from top, modified from Krahe 2004.
var Alepto_male_side
-
Outline of an Apteronotus leptorhynchus male viewed from the side.
var Eigenmannia_top
-
Outline of an Eigenmannia virescens viewed from top.
var Eigenmannia_side
-
Outline of an Eigenmannia virescens viewed from the side.
var fish_shapes
-
Dictionary holding all electric fish shapes.
var fish_top_shapes
-
Dictionary holding electric fish shapes viewed from top.
var fish_side_shapes
-
Dictionary holding electric fish shapes viewed from the side.
Functions
def fish_shape(fish)
-
Get a dictinary containing shapes of a fish.
Parameters
fish
:string
ortuple
ordict
- Specifies a fish to show:
- any of the strings defining a shape contained in the
fish_shapes
dictionary, - a tuple with the name of the fish as the first element and 'top' or 'side' as the second element, - a dictionary with at least a 'body' key holding pathes to be drawn.
Returns
fish
:dict
- Dictionary with at least a 'body' key holding pathes to be drawn.
Expand source code
def fish_shape(fish): """Get a dictinary containing shapes of a fish. Parameters ---------- fish: string or tuple or dict Specifies a fish to show: - any of the strings defining a shape contained in the `fish_shapes` dictionary, - a tuple with the name of the fish as the first element and 'top' or 'side' as the second element, - a dictionary with at least a 'body' key holding pathes to be drawn. Returns ------- fish: dict Dictionary with at least a 'body' key holding pathes to be drawn. """ if not isinstance(fish, dict): if isinstance(fish, (tuple, list)): if fish[1] == 'top': fish = fish_top_shapes[fish[0]] else: fish = fish_side_shapes[fish[0]] else: fish = fish_shapes[fish] return fish
def plot_fish(ax, fish, pos=(0, 0), direction=(1, 0), size=20.0, bend=0, scaley=1, bodykwargs={}, finkwargs={}, eyekwargs=None)
-
Plot body, fins and eye of an electric fish.
Parameters
ax
:matplotlib axes
- Axes where to draw the fish.
fish
:string
ortuple
ordict
- Specifies a fish to show:
- any of the strings defining a shape contained in the
fish_shapes
dictionary, - a tuple with the name of the fish as the first element and 'top' or 'side' as the second element, - a dictionary with at least a 'body' key holding pathes to be drawn. pos
:tuple
offloats
- Coordinates of the fish's position (its center).
direction
:tuple
offloats
- Coordinates of a vector defining the orientation of the fish.
size
:float
- Size of the fish.
bend
:float
- Bending angle of the fish's tail in degree.
scaley
:float
- Scale factor applied in y direction after bending and rotation to compensate for differently scaled axes.
bodykwargs
:dict
- Key-word arguments for PathPatch used to draw the fish's body.
finkwargs
:dict
- Key-word arguments for PathPatch used to draw the fish's fins.
Returns
bpatch
:matplotlib.patches.PathPatch
- The fish's body. Can be used for set_clip_path().
Example
fig, ax = plt.subplots() bodykwargs=dict(lw=1, edgecolor='k', facecolor='k') finkwargs=dict(lw=1, edgecolor='k', facecolor='grey') fish = (('Eigenmannia', 'side'), (0, 0), (1, 0), 20.0, -25) plot_fish(ax, *fish, bodykwargs=bodykwargs, finkwargs=finkwargs) ax.set_xlim(-15, 15) ax.set_ylim(-10, 10) plt.show()
Expand source code
def plot_fish(ax, fish, pos=(0, 0), direction=(1, 0), size=20.0, bend=0, scaley=1, bodykwargs={}, finkwargs={}, eyekwargs=None): """Plot body, fins and eye of an electric fish. Parameters ---------- ax: matplotlib axes Axes where to draw the fish. fish: string or tuple or dict Specifies a fish to show: - any of the strings defining a shape contained in the `fish_shapes` dictionary, - a tuple with the name of the fish as the first element and 'top' or 'side' as the second element, - a dictionary with at least a 'body' key holding pathes to be drawn. pos: tuple of floats Coordinates of the fish's position (its center). direction: tuple of floats Coordinates of a vector defining the orientation of the fish. size: float Size of the fish. bend: float Bending angle of the fish's tail in degree. scaley: float Scale factor applied in y direction after bending and rotation to compensate for differently scaled axes. bodykwargs: dict Key-word arguments for PathPatch used to draw the fish's body. finkwargs: dict Key-word arguments for PathPatch used to draw the fish's fins. Returns ------- bpatch: matplotlib.patches.PathPatch The fish's body. Can be used for set_clip_path(). Example ------- ``` fig, ax = plt.subplots() bodykwargs=dict(lw=1, edgecolor='k', facecolor='k') finkwargs=dict(lw=1, edgecolor='k', facecolor='grey') fish = (('Eigenmannia', 'side'), (0, 0), (1, 0), 20.0, -25) plot_fish(ax, *fish, bodykwargs=bodykwargs, finkwargs=finkwargs) ax.set_xlim(-15, 15) ax.set_ylim(-10, 10) plt.show() ``` """ fish = fish_shape(fish) bpatch = None size_fac = 1.1 bbox = bbox_pathes(*fish.values()) trans = mpl.transforms.Affine2D() angle = np.arctan2(direction[1], direction[0]) trans.rotate(angle) #trans.scale(dxu/dyu, dyu/dxu) # what is the right scaling???? trans.scale(1, scaley) trans.translate(*pos) for part, verts in fish.items(): if part == 'eye': if eyekwargs is not None: verts = np.array(verts)*size*size_fac verts[:2] = trans.transform_point(verts[:2]) if not 'zorder' in eyekwargs: eyekwargs['zorder'] = 20 ax.add_patch(Circle(verts[:2], verts[2], **eyekwargs)) continue verts = bend_path(verts, bend, size, size_fac) codes = np.zeros(len(verts)) codes[:] = Path.LINETO codes[0] = Path.MOVETO codes[-1] = Path.CLOSEPOLY path = Path(verts, codes) #pixelx = np.abs(np.diff(ax.get_window_extent().get_points()[:,0]))[0] #pixely = np.abs(np.diff(ax.get_window_extent().get_points()[:,1]))[0] #xmin, xmax = ax.get_xlim() #ymin, ymax = ax.get_ylim() #dxu = np.abs(xmax - xmin)/pixelx #dyu = np.abs(ymax - ymin)/pixely path = path.transformed(trans) kwargs = bodykwargs if part == 'body' else finkwargs if not 'zorder' in kwargs: kwargs['zorder'] = 0 if part == 'body' else 10 patch = PathPatch(path, **kwargs) if part == 'body': bpatch = patch ax.add_patch(patch) return bpatch
def plot_object(ax, pos=(0, 0), radius=1.0, **kwargs)
-
Plot circular object.
Parameters
ax
:matplotlib axes
- Axes where to draw the object.
pos
:tuple
offloats
- Coordinates of the objects's position (its center).
radius
:float
- Radius of the cirular object.
kwargs
:key word arguments
- Arguments for Circle used to draw the obkect.
Expand source code
def plot_object(ax, pos=(0, 0), radius=1.0, **kwargs): """Plot circular object. Parameters ---------- ax: matplotlib axes Axes where to draw the object. pos: tuple of floats Coordinates of the objects's position (its center). radius: float Radius of the cirular object. kwargs: key word arguments Arguments for Circle used to draw the obkect. """ ax.add_patch(Circle(pos, radius, **kwargs))
def plot_fishfinder(ax, pos, direction, length, handle=0.05, central_ground=False, wires=False, rodkwargs={'edgecolor': 'none', 'facecolor': 'gray'}, poskwargs={'edgecolor': 'none', 'facecolor': 'red'}, negkwargs={'edgecolor': 'none', 'facecolor': 'blue'}, gndkwargs={'edgecolor': 'none', 'facecolor': 'black'}, lw=1, zorder=50)
-
Plot a fishfinder with electrodes and wires.
Parameters
ax
:matplotlib axes
- Axes where to draw the fishfinder.
pos
:tuple
offloats
- Coordinates of the fishfinder's position (its center).
direction
:tuple
offloats
- Coordinates defining the orientation of the fishfinder.
length
:float
- Length of the fishfinder (center of positive electrode minus center of negative electrode).
handle
:float
- Length of handle (rod beyond the negative electrode)
as a fraction of the
length
of fishfinder. central_ground
:bool
- Add a central ground electrode.
wires
:bool, 'postop'
or'negtop'
- Draw wires for each electrode. - True or 'postop': draw wire of positive electrode on top. - 'negtop': draw wire of negative electrode on top. Return the coordinates of the endpoints of the wires.
rodkwargs
:dict
- Key-word arguments for Rectangle used to draw the rod.
poskwargs
:dict
- Key-word arguments for Rectangle used to draw the positive electrode.
negkwargs
:dict
- Key-word arguments for Rectangle used to draw the negative electrode.
gndkwargs
:dict
- Key-word arguments for Rectangle used to draw the ground electrode.
lw
:float
- Width of the lines used for drawing the wires.
zorder
:int
- zorder for the fishfinder.
Returns
negpos
:tuple
offloats
- Coordinates of center of negative electrode.
pospos
:tuple
offloats
- Coordinates of center of positive electrode.
negwirepos
:tuple
offloats
- If
wire
, the end of the wire of the negative electrode. poswirepos
:tuple
offloats
- If
wire
, the end of the wire of the positive electrode. gndwirepos
:tuple
offloats
- If
central_ground
andwire
, the end of the wire of the ground electrode.
Expand source code
def plot_fishfinder(ax, pos, direction, length, handle=0.05, central_ground=False, wires=False, rodkwargs=dict(edgecolor='none', facecolor='gray'), poskwargs=dict(edgecolor='none', facecolor='red'), negkwargs=dict(edgecolor='none', facecolor='blue'), gndkwargs=dict(edgecolor='none', facecolor='black'), lw=1, zorder=50): """Plot a fishfinder with electrodes and wires. Parameters ---------- ax: matplotlib axes Axes where to draw the fishfinder. pos: tuple of floats Coordinates of the fishfinder's position (its center). direction: tuple of floats Coordinates defining the orientation of the fishfinder. length: float Length of the fishfinder (center of positive electrode minus center of negative electrode). handle: float Length of handle (rod beyond the negative electrode) as a fraction of the `length` of fishfinder. central_ground: bool Add a central ground electrode. wires: bool, 'postop' or 'negtop' Draw wires for each electrode. - True or 'postop': draw wire of positive electrode on top. - 'negtop': draw wire of negative electrode on top. Return the coordinates of the endpoints of the wires. rodkwargs: dict Key-word arguments for Rectangle used to draw the rod. poskwargs: dict Key-word arguments for Rectangle used to draw the positive electrode. negkwargs: dict Key-word arguments for Rectangle used to draw the negative electrode. gndkwargs: dict Key-word arguments for Rectangle used to draw the ground electrode. lw: float Width of the lines used for drawing the wires. zorder: int zorder for the fishfinder. Returns ------- negpos: tuple of floats Coordinates of center of negative electrode. pospos: tuple of floats Coordinates of center of positive electrode. negwirepos: tuple of floats If `wire`, the end of the wire of the negative electrode. poswirepos: tuple of floats If `wire`, the end of the wire of the positive electrode. gndwirepos: tuple of floats If `central_ground` and `wire`, the end of the wire of the ground electrode. """ width = 0.07*length transform = mpt.Affine2D().rotate(np.arctan2(direction[1], direction[0])).translate(*pos) ax.add_patch(Rectangle((-(0.5+handle)*length, -0.5*width), (1+handle+0.05)*length, width, transform=transform + ax.transData, zorder=zorder, **rodkwargs)) ax.add_patch(Rectangle((0.5*length-0.4*width, -0.6*width), 0.8*width, 1.2*width, transform=transform + ax.transData, zorder=zorder+2, **poskwargs)) ax.add_patch(Rectangle((-0.5*length-0.4*width, -0.6*width), 0.8*width, 1.2*width, transform=transform + ax.transData, zorder=zorder+2, **negkwargs)) nodes = [(-0.5*length, 0), (0.5*length, 0)] if central_ground: ax.add_patch(Rectangle((-0.4*width, -0.6*width), 0.8*width, 1.2*width, transform=transform + ax.transData, zorder=zorder+2, **gndkwargs)) if wires: offs = 0.03*width*lw if wires == 'negtop': offs *= -1 if central_ground: offs *= 2 color = negkwargs.get('facecolor') ax.plot((-0.5*length, -(0.5+handle)*length), (-offs, -offs), color=color, lw=lw, solid_capstyle='butt', transform=transform + ax.transData, zorder=zorder+1) color = poskwargs.get('facecolor') ax.plot((0.5*length, -(0.5+handle)*length), (offs, offs), color=color, lw=lw, solid_capstyle='butt', transform=transform + ax.transData, zorder=zorder+1) nodes.extend(((-(0.5+handle)*length, -offs), (-(0.5+handle)*length, offs))) if central_ground: color = gndkwargs.get('facecolor') ax.plot((0, -(0.5+handle)*length), (0, 0), color=color, lw=lw, solid_capstyle='butt', transform=transform + ax.transData, zorder=zorder+1) nodes.append((-(0.5+handle)*length, 0)) nodes = transform.transform(nodes) return nodes
def plot_pathes(ax, *vertices, **kwargs)
-
Plot pathes.
Parameters
ax
:matplotlib axes
- Axes where to draw the path.
vertices
:one
ormore 2D arrays
- The coordinates of pathes to be plotted (first column x-coordinates, second colum y-coordinates).
kwargs
:key word arguments
- Arguments for PathPatch used to draw the path.
Expand source code
def plot_pathes(ax, *vertices, **kwargs): """Plot pathes. Parameters ---------- ax: matplotlib axes Axes where to draw the path. vertices: one or more 2D arrays The coordinates of pathes to be plotted (first column x-coordinates, second colum y-coordinates). kwargs: key word arguments Arguments for PathPatch used to draw the path. """ for verts in vertices: codes = np.zeros(len(verts)) codes[:] = Path.LINETO codes[0] = Path.MOVETO codes[-1] = Path.CLOSEPOLY path = Path(verts, codes) ax.add_patch(PathPatch(path, **kwargs)) bbox = bbox_pathes(*vertices) center = np.mean(bbox, axis=0) bbox -= center bbox *= 1.2 bbox += center ax.set_xlim(*bbox[:,0]) ax.set_ylim(*bbox[:,1])
def fish_surface(fish, pos=(0, 0), direction=(1, 0), size=20.0, bend=0, gamma=1.0)
-
Generate meshgrid of one side of the fish from shape.
Parameters
fish
:string
ortuple
ordict
- Specifies a fish to show:
- any of the strings defining a shape contained in the
fish_shapes
dictionary, - a tuple with the name of the fish and 'top' or 'side', - a dictionary with at least a 'body' key holding pathes to be drawn. pos
:tuple
offloats
- Coordinates of the fish's position (its center).
direction
:tuple
offloats
- Coordinates of a vector defining the orientation of the fish.
size
:float
- Size of the fish.
bend
:float
- Bending angle of the fish's tail in degree.
gamma
:float
- Gamma distortion of the ellipse. The ellipse equation is raised to the power of gamma before its smaller diameter is scaled up from one to the actual value.
Returns
xx
:2D array
offloats
- x-coordinates in direction of body axis.
yy
:2D array
offloats
- y-coordinates in direction upwards from body axis.
zz
:2D array
offloats
- z-coordinates of fish surface, outside of fish NaN.
Expand source code
def fish_surface(fish, pos=(0, 0), direction=(1, 0), size=20.0, bend=0, gamma=1.0): """Generate meshgrid of one side of the fish from shape. Parameters ---------- fish: string or tuple or dict Specifies a fish to show: - any of the strings defining a shape contained in the `fish_shapes` dictionary, - a tuple with the name of the fish and 'top' or 'side', - a dictionary with at least a 'body' key holding pathes to be drawn. pos: tuple of floats Coordinates of the fish's position (its center). direction: tuple of floats Coordinates of a vector defining the orientation of the fish. size: float Size of the fish. bend: float Bending angle of the fish's tail in degree. gamma: float Gamma distortion of the ellipse. The ellipse equation is raised to the power of gamma before its smaller diameter is scaled up from one to the actual value. Returns ------- xx: 2D array of floats x-coordinates in direction of body axis. yy: 2D array of floats y-coordinates in direction upwards from body axis. zz: 2D array of floats z-coordinates of fish surface, outside of fish NaN. """ if direction[1] != 0: raise ValueError('rotation not supported by fish_surface yet.') fish = fish_shape(fish) bbox = bbox_pathes(*fish.values()) size_fac = -1.05*0.5/bbox[0,0] path = bend_path(fish['body'], bend, size, size_fac) # split in top and bottom half: minxi = np.argmin(path[:,0]) maxxi = np.argmax(path[:,0]) i0 = min(minxi, maxxi) i1 = max(minxi, maxxi) path0 = path[i0:i1,:] path1 = np.vstack((path[i0::-1,:], path[:i1:-1,:])) if np.mean(path0[:,1]) < np.mean(path1[:,1]): path0, path1 = path1, path0 # make sure x coordinates are monotonically increasing: pm = np.maximum.accumulate(path0[:,0]) path0 = np.delete(path0, np.where(path0[:,0] < pm)[0], axis=0) pm = np.maximum.accumulate(path1[:,0]) path1 = np.delete(path1, np.where(path1[:,0] < pm)[0], axis=0) # rotate: XXX # translate: minx = path[minxi,0] + pos[0] maxx = path[maxxi,0] + pos[0] path0 += pos[:2] path1 += pos[:2] # interpolate: n = 5*max(len(path0), len(path1)) #n = 200 x = np.linspace(minx, maxx, n) upperpath = np.zeros((len(x), 2)) upperpath[:,0] = x upperpath[:,1] = np.interp(x, path0[:,0], path0[:,1]) lowerpath = np.zeros((len(x), 2)) lowerpath[:,0] = x lowerpath[:,1] = np.interp(x, path1[:,0], path1[:,1]) # ellipse origin and semi axes: midline = np.array(upperpath) midline[:,1] = np.mean(np.vstack((upperpath[:,1], lowerpath[:,1])), axis=0) diamy = upperpath[:,1] - midline[:,1] diamz = 0.3*diamy # take it from the top view! # apply ellipse: y = np.linspace(np.min(midline[:,1]-diamy), np.max(midline[:,1]+diamy), n//2) xx, yy = np.meshgrid(x ,y) zz = diamz * (np.sqrt(1.0 - ((yy-midline[:,1])/diamy)**2))**gamma return xx, yy, zz
def surface_normals(xx, yy, zz)
-
Normal vectors on a surface.
Compute surface normals on a surface as returned by
fish_surface()
.Parameters
xx
:2D array
offloats
- Mesh grid of x coordinates.
yy
:2D array
offloats
- Mesh grid of y coordinates.
zz
:2D array
offloats
- z-coordinates of surface on the xx and yy coordinates.
Returns
nx
:2D array
offloats
- x-coordinates of normal vectors for each point in xx and yy.
ny
:2D array
offloats
- y-coordinates of normal vectors for each point in xx and yy.
nz
:2D array
offloats
- z-coordinates of normal vectors for each point in xx and yy.
Expand source code
def surface_normals(xx, yy, zz): """Normal vectors on a surface. Compute surface normals on a surface as returned by `fish_surface()`. Parameters ---------- xx: 2D array of floats Mesh grid of x coordinates. yy: 2D array of floats Mesh grid of y coordinates. zz: 2D array of floats z-coordinates of surface on the xx and yy coordinates. Returns ------- nx: 2D array of floats x-coordinates of normal vectors for each point in xx and yy. ny: 2D array of floats y-coordinates of normal vectors for each point in xx and yy. nz: 2D array of floats z-coordinates of normal vectors for each point in xx and yy. """ dx = xx[0,1] - xx[0,0] dy = yy[1,0] - yy[0,0] nx = np.zeros(xx.shape) nx[:,:-1] = -np.diff(zz, axis=1)/dx ny = np.zeros(xx.shape) ny[:-1,:] = -np.diff(zz, axis=0)/dy nz = np.ones(xx.shape) norm = np.sqrt(nx*nx+ny*ny+1) return nx/norm, ny/norm, nz/norm
def extract_path(data)
-
Convert SVG coordinates to numpy array with path coordinates.
Draw a fish outline in inkscape. Open the XML Editor (shift+ctrl+x) and copy the value of the data field ('d') into a variable that you pass to this function. Alternatively, try the 'inkscape:original-d' variable.
Parameters
data
:string
- Space separated coordinate pairs describing the outline of a fish. The coordinates are separated by commas. Coordinate pairs without a comma are ignored.
Returns
vertices
:2D array
- The coordinates of the outline of a fish.
Expand source code
def extract_path(data): """Convert SVG coordinates to numpy array with path coordinates. Draw a fish outline in inkscape. Open the XML Editor (shift+ctrl+x) and copy the value of the data field ('d') into a variable that you pass to this function. Alternatively, try the 'inkscape:original-d' variable. Parameters ---------- data: string Space separated coordinate pairs describing the outline of a fish. The coordinates are separated by commas. Coordinate pairs without a comma are ignored. Returns ------- vertices: 2D array The coordinates of the outline of a fish. """ coords = data.split(' ') vertices = [] relative = False xc = yc = 0 for c in coords: if ',' in c: xs, ys = c.split(',') x = float(xs) y = float(ys) if relative: xc += x yc += y else: xc = x yc = y vertices.append((xc, yc)) else: if c in 'MLC': relative = False elif c in 'mlc': relative = True vertices = np.array(vertices) return vertices
def bbox_pathes(*vertices)
-
Common bounding box of pathes.
Parameters
vertices
:one
ormore 2D arrays
- The coordinates of pathes (first column x-coordinates, second colum y-coordinates).
Returns
bbox
:2D array
- Bounding box of the pathes: [[x0, y0], [x1, y1]]
Expand source code
def bbox_pathes(*vertices): """Common bounding box of pathes. Parameters ---------- vertices: one or more 2D arrays The coordinates of pathes (first column x-coordinates, second colum y-coordinates). Returns ------- bbox: 2D array Bounding box of the pathes: [[x0, y0], [x1, y1]] """ # get bounding box of all pathes: bbox = np.zeros((2, 2)) first = True for verts in vertices: if len(verts.shape) != 2: continue vbbox = np.array([[np.min(verts[:,0]), np.min(verts[:,1])], [np.max(verts[:,0]), np.max(verts[:,1])]]) if first: bbox = vbbox first = False else: bbox[0,0] = min(bbox[0,0], vbbox[0,0]) bbox[0,1] = min(bbox[0,1], vbbox[0,1]) bbox[1,0] = max(bbox[1,0], vbbox[1,0]) bbox[1,1] = max(bbox[1,1], vbbox[1,1]) return bbox
def translate_pathes(dx, dy, *vertices)
-
Translate pathes in place.
Parameters
dx
:float
- Shift in x direction.
dy
:float
- Shift in y direction.
vertices
:one
ormore 2D arrays
- The coordinates of pathes to be translated (first column x-coordinates, second colum y-coordinates).
Expand source code
def translate_pathes(dx, dy, *vertices): """Translate pathes in place. Parameters ---------- dx: float Shift in x direction. dy: float Shift in y direction. vertices: one or more 2D arrays The coordinates of pathes to be translated (first column x-coordinates, second colum y-coordinates). """ for verts in vertices: verts[:,0] += dx verts[:,1] += dy
def center_pathes(*vertices)
-
Translate pathes to their common origin in place.
Parameters
vertices
:one
ormore 2D arrays
- The coordinates of pathes to be centered (first column x-coordinates, second colum y-coordinates).
Expand source code
def center_pathes(*vertices): """Translate pathes to their common origin in place. Parameters ---------- vertices: one or more 2D arrays The coordinates of pathes to be centered (first column x-coordinates, second colum y-coordinates). """ center = np.mean(bbox_pathes(*vertices), axis=1) # shift: for verts in vertices: verts[:,0] -= center[0] verts[:,1] -= center[1]
def rotate_pathes(theta, *vertices)
-
Rotate pathes in place.
Parameters
theta
:float
- Rotation angle in degrees.
vertices
:one
ormore 2D arrays
- The coordinates of pathes to be rotated (first column x-coordinates, second colum y-coordinates).
Expand source code
def rotate_pathes(theta, *vertices): """Rotate pathes in place. Parameters ---------- theta: float Rotation angle in degrees. vertices: one or more 2D arrays The coordinates of pathes to be rotated (first column x-coordinates, second colum y-coordinates). """ theta *= np.pi/180.0 # rotation matrix: c = np.cos(theta) s = np.sin(theta) rm = np.array(((c, -s), (s, c))) # rotation: for verts in vertices: verts[:,:] = np.dot(verts, rm)
def flipx_pathes(*vertices)
-
Flip pathes in x-direction in place.
Parameters
vertices
:one
ormore 2D arrays
- The coordinates of pathes to be flipped (first column x-coordinates, second colum y-coordinates).
Expand source code
def flipx_pathes(*vertices): """Flip pathes in x-direction in place. Parameters ---------- vertices: one or more 2D arrays The coordinates of pathes to be flipped (first column x-coordinates, second colum y-coordinates). """ for verts in vertices: verts[:,0] = -verts[:,0]
def flipy_pathes(*vertices)
-
Flip pathes in y-direction in place.
Parameters
vertices
:one
ormore 2D arrays
- The coordinates of pathes to be flipped (first column x-coordinates, second colum y-coordinates).
Expand source code
def flipy_pathes(*vertices): """Flip pathes in y-direction in place. Parameters ---------- vertices: one or more 2D arrays The coordinates of pathes to be flipped (first column x-coordinates, second colum y-coordinates). """ for verts in vertices: verts[:,1] = -verts[:,1]
def mirror_path(vertices1)
-
Complete path of half a fish outline by appending the mirrored path.
It is sufficient to draw half of a top view of a fish. Import with extract_path() and use this function to add the missing half of the outline to the path. The outline is mirrored on the x-axis.
Parameters
vertices1
:2D array
- The coordinates of one half of the outline of a fish (first column x-coordinates, second colum y-coordinates).
Returns
vertices
:2D array
- The coordinates of the complete outline of a fish.
Expand source code
def mirror_path(vertices1): """Complete path of half a fish outline by appending the mirrored path. It is sufficient to draw half of a top view of a fish. Import with extract_path() and use this function to add the missing half of the outline to the path. The outline is mirrored on the x-axis. Parameters ---------- vertices1: 2D array The coordinates of one half of the outline of a fish (first column x-coordinates, second colum y-coordinates). Returns ------- vertices: 2D array The coordinates of the complete outline of a fish. """ vertices2 = np.array(vertices1[::-1,:]) vertices2[:,1] *= -1 vertices = np.concatenate((vertices1, vertices2)) return vertices
def normalize_path(*vertices)
-
Normalize and shift path in place.
The path extent in x direction is normalized to one and its center is shifted to the origin.
Parameters
vertices
:one
ormore 2D arrays
- The coordinates of the outline of a fish (first column x-coordinates, second colum y-coordinates).
Expand source code
def normalize_path(*vertices): """Normalize and shift path in place. The path extent in x direction is normalized to one and its center is shifted to the origin. Parameters ---------- vertices: one or more 2D arrays The coordinates of the outline of a fish (first column x-coordinates, second colum y-coordinates). """ bbox = bbox_pathes(*vertices) for verts in vertices: verts[:,1] -= np.mean(bbox[:,1]) verts[:,0] -= bbox[0,0] verts /= bbox[1,0] - bbox[0,0] verts[:,0] -= 0.5
def bend_path(path, bend, size, size_fac=1.0)
-
Bend and scale a path.
Parameters
path
:2D array
- The coordinates of a path.
bend
:float
- Angle for bending in degrees.
size
:float
- Scale path to this size.
size_fac
:float
- Scale path even more, but keep size for calculating the bending.
Returns
path
:2D array
- The coordinates of the bent and scaled path.
Expand source code
def bend_path(path, bend, size, size_fac=1.0): """Bend and scale a path. Parameters ---------- path: 2D array The coordinates of a path. bend: float Angle for bending in degrees. size: float Scale path to this size. size_fac: float Scale path even more, but keep size for calculating the bending. Returns ------- path: 2D array The coordinates of the bent and scaled path. """ path = np.array(path) path *= size_fac*size if np.abs(bend) > 1.e-8: sel = path[:,0]<0.0 xp = path[sel,0] # all negative x coordinates of path yp = path[sel,1] # y coordinates of all negative x coordinates of path r = -180.0*0.5*size/bend/np.pi # radius of circle on which to bend the tail beta = xp/r # angle on circle for each y coordinate R = r-yp # radius of point path[sel,0] = -np.abs(R*np.sin(beta)) # transformed x coordinates path[sel,1] = r-R*np.cos(beta) # transformed y coordinates return path
def export_path(vertices)
-
Print coordinates of path for import as numpy array.
The variable name, a leading 'np.array([' and the closing '])' are not printed.
Parameters
vertices
:2D array
- The coordinates of the path (first column x-coordinates, second colum y-coordinates).
Expand source code
def export_path(vertices): """Print coordinates of path for import as numpy array. The variable name, a leading 'np.array([' and the closing '])' are not printed. Parameters ---------- vertices: 2D array The coordinates of the path (first column x-coordinates, second colum y-coordinates). """ n = 2 for k, v in enumerate(vertices): if k%n == 0: print(' ', end='') print(' [%.8e, %.8e],' % (v[0], v[1]), end='') if k%n == n-1 and k < len(vertices)-1: print('')
def export_fish(name, body, *fins)
-
Serialize coordinates of fish outlines as a dictionary.
Writes a dictionary with name 'name' and keys 'body', 'fin0', 'fin1', … holding the pathes.
Copy these coordinates from the console and paste them into this module. Give it a proper name and don't forget to add it to the fish_shapes dictionary to make it know to plot_fish().
Parameters
name
:string
- Name of the variable.
body
:2D array
- The coordinates of fish's body (first column x-coordinates, second colum y-coordinates).
fins
:zero
ormore 2D arrays
- The coordinates of the fish's fins (first column x-coordinates, second colum y-coordinates).
Returns
fish
:dict
- A dictionary holding the pathes that can be passed directly to plot_fish().
Expand source code
def export_fish(name, body, *fins): """Serialize coordinates of fish outlines as a dictionary. Writes a dictionary with name 'name' and keys 'body', 'fin0', 'fin1', ... holding the pathes. Copy these coordinates from the console and paste them into this module. Give it a proper name and don't forget to add it to the fish_shapes dictionary to make it know to plot_fish(). Parameters ---------- name: string Name of the variable. body: 2D array The coordinates of fish's body (first column x-coordinates, second colum y-coordinates). fins: zero or more 2D arrays The coordinates of the fish's fins (first column x-coordinates, second colum y-coordinates). Returns ------- fish: dict A dictionary holding the pathes that can be passed directly to plot_fish(). """ print('%s = dict(body=np.array([' % name) export_path(body) fish = dict(body=body) for k, f in enumerate(fins): print(']),') print(' fin%d=np.array([' % k) export_path(f) fish['fin%d' % k] = f print(']))') return fish
def export_fish_demo()
-
Code demonstrating how to export a fish outline from SVG.
Expand source code
def export_fish_demo(): """Code demonstrating how to export a fish outline from SVG. """ # copy the path specification from an SVG object: 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" verts = extract_path(data) # look at the path: fig, ax = plt.subplots() plot_pathes(ax, verts) ax.set_aspect('equal') plt.show() # fix path: center_pathes(verts) rotate_pathes(-90.0, verts) verts[:,1] *= 0.8 # change aspect ratio verts = verts[1:,:] # remove first point translate_pathes(0.0, -np.min(verts[:,1]), verts) # mirror, normalize and export path: verts = mirror_path(verts) normalize_path(verts) fish = export_fish('Alepto_top', verts) # plot outline: fig, ax = plt.subplots() plot_fish(ax, fish, size=1.0/1.1, bodykwargs=dict(lw=1, edgecolor='k', facecolor='r'), finkwargs=dict(lw=1, edgecolor='k', facecolor='b')) ax.set_xlim(-1.5, 1.5) ax.set_ylim(-1, 1) plt.show()
def main()
-
Plot some fish shapes and surface normals.
Expand source code
def main(): """Plot some fish shapes and surface normals. """ bodykwargs = dict(lw=1, edgecolor='k', facecolor='none') finkwargs = dict(lw=1, edgecolor='k', facecolor='grey') eyekwargs = dict(lw=1, edgecolor='white', facecolor='grey') var = ['zz', 'nx', 'ny', 'nz'] fig, ax = plt.subplots() for k in range(4): y = (1.5-k)*9 fish = (('Alepto_male', 'side'), (0, y), (1, 0), 20.0, 0) xx, yy, zz = fish_surface(*fish, gamma=0.5) nx, ny, nz = surface_normals(xx, yy, zz) a = [zz, nx, ny, nz] th = np.nanmax(np.abs(a[k])) ax.contourf(xx[0,:], yy[:,0], -a[k], 20, vmin=-th, vmax=th, cmap='RdYlBu') plot_fish(ax, *fish, bodykwargs=bodykwargs, finkwargs=finkwargs, eyekwargs=eyekwargs) ax.text(-11, y+2, var[k]) fish = (('Alepto_male', 'side'), (20, -9), (1, 0), 23.0, 10) xx, yy, zz = fish_surface(*fish, gamma=0.8) nv = surface_normals(xx, yy, zz) ilumn = [-0.05, 0.1, 1.0] dv = np.zeros(nv[0].shape) for nc, ic in zip(nv, ilumn): dv += nc*ic #ax.contourf(xx[0,:], yy[:,0], dv, 20, cmap='gist_gray') 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') plot_fish(ax, *fish, bodykwargs=bodykwargs, finkwargs=finkwargs, eyekwargs=eyekwargs) bodykwargs = dict(lw=1, edgecolor='k', facecolor='k') fish = (('Alepto', 'top'), (23, 0), (2, 1), 16.0, 25) plot_fish(ax, *fish, bodykwargs=bodykwargs, finkwargs=finkwargs) fish = (('Eigenmannia', 'top'), (23, 8), (1, 0.3), 16.0, -15) plot_fish(ax, *fish, bodykwargs=bodykwargs, finkwargs=finkwargs) fish = (('Eigenmannia', 'side'), (20, 18), (1, 0), 20.0, -25) plot_fish(ax, *fish, bodykwargs=bodykwargs, finkwargs=finkwargs, eyekwargs=eyekwargs) plot_fishfinder(ax, (38, 13), (1, 2), 18, handle=0.2, central_ground=True, wires=True, lw=2) plot_fishfinder(ax, (38, -8), (1, 2), 18, central_ground=False) ax.set_xlim(-15, 45) ax.set_ylim(-20, 24) ax.set_aspect('equal') plt.show()