Source code for allensdk.internal.morphology.morphvis

from PIL import Image, ImageDraw
import numpy as np

[docs]class MorphologyColors(object): def __init__(self): self.soma = (0, 0, 0) self.axon = (70, 130, 180) self.basal = (178, 34, 34) self.apical = (255, 127, 80)
[docs] def set_soma_color(self, r, g, b): self.soma = (r, g, b)
[docs] def set_axon_color(self, r, g, b): self.axon = (r, g, b)
[docs] def set_basal_color(self, r, g, b): self.basal = (r, g, b)
[docs] def set_apical_color(self, r, g, b): self.apical = (r, g, b)
# create empty image
[docs]def create_image(w, h, color=None, alpha=False): if alpha: mode = 'RGBA' else: mode = 'RGB' if color is not None: return Image.new(mode, (w,h), color) else: return Image.new(mode, (w,h))
[docs]def calculate_scale(morph, pix_width, pix_height): """ Calculates scaling factor and x,y insets required to auto-scale and center morphology into box with specified numbers of pixels Parameters ---------- morph: AISDK Morphology object pix_width: int Number of image pixels on X axis pix_height: int Number of image pixels on Y axis Returns ------- real, real, real First return value is the scaling factor. Second is the number of pixels needed to adjust x-coordinates so that the morphology is horizontally centered. Third is the number of pixels needed to adjust the y-coordinates so that the morphology is vertically centered. """ dims, low, high = morph.get_dimensions() # get boundaries of morphology xlow = low[0] xhigh = high[0] ylow = low[1] yhigh = high[1] # determine scale on X and Y to make morphology fit in image area hscale = pix_width / (xhigh - xlow) vscale = pix_height / (yhigh - ylow) # select lowest scaling factor so morphology is stretched to # maximum width/height along axis that is tightest fit # and adjust inset on other axis so morphology is centered scale_factor = min(hscale, vscale) if hscale < vscale: # image constrained on horizontal axis scale_factor = hscale # center image vertically v_center = (ylow + yhigh) / 2.0 scale_inset_x = -low[0] * scale_factor # invert y coordinates for conversion to pixel space scale_inset_y = pix_height/2 + scale_factor*v_center else: # image constrained on vertical axis scale_factor = vscale # center image horizontally h_center = (xlow + xhigh) / 2.0 scale_inset_x = pix_width/2 - scale_factor*h_center scale_inset_y = -low[1] * scale_factor return scale_factor, scale_inset_x, scale_inset_y
# draw morphology on image -- takes image and morphology, modifies image # options: scale to fit | linear scaling
[docs]def draw_morphology(img, morph, inset_left=0, inset_right=0, inset_top=0, inset_bottom=0, scale_to_fit=False, scale_factor=1.0, colors=None): """ Draws morphology onto image When no scaling is applied, and no insets are provided, the coordinates of the morphology are used directly -- i.e., 100 in morphology coordinates is equal to 100 pixels. The scale factor is multiplied to morphology coordinates before being drawn. If scale_factor=2 then 50 in morphology coordinates is 100 pixels. Left and top insets shift the coordinate axes for drawing. E.g., if left=10 and top=5 then 0,0 in morphology coordinates is 10,5 in pixel space. Bottom and right insets are ignored. If scale_to_fit is set then scale factor is ignored. The morphology is scaled to be the maximum size that fits in the image, taking into account insets. In a 100x100 image, if all insets=10, then the image is scaled to fit into the center 80x80 pixel area, and nothing is drawn in the inset border areas. Axons are drawn before soma and dendrite compartments. Parameters ---------- img: PIL image object morph: AISDK Morphology object inset_*: real This is the number of pixels to use as border on top/bottom/ right/left. If scale_to_fit is false then only the top/left values are used, as the scale_factor will determine how large the morphology is (it can be drawn beyond insets and even beyond image boundaries) scale_to_fit: boolean If true then morphology is scaled to the inset area of the image and scale_factor is ignored. Morphology is centered in the image in the sense that the top/bottom and left/right edges of the morphology are equidistant from image borders. scale_factor: real A scalar amount that is multiplied to morphology coordinates before drawing colors: MorphologyColors object This is the color scheme used to draw the morphology. If colors=None then default coloring is used Returns ------- 2-dimensional array, the pixel coordinates of the soma root [x,y] """ # determine drawing area, scaling factor, offset to origin # if scaling to fit, find value that scales morphology so height # or width matches image area. adjust scale_factor and insets # as necessary so morphology can be drawn normally dims, low, high = morph.get_dimensions() if scale_to_fit: # get image area based on requested insets width, height = img.size pix_width = (width - inset_right) - inset_left pix_height = (height - inset_bottom) - inset_top # get scale and x,y insets from auto-scaling scale_factor, scale_inset_x, scale_inset_y = calculate_scale(morph, pix_width, pix_height) else: # no implicit inset necessary due to scaling scale_inset_x = 0 scale_inset_y = 0 # order compartments by depth to approximate 3D rendering sorted(morph.compartment_list, key=lambda x: x.node1.y) # if color not specified, select default if colors is None: colors = MorphologyColors() canvas = ImageDraw.Draw(img) for i in range(3): for comp in morph.compartment_list: if comp.node2.t == 1: if i != 2: # soma drawn last # NOTE: there are unlikely to be soma compartments # additional soma-drawing code is below continue color = colors.soma elif comp.node2.t == 2: if i != 0: # axon drawn first continue color = colors.axon elif comp.node2.t == 3: if i != 1: # dendrite drawn second continue color = colors.basal elif comp.node2.t == 4: if i != 1: # dendrite drawn second continue color = colors.apical x0 = scale_inset_x + inset_left + scale_factor * comp.node1.x x1 = scale_inset_x + inset_left + scale_factor * comp.node2.x # y coordinate inverted because morphology values are # increasing going 'up while pixel values increase # going down y0 = scale_inset_y + inset_top - scale_factor * comp.node1.y y1 = scale_inset_y + inset_top - scale_factor * comp.node2.y canvas.line((x0, y0, x1, y1), color) # a compartment type is defined by the type of the 2nd node defining # the compartment. if there is a single soma node, there can be # no soma compartments. draw the root soma node root = morph.soma_root() x = scale_inset_x + inset_left + scale_factor * root.x y = scale_inset_y + inset_top - scale_factor * root.y rad = scale_factor * root.radius x0 = int(x - rad) y0 = int(y - rad) x1 = int(x0 + 2*rad) y1 = int(y0 + 2*rad) canvas.ellipse((x0,y0,x1,y1), fill=colors.soma, outline=colors.soma) # return soma root coordinate, in unit of pixels return [x, y]
[docs]def draw_density_hist(img, morph, vert_scale, inset_left=0, inset_right=0, inset_top=0, inset_bottom=0, num_bins=None, colors=None): """ Draws density histogram onto image When no scaling is applied, and no insets are provided, the coordinates of the morphology are used directly -- i.e., 100 in morphology coordinates is equal to 100 pixels. The scale factor is multiplied to morphology coordinates before being drawn. If scale_factor=2 then 50 in morphology coordinates is 100 pixels. Left and top insets shift the coordinate axes for drawing. E.g., if left=10 and top=5 then 0,0 in morphology coordinates is 10,5 in pixel space. Bottom and right insets are ignored. If scale_to_fit is set then scale factor is ignored. The morphology is scaled to be the maximum size that fits in the image, taking into account insets. In a 100x100 image, if all insets=10, then the image is scaled to fit into the center 80x80 pixel area, and nothing is drawn in the inset border areas. Axons are drawn before soma and dendrite compartments. Parameters ---------- img: PIL image object morph: AISDK Morphology object vert_scale: real This is the amout required to multiply to a moprhology y-coordinate to convert it to relative cortical depth (on [0,1]). This is the inverse of the cortical thickness. inset_*: real This is the number of pixels to use as border on top/bottom/ right/left. If scale_to_fit is false then only the top/left values are used, as the scale_factor will determine how large the morphology is (it can be drawn beyond insets and even beyond image boundaries) num_bins: int The number of bins in the histogram colors: MorphologyColors object This is the color scheme used to draw the morphology. If colors=None then default coloring is used Returns ------- Histogram arrays: [hist, hist2, hist3, hist4] where hist is the histgram of all neurites, and hist[234] are the histograms of SWC types 2,3,4 """ # if number of bins not specified, default to vertical size of # drawing area in image img_width, img_height = img.size draw_width = img_width - (inset_left + inset_right) draw_height = img_height - (inset_top + inset_bottom) if num_bins is None: num_bins = draw_height # histograms for each analyzed SWC type hist_2 = np.zeros(num_bins) hist_3 = np.zeros(num_bins) hist_4 = np.zeros(num_bins) # total response in a bin hist = np.zeros(num_bins) print("Vert scale", vert_scale) # if color not specified, select default if colors is None: colors = MorphologyColors() canvas = ImageDraw.Draw(img) # for each compartment, split its length (weight) between bins for # start and end nodes for seg in morph.compartment_list: wt = seg.length / 2.0 # node 1 bin1 = int(-vert_scale * seg.node1.y * num_bins) if bin1 == num_bins: bin1 = num_bins-1 # only include parts of the histogram that are in the viewable # range (ie, exclude parts that extend to pia and/or wm) if bin1 >= 0 and bin1 < num_bins: if seg.node1.t == 2: hist_2[bin1] += wt elif seg.node1.t == 3: hist_3[bin1] += wt elif seg.node1.t == 4: hist_4[bin1] += wt hist[bin1] += wt # node 2 bin2 = int(-vert_scale * seg.node1.y * num_bins) if bin2 == num_bins: bin2 = num_bins-1 if bin2 >= 0 and bin2 < num_bins: if seg.node2.t == 2: hist_2[bin2] += wt elif seg.node2.t == 3: hist_3[bin2] += wt elif seg.node2.t == 4: hist_4[bin2] += wt hist[bin2] += wt # draw axis line col = (128, 128, 128, 256) x0 = inset_left x1 = x0 y0 = inset_top y1 = img_height-inset_bottom canvas.line((x0, y0, x1, y1), col) hist_step = 1.0 * draw_height / num_bins; hist_scale = (draw_width-1) / hist.max() for seg in morph.compartment_list: ypos = 1.0 * inset_top for i in range(num_bins): y0 = int(ypos) y1 = int(ypos + hist_step) x0 = int(1 + inset_left) x1 = int(1 + inset_left + hist_2[i] * hist_scale + 0.99) if x0 != x1: ytmp = ypos while ytmp < y1: canvas.line((x0, int(ytmp), x1, int(ytmp)), colors.axon) ytmp += hist_step x0 = x1 x1 += int(hist_3[i] * hist_scale + 0.99) if x0 != x1: ytmp = ypos while ytmp < y1: canvas.line((x0, int(ytmp), x1, int(ytmp)), colors.basal) ytmp += hist_step x0 = x1 x1 += int(hist_4[i] * hist_scale + 0.99) if x0 < x1: ytmp = ypos while ytmp < y1: canvas.line((x0, int(ytmp), x1, int(ytmp)), colors.apical) ytmp += hist_step ypos += hist_step return hist, hist_2, hist_3, hist_4
# TODO # draw path on image -- takes image and path, modifies image #def draw_path(img, path, color): # path is in units of pixels # foreach vertex, draw line in specified color # refine section boundaries -- takes labeled regions and generates labeled mask # -> need constraints on input. lookup of points is easy. categorizing # what pixel is part of what ask is not without expanding masks laterally # label morphology -- takes morpholgoy and labeled mask and adds lables # to each compartment