Demo model from MRI#

This script builds a model from a segmented MRI for Subject_01. The corresponding jupyter notebook script can be downloaded here. The input data can be downloaded here.

Segmented MRI is stored in a “MRI Segmentation.nii.gz” file and is open with nibabel package.

The detailed label information is presented in “label_description.json” file. It contains tissue information for each image label as well as muscle information (human readable labels and tendon planes) for most of the muscles. This input is sufficient to build a full model.

%config Completer.use_jedi = False

import glob
import os
import json
from itertools import product
import time
import pickle

import matplotlib.pyplot as plt
import trimesh
import numpy as np
import nibabel as nib

import neurodec as nd

nd.mdt._API_URL = "http://18.205.41.67:8001/"

nd.mdt.wait = False  # Set to true to wait step by step.

Prepare model#

Load MRI segmentation and labels information

DATA_DIR = '<your-data-path>/subject-01/'
nii = nib.load(DATA_DIR + 'MRI Segmentation.nii.gz')
image = nii.get_fdata()
voxel_sizes = np.array(nii.header.get_zooms()) * 1e-3 # from mm to m

with open(DATA_DIR + 'labels_description.json') as f:
    labels_description = json.load(f)

# Only these labels will be taken into account to build volume conductor
labels_per_tissue = {
    nd.mdt.TissueType.BONE: [ld['label_in_image'] for ld in labels_description['bones']],
    nd.mdt.TissueType.MUSCLE: [ld['label_in_image'] for ld in labels_description['muscles']],
    nd.mdt.TissueType.FAT: labels_description['fat']['label_in_image'],
    nd.mdt.TissueType.SKIN: labels_description['skin']['label_in_image'],
}

# Ignore marker labels when building volume conductor. Defining this list is optional,
# but it prevents warning messages when creating Image object
labels_to_ignore = np.arange(23, 35)

Create image object

mri_image = nd.mdt.Image.new(image, voxel_sizes, labels_per_tissue, labels_to_ignore)

Create volume tetrahedral mesh from image

volume_mesh = nd.mdt.Tetrahedra.from_image(mri_image)

Create volume conductor

volume_conductor = nd.mdt.Conductor.new(volume_mesh)

Create electrode bracelet

outer_skin_surface = volume_mesh.skin
radius = 0.003  # in meters
first_electrode_location = np.array([0.048833, 0.039151, 0.225424])
rings_normal = np.array([-0.06736656677532368, 0.028649074948560272, -0.9973168885492202])
n_rings = 10
distance_between_rings = 0.009  # in meters
n_electrodes_per_ring = 16

bracelet = nd.mdt.ElectrodeBracelet.new(
    outer_skin_surface, radius, first_electrode_location, rings_normal,
    n_rings, distance_between_rings, n_electrodes_per_ring)

Generated electrodes can be saved as spheres for further visualization

for i, electrode in enumerate(bracelet.electrodes):
    sphere = trimesh.creation.icosphere()
    sphere.vertices = sphere.vertices * electrode.radius
    sphere.vertices = sphere.vertices + electrode.location
    sphere.export(DATA_DIR + f'electrodes/elec_{i}.stl')

Create forward solution

forward_solution = nd.mdt.ForwardSolution.new(volume_conductor, bracelet.electrodes)

Generate MUAPs for each muscle of interest#

Store muscle surfaces extracted from volume mesh with respect to their label in image

surfaces = {}
for surface in volume_mesh.muscles:
    surfaces[surface.image_label] = surface

Generate fibers, motor units and MUAPs.

Note, that we don’t generate fibers for all the muscles in volume conductor. Anconeus and Brachioradialis are not fully contained within the forearm segment and Supinator has too complex geometry to efficiently extract fibers with our current approach. For these muscles we don’t provide tendon plane origins and normals in label description file.

SAMPLING_FREQUENCY = 4000
n_motor_units = 200

muscles_of_interest = []
muaps_per_muscle = []

for muscle in labels_description['muscles']:
    # We are only interested in muscles for which tendon planes are defined
    if 'plane_origins' in muscle:
        # Get muscle surface
        muscles_of_interest.append(muscle)
        muscle_surface = surfaces[muscle['label_in_image']]
        plane_origins = np.array(muscle['plane_origins'])
        plane_normals = np.array(muscle['plane_normals'])

        # Generate fibers
        fibers = nd.mdt.Fibers.new(muscle_surface, plane_origins, plane_normals)

        # Generate fiber basis
        fiber_basis = nd.mdt.FiberBasis.new(fibers, forward_solution)

        # Define fiber properties
        fiber_properties = nd.mdt.FiberProperties.new(fibers)

        # Generate motor units
        motor_units = nd.mdt.MotorUnits.new(fibers, n_motor_units)

        # Simulate corresponding muaps
        muaps = nd.mdt.MotorUnitsActionPotentials.new(fibers, fiber_basis, fiber_properties, motor_units,
                                                      SAMPLING_FREQUENCY)
        muaps_per_muscle.append(muaps)

Simulate simple movement#

def single_muscle_activation(max_mvc, duration, peak_time, fs):
    n_time_samples = int((peak_time + duration / 2.) * fs)
    t = np.linspace(-np.pi / 2, np.pi + np.pi / 2, int(duration * fs))
    waveform = max_mvc * 0.5 * (np.sin(t) + 1.)
    i_0 = n_time_samples - len(waveform)
    muscle_activation = np.zeros(n_time_samples)
    muscle_activation[i_0:] = waveform
    return muscle_activation

def generate_movement(n_muscles, mean_max_mvc, std_max_mvc, duration, peak_time, fs):
    muscle_activations = []
    for i in range(n_muscles):
        max_mvc = np.random.uniform(mean_max_mvc - std_max_mvc,
                                    mean_max_mvc + std_max_mvc)
        max_mvc = np.clip(max_mvc, 0., 100.)
        muscle_activations.append(single_muscle_activation(max_mvc, duration, peak_time, fs))
    return muscle_activations

Print labels of the muscles of interest

for i, muscle in enumerate(muscles_of_interest):
    print(i, muscle['label'])
0 Extensor Carpi Ulnaris
1 Flexor Digitorium Profundus
2 Flexor Carpi Ulnaris
3 Extensor Digitorium
4 Extensor Digiti Minimi
5 Extersor Carpi radialis Longus AND Brevis
6 Palmaris Longus
7 Flexor Carpi Radialis
8 Flexor Digitorium Superficialis
9 Abductor Pollicis Longus
10 Extensor Indicis
11 Extensor Pollicis Longus
12 Pronator Teres
13 Flexor Policis Longus
14 Pronator Quadratus

Select wrist extensors

muscle_idx = [0, 5]
../_images/wrist_extensors.png

Generate a simple 2 second long muscle activation pattern corresponding to a wrist extension of around 40%MVC at its peak.

np.random.seed(1)
n_muscles = len(muscle_idx)
mean_max_mvc = 40 # %MVC
std_max_mvc = 10 # add some inter muscle variability in %MVC
duration = 2. # sec
peak_time = 1.5 # sec
activations = generate_movement(n_muscles, mean_max_mvc, std_max_mvc, duration, peak_time, SAMPLING_FREQUENCY)
for single_activation in activations:
    times = np.arange(len(single_activation)) / SAMPLING_FREQUENCY
    plt.plot(times, single_activation)
../_images/output_29_0.png

Generate impulse trains for each active muscle

impulse_trains_per_muscle = []
for muscle_id, single_activation in zip(muscle_idx, activations):
    impulse_trains_per_muscle.append(nd.mdt.ImpulseTrains.new(muaps_per_muscle[muscle_id],
                                                              single_activation,
                                                              random_seed=0))

Assemble raw sEMG signal from muaps and impulse trains, and load the resulting data.

emg_per_muscle = []
for impulse_trains in impulse_trains_per_muscle:
    emg = nd.mdt.Electromyography.new(impulse_trains)
    emg.wait() # wait for the computation to finish before loading the EMG data
    emg_per_muscle.append(emg.data.copy())

Sum up EMG signals of individual muscles

min_length = np.min([e.shape[1] for e in emg_per_muscle])
emg_total = np.sum([e[:, :min_length] for e in emg_per_muscle], axis=0)

Plot the results

times = np.arange(emg_total.shape[1]) / SAMPLING_FREQUENCY
plot_shift = np.ptp(emg_total)
rings_to_plot = [0, 4, 9]

for ring in rings_to_plot:
    emg_to_plot = emg_total[ring*16:(ring+1)*16]
    plt.figure()
    plt.plot(times, emg_to_plot.T + plot_shift * np.arange(16))
    plt.show()
../_images/output_37_01.png ../_images/output_37_11.png ../_images/output_37_21.png