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.Gpetersii_top
: Ganthonemus petersii viewed from top.Gpetersii_side
: Ganthonemus petersii 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 pathes to numpy arrays 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 file.
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 Gpetersii_top
-
Outline of an Gnathonemus petersii viewed from top.
var Gpetersii_side
-
Outline of an Gnathonemus petersii 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)
-
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
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.
def plot_fish(ax,
fish,
pos=(0, 0),
direction=(1, 0),
size=20.0,
bend=0,
scaley=1,
bodykwargs={},
finkwargs={},
eyekwargs=None)-
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
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()
def plot_object(ax, pos=(0, 0), radius=1.0, **kwargs)
-
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))
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.
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)-
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
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.
def plot_pathes(ax, *vertices, **kwargs)
-
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])
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.
def fish_surface(fish, pos=(0, 0), direction=(1, 0), size=20.0, bend=0, gamma=1.0)
-
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
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.
def surface_normals(xx, yy, zz)
-
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
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.
def extract_path(svgfile, path_index, npoints)
-
Expand source code
def extract_path(svgfile, path_index, npoints): """Convert SVG pathes to numpy array with path coordinates. Draw a fish outline in inkscape and save it to a svg file. Install the svgpathtools package (https://github.com/mathandy/svgpathtools): ``` pip install svgpathtools ``` Parameters ---------- svgfile: string Name of the svg file containing the path. path_index: int Index selecting the path. npoints: int Number of points to spread out on the path. Returns ------- vertices: 2D array The coordinates of the outline of a fish. npoints rows of x and y coordinates. """ from svgpathtools import svg2paths2 paths, attributes, svg_attributes = svg2paths2(svgfile) path = paths[path_index] vertices = np.zeros((npoints, 2)) for i in range(npoints): p = path.point(i/npoints) vertices[i, 0] = p.real vertices[i, 1] = p.imag return vertices
Convert SVG pathes to numpy array with path coordinates.
Draw a fish outline in inkscape and save it to a svg file.
Install the svgpathtools package (https://github.com/mathandy/svgpathtools):
pip install svgpathtools
Parameters
svgfile
:string
- Name of the svg file containing the path.
path_index
:int
- Index selecting the path.
npoints
:int
- Number of points to spread out on the path.
Returns
vertices
:2D array
- The coordinates of the outline of a fish. npoints rows of x and y coordinates.
def bbox_pathes(*vertices)
-
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
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]]
def translate_pathes(dx, dy, *vertices)
-
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
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).
def center_pathes(*vertices)
-
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]
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).
def rotate_pathes(theta, *vertices)
-
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)
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).
def flipx_pathes(*vertices)
-
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]
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).
def flipy_pathes(*vertices)
-
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]
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).
def mirror_path(vertices1)
-
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
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.
def normalize_path(*vertices)
-
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
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).
def bend_path(path, bend, size, size_fac=1.0)
-
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
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.
def export_path(vertices)
-
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('')
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).
def export_fish(name, body, *fins)
-
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
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().
def export_fish_demo()
-
Expand source code
def export_fish_demo(): """Code demonstrating how to export a fish outline from SVG file. """ body = extract_path('fish.svg', 0, 300) fin0 = extract_path('fish.svg', 1, 70) fin1 = extract_path('fish.svg', 2, 70) verts = (body, fin0, fin1) # 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('fish_side', *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, 1) ax.set_ylim(-0.5, 0.5) plt.show()
Code demonstrating how to export a fish outline from SVG file.
def main()
-
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()
Plot some fish shapes and surface normals.