Python classes for representing nerve morphology (Sample)
The nerve cross section includes the outer nerve trace (if present; not required for monofascicular nerves) and, for each fascicle, either a single “inner” perineurium trace or both “inner” and “outer” perineurium traces. We provide automated control to correct for tissue shrinkage during histological processes [1] (S8 Text). Morphology metrics (e.g., nerve and fascicle(s) cross-sectional areas and centroids, major and minor axis lengths, and rotations of the best-fit ellipses) are automatically reported in Sample (S8 Text).
Trace
Trace is the core Python class for handling a list of points that define a closed loop for a tissue boundary in a nerve cross section (see “Tissue Boundaries” in Fig 2). Trace has built-in functionality for transforming, reporting, displaying, saving, and performing calculations on its data contents and properties. Python classes Nerve, Fascicle, and Slide are all special instances or hierarchical collections of Trace.
A Trace requires inputs of a set of (x,y)-points that define a closed loop and an exceptions JSON configuration file. The z-points are assumed to be ‘0’. The Trace class already provides many built-in functionalities, but any further user-desired methods needed either to mutate or access nerve morphology should be added to the Trace class.
Trace uses the OpenCV [2], Pyclipper [3], and Shapely [4] Python packages to support modifier methods (e.g., for performing transformations):
scale()
: Used to assign dimensional units to points and to correct for shrinkage of nerve tissues during processing of histology.rotate()
: Performs a rigid rotational transformation of Trace about a point (positive angles are counter-clockwise and negative are clockwise).shift()
: performs a 2D translational shift to Trace (in the (x,y)-plane, i.e., the sample cross section).offset()
: Offsets Trace’s boundary by a discrete distance from the existing Trace boundary (non-affine transformation in the (x,y)-plane, i.e., the sample cross section).pymunk_poly()
: Uses Pymunk to create a body with mass and inertia for a given Trace boundary (used indeform()
, the fascicle repositioning method, from the Deformable class).pymunk_segments()
: Uses Pymunk to create a static body for representing intermediate nerve boundaries (used indeform()
, the fascicle repositioning method, from the Deformable class).
Trace also contains accessor methods:
within()
: Returns a Boolean indicating if a Trace is completely within another Trace.intersects()
: Returns a Boolean indicating if a Trace is intersecting another Trace.centroid()
: Returns the centroid of the best fit ellipse of Trace.area()
: Returns the cross-sectional area of Trace.random_points()
: Returns a random list of coordinates within the Trace (used to define axon locations within the Trace).
Lastly, Trace has a few utility methods:
plot()
: Plots the Trace using formatting options (i.e., using theplt.plot
format, see Matplotlib documentation for details).deepcopy()
: Fully copy an instance of Trace (i.e., copy data, not just a reference/pointer to original instance).write()
: Writes the Trace data to the provided file format (currently, only COMSOL’s sectionwise format —ASCII with .txt extension containing column vectors for x- and y-coordinates—is supported).
Nerve
Nerve is the name of a special instance of Trace reserved for
representing the outer nerve (epineurium) boundary. It functions as an
alias for Trace. An instance of the Nerve class is created if the
“NerveMode”
in Sample (“nerve”) is “PRESENT” (S8 Text)”
Fascicle
Fascicle is a class that bundles together instance(s) of Trace to
represent a single fascicle in a slide. Fascicle can be defined with
either (1) an instance of Trace representing an outer perineurium trace
and one or more instances of Trace representing inner perineurium
traces, or (2) an inner perineurium trace that is subsequently scaled to
make a virtual outer using Trace’s methods deepcopy()
and offset()
and
the perineurium thickness defined by the “PerineuriumThicknessMode”
in
Sample (“ci_perineurium_thickness”
) (S8 Text). Upon instantiation,
Fascicle automatically validates that each inner instance of Trace is
fully within its outer instance of Trace and that no inner instance of
Trace intersects another inner instance of Trace.
Fascicle contains methods for converting a binary mask image of
segmented fascicles into instances of the Fascicle class. The method
used depends on the contents of the binary image inputs to the pipeline
as indicated by the “MaskInputMode”
in Sample (“mask_input”
)
(i.e., INNER_AND_OUTER_SEPARATE
, INNER_AND_OUTER_COMPILED
, or
INNERS
). For each of the mask-to-Fascicle conversion methods, the OpenCV
Python package finds material boundaries and reports their nested
hierarchy (i.e., which inner Traces are within which outer Traces,
thereby associating each outer with one or more inners). The methods are
expecting a maximum hierarchical level of 2: one level for inners and
one level for outers.
If separate binary images were provided containing contours for inners (
i.tif
) and outers (o.tif
), then the“MaskInputMode”
in Sample (“mask_input”
, S8 Text) isINNER_AND_OUTER_SEPARATE
.If a single binary image was provided containing combined contours of inners and outers (
c.tif
), then the“MaskInputMode”
in Sample (“mask_input”
, S8 Text) isINNER_AND_OUTER_COMPILED
.If only a binary image was provided for contours of inners (
i.tif
), the“MaskInputMode”
(“mask_input”
, S8 Text) in Sample isINNERS
.
In all cases, the Fascicle class uses its to_list()
method to generate the appropriate
Traces. Additionally, Fascicle has a write()
method which saves a Fascicle’s
inner (one or many) and outer Traces to files that later serve as inputs
for COMSOL to define material boundaries in a nerve cross section
(sectionwise file format
, i.e., ASCII with .txt
extension
containing column vectors for x- and y-coordinates). Lastly, Fascicle
has a morphology_data()
method which uses Trace’s area()
and ellipse()
methods to return the area and the best-fit ellipse centroid, axes, and
rotation of each outer and inner as a JSON Object to Sample (S8 Text).
Slide
The Slide class represents the morphology of a single transverse cross section of a nerve sample (i.e., nerve and fascicle boundaries). An important convention of the pipeline is that the nerve is always translated such that its centroid (i.e., from best-fit ellipse) is at the origin (x,y,z) = (0,0,0) and then extruded in the positive (z)-direction in COMSOL. Slide allows operations such as translation and plotting to be performed on all Nerve and Fascicle Traces that define a sample collectively.
To create an instance of the Slide class, the following items must be defined:
A list of instance(s) of the Fascicle class.
“NerveMode”
from Sample (“nerve”) (i.e.,PRESENT
as in the case of nerves with epineurium (n.tif
) orNOT_PRESENT
otherwise, (S8 Text)).An instance of the Nerve class if
"NerveMode"
isPRESENT
.A Boolean for whether to reposition fascicles within the nerve from
“ReshapeNerveMode”
in Sample (S8 Text).A list of exceptions.
The Slide class validates, manipulates, and writes its contents.
In Slide’s
validation()
method, Slide returns a Boolean indicating if its Fascicles and Nerve Traces are overlapping or too close to one another (based on the minimum fascicle separation parameter in Sample).
In Slide’s
smooth_traces()
method, Slide applies a smooth operation to each Trace in the slide, leveraging the offset method, and applying smoothing based onsmoothing
parameters. defined in Sample (S8 Text).In Slide’s
move_center()
method, Slide repositions its contents about a central coordinate using Trace’sshift()
method available to both the Nerve and Fascicle classes (by convention, in ASCENT this is (x,y) = (0,0)).In Slide’s
reshaped_nerve()
method, Slide returns the deformed boundary of Nerve based on the“ReshapeNerveMode”
in Sample (“reshape_nerve”
, (S8 Text)) (e.g., CIRCLE).Using the methods of Nerve and Fascicle, which are both manifestations of Trace, Slide has its own methods
plot()
,scale()
, androtate()
.Slide has its own
write()
method which determines the file structure to which the Trace contours are saved to file insamples/<sample index>/slides/
.
Note that the sample data hierarchy can contain more than a single Slide instance (the default being 0 as the cassette index and 0 as the section index, hence the 0/0 seen in S3 Text Figure A) even though the pipeline data processing assumes that only a single Slide exists. This will allow the current data hierarchy to be backwards compatible if multi-Slide samples are processed in the future.
Map
Map is a Python class used to keep track of the relationship of the longitudinal position of all Slide instances for a Sample class. At present, the pipeline only supports models of nerves with constant cross-sectional area, meaning only one Slide is used per FEM, but this class is implemented for future expansion of the pipeline to construct three-dimensional nerve models with varying cross section (e.g., using serial histological sections). If only one slide is provided, Map is generated automatically, and the user should have little need to interact with this class.
Sample
The Sample class is initialized within Runner’s run()
method by loading
Sample and Run configurations (S7 and S8 Text). First, Sample’s
build_file_structure()
method creates directories in samples/
and
populates them with the user’s file inputs from input/<NAME>/;
the
images are copied over for subsequent processing, as well as for
convenience in creating summary figures. Sample then uses its populate()
method to construct instances of Nerve and Fascicle in memory from the
input sample morphology binary images (see Fascicle class above for
details). Sample’s populate()
method packages instances of Nerve and
Fascicle(s) into an instance of Slide.
Sample’s get_factor()
method obtains the ratio of microns/pixel for the input masks,
either utilizing a scale bar image, or an explicit scale factor input depending on
the user’s ScaleInputMode
defined in Sample (S8 Text).
Sample’s scale()
method is used to convert Trace points from pixel
coordinates to coordinates with units of distance based either on the length of
the horizontal scale bar as defined in Sample (micrometers) and
the width of the scale bar (pixels) in the input binary image (s.tif
), or on the explicitly specified scale ratio defined in Sample (S8 Text).
If using a scale bar for scale input, it must be a perfectly horizontal line. Sample’s scale()
method is also used within populate()
to correct for shrinkage that may
have occurred during the histological tissue processing. The percentage
increase for shrinkage correction in the slide’s 2D geometry is stored
as a parameter “shrinkage” in Sample (S8 Text). Additionally, Slide has a
move_center()
method which is used to center Slide about a point within
populate()
. Note that Sample is centered with the centroid of the
best-fit ellipse of the outermost Trace (Nerve if “NerveMode”
in
Sample (“nerve”) is “PRESENT”
, outer Trace if “NerveMode”
is
“NOT_PRESENT”
, (S8 Text)) at the origin (0,0,0). Change in rotational or
translational placement of the cuff around the nerve is accomplished by
moving the cuff and keeping the nerve position fixed (S19 Text).
Sample’s im_preprocess()
method performs preprocessing operations on the binary input
masks based on the parameters given under preprocess
in Sample (S8 Text).
Sample’s io_from_compiled()
generates outers (o.tif) and inners (i.tif) from the
(compiled) c.tif mask if "MaskInputMode"
is INNER_AND_OUTER_COMPILED
. These generated
masks are then used in Fascicle’s to_list()
method.
Sample’s generate_perineurium()
method is used to generate the perineurium for fascicle
inners in the case where "MaskInputMode"
= INNERS
. This leverages Trace’s offset()
method, and fits the generated perineurium based on ci_perineurium_thickness
defined in
Sample (S8 Text).
Sample’s populate()
method also manages operations for saving tissue
boundaries of the Sample (Nerve and Fascicles) to CAD files
(slides/#/#/sectionwise2d/
) for input to COMSOL with Sample’s
write()
method.
Sample’s output_morphology_data()
method collects sample morphology
information (area, and the best-fit ellipse information: centroid, major
axis, minor axis, and rotation) for each original Trace (i.e., Fascicle
inners and outers, and Nerve) and saves the data under “Morphology” in
Sample.
Lastly, since Sample inherits Saveable
, Sample has access to the save()
method which saves the Python object to file.
Deformable
If “DeformationMode”
in Sample (“deform”) is set to NONE
, then the
Deformable class takes no action (S8 Text). However, if “DeformationMode”
in
Sample is set to PHYSICS
, then Deformable’s deform()
method
simulates the change in nerve cross section that occurs when a nerve is
placed in a cuff electrode. Specifically, the outer boundary of a
Slide’s Nerve mask is transformed into a user-defined final geometry
based on the “ReshapeNerveMode”
in Sample (i.e., CIRCLE
) while
maintaining the cross-sectional area. Meanwhile, the fascicles (i.e.,
outers) are repositioned within the new nerve cross section in a
physics-based way using Pymunk [5], a 2D physics library, in which
each fascicle is treated as rigid body with no elasticity as it is
slowly “pushed” into place by both surrounding fascicles and the nerve
boundary (Figure A).
Figure A. Snapshots at 0%, 50%, and 100% (left-to-right) of the deformation process powered by the pygame package [6]. The deformation process is shown for two minimum fascicle separation constraints: 10 µm (top row) and 20 µm (bottom row). The geometry at 0% deformation is shown after the fascicles have been spread out to the minimum separation constraint.
The deform()
method updates the nerve boundary to
intermediately-deformed nerve traces between the nerve’s
“boundary_start”
(i.e., the Trace’s profile in segmented image) and
“boundary_end”
(i.e., the Trace’s profile after accommodation to the
cuff’s inner diameter, which is determined by the “ReshapeNerveMode”
(“reshape_nerve”
, S8 Text) while the fascicle contents are allowed to
rearrange in a physics-space. By default, all fascicles have the same
“mass”, but their moment of inertia is calculated for each fascicle
based on its geometry (see Trace’s pymunk_poly()
method). Each fascicle
is also assigned a “friction coefficient” of 0.5 as well as a “density”
of 0.01. These measurements are mostly important as they relate to one
another, not as absolute values. Importantly, we set the elasticity of
all the fascicles to 0, so all kinetic energy is absorbed, and fascicles
only move if they are directly pushed by another fascicle or by the
nerve barrier. In Sample’s populate()
method, the final fascicle
locations and rotations returned by the deform()
method are then applied
to each fascicle using the Fascicle class’s shift()
and rotate()
methods.
Deformable’s convenience constructor, from_slide()
, is automatically
called in Sample’s populate()
method, where a Slide is deformed to user
specification. The from_slide()
method takes three input arguments: The
Slide object from which to construct the current Deformable object, the
“ReshapeNerveMode”
(e.g., CIRCLE, S8 Text), and the minimum distance between
fascicles. If only inners are provided, virtual outers interact during
nerve deformation to account for the thickness of the perineurium. Each
inner’s perineurium thickness is defined by the
“PerineuriumThicknessMode”
in Sample
(“ci_perineurium_thickness”
, S8 Text), which specifies the linear
relationship between inner diameter and perineurium thickness defined in
config/system/ci_peri_thickness.json
(S8 Text). Deformable’s
from_slide()
method uses Deformable’s deform_steps()
method to
calculate the intermediately-deformed nerve traces between the
boundary_start
and the boundary_end
, which contain the same number of
points and maintain nerve cross-sectional area. The deform_steps()
method maps points between boundary_start
and boundary_end
in the
following manner. Starting from the two points where the major axis of
the Nerve’s best-fit ellipse intersects boundary_start
and
boundary_end
, the algorithm matches consecutive boundary_start
and
boundary_end
points and calculates the vectors between all point pairs.
The deform_steps()
method then returns a list of
intermediately-deformed nerve traces between the boundary_start
and
boundary_end
by adding linearly-spaced portions of each point pair’s
vector to boundary_start
. Also note that by defining “deform_ratio”
(value between 0 and 1) in Sample, the user can optionally
indicate a partial deformation of the Nerve (S8 Text).
In the case where "deform_ratio"
is set to 0, minimum fascicle separation will still be
enforced, but no changes to the nerve boundary will occur.
Enforcing a minimum fascicle separation that is extremely large (e.g., 20 µm) can cause inaccurate deformation, as fascicles may be unable to satisfy both minimum separation constraints and nerve boundary constraints.
To maintain a minimum distance between adjacent fascicles, the
Deformable’s deform()
method uses Trace’s offset()
method to perform a
non-affine scaling out of the fascicle boundaries by a fixed distance
before defining the fascicles as rigid bodies in the pygame
physics
space. At regular intervals in physics simulation time, the nerve
boundary is updated to the next Trace in the list of
intermediately-deformed nerve traces created by deform_steps()
. This
number of Trace steps defaults to 36 but can be optionally set in
Sample with the “morph_count”
parameter by the user (S8 Text). It is
important to note that too few Trace steps can result in fascicles lying
outside of the nerve during deformation, while too many Trace steps can
be unnecessarily time intensive. We’ve set the default to 36 because it
tends to minimize both aforementioned issues for all sample sizes
and types that we have tested.
The user may also visualize nerve deformation by setting the
“deform_animate”
argument to true in sample.populate()
(called in
Runner’s run()
method) (S8 Text). Visualizing sample deformation can be helpful
for debugging but increases computational load and slows down the
deformation process significantly. When performing deformation on many
slides, we advise setting this flag to false.
References
Boyd IA, Kalu KU. Scaling factor relating conduction velocity and diameter for myelinated afferent nerve fibres in the cat hind limb. J Physiol. 1979 Apr;289:277–97. Available from: https://doi.org/10.1113/jphysiol.1979.sp012737
Bradski G, Daebler A. Learning OpenCV. Computer vision with OpenCV library. 2008 Jan 1;222–64.
Johnson A, Chalton M, Treyer L, Ratajc G. pyclipper · PyPI [Internet]. 2019 [cited 2020 Apr 20]. Available from: https://pypi.org/project/pyclipper/
Gillies S. Shapely · PyPI [Internet]. 2019 [cited 2020 Apr 20]. Available from: https://pypi.org/project/Shapely/
Blomqvist V. pymunk · PyPI [Internet]. 2019 [cited 2020 Apr 20]. Available from: https://pypi.org/project/pymunk/
Shinners P. Pygame Intro — pygame v2.0.0.dev5 documentation [Internet]. [cited 2020 Apr 20]. Available from: https://www.pygame.org/docs/tut/PygameIntro.html