Source code for ethoscope.roi_builders.target_roi_builder

__author__ = 'quentin'

import cv2

try:
    CV_VERSION = int(cv2.__version__.split(".")[0])
except:
    CV_VERSION = 2

try:
    from cv2.cv import CV_CHAIN_APPROX_SIMPLE as CHAIN_APPROX_SIMPLE
    from cv2.cv import CV_AA as LINE_AA
except ImportError:
    from cv2 import CHAIN_APPROX_SIMPLE
    from cv2 import LINE_AA

import numpy as np
import logging
from ethoscope.roi_builders.roi_builders import BaseROIBuilder
from ethoscope.core.roi import ROI
from ethoscope.utils.debug import EthoscopeException
import itertools


[docs]class TargetGridROIBuilder(BaseROIBuilder): _adaptive_med_rad = 0.10 _expected__min_target_dist = 10 # the minimal distance between two targets, in 'target diameter' _n_rows = 10 _n_cols = 2 _top_margin = 0 _bottom_margin = None _left_margin = 0 _right_margin = None _horizontal_fill = 1 _vertical_fill = None _description = {"overview": "A flexible ROI builder that allows users to select parameters for the ROI layout." "Lengths are relative to the distance between the two bottom targets (width)", "arguments": [ {"type": "number", "min": 1, "max": 16, "step":1, "name": "n_cols", "description": "The number of columns","default":1}, {"type": "number", "min": 1, "max": 16, "step":1, "name": "n_rows", "description": "The number of rows","default":1}, {"type": "number", "min": 0.0, "max": 1.0, "step":.001, "name": "top_margin", "description": "The vertical distance between the middle of the top ROIs and the middle of the top target.","default":0.0}, {"type": "number", "min": 0.0, "max": 1.0, "step":.001, "name": "bottom_margin", "description": "Same as top_margin, but for the bottom.","default":0.0}, {"type": "number", "min": 0.0, "max": 1.0, "step":.001, "name": "right_margin", "description": "Same as top_margin, but for the right.","default":0.0}, {"type": "number", "min": 0.0, "max": 1.0, "step":.001, "name": "left_margin", "description": "Same as top_margin, but for the left.","default":0.0}, {"type": "number", "min": 0.0, "max": 1.0, "step":.001, "name": "horizontal_fill", "description": "The proportion of the grid space user by the roi, horizontally.","default":0.90}, {"type": "number", "min": 0.0, "max": 1.0, "step":.001, "name": "left_margin", "description": "Same as horizontal_margin, but vertically.","default":0.90} ]} def __init__(self, n_rows=1, n_cols=1, top_margin=0, bottom_margin=0, left_margin=0, right_margin=0, horizontal_fill=.9, vertical_fill=.9): """ This roi builder uses three black circles drawn on the arena (targets) to align a grid layout: IMAGE HERE :param n_rows: The number of rows in the grid. :type n_rows: int :param n_cols: The number of columns. :type n_cols: int :param top_margin: The vertical distance between the middle of the top ROIs and the middle of the top target :type top_margin: float :param bottom_margin: same as top_margin, but for the bottom. :type bottom_margin: float :param left_margin: same as top_margin, but for the left side. :type left_margin: float :param right_margin: same as top_margin, but for the right side. :type right_margin: float :param horizontal_fill: The proportion of the grid space user by the roi, horizontally (between 0 and 1). :type horizontal_fill: float :param vertical_fill: same as vertical_fill, but horizontally. :type vertical_fill: float """ self._n_rows = n_rows self._n_cols = n_cols self._top_margin = top_margin self._bottom_margin = bottom_margin self._left_margin = left_margin self._right_margin = right_margin self._horizontal_fill = horizontal_fill self._vertical_fill = vertical_fill # if self._vertical_fill is None: # self._vertical_fill = self._horizontal_fill # if self._right_margin is None: # self._right_margin = self._left_margin # if self._bottom_margin is None: # self._bottom_margin = self._top_margin super(TargetGridROIBuilder,self).__init__() def _find_blobs(self, im, scoring_fun): grey= cv2.cvtColor(im,cv2.COLOR_BGR2GRAY) rad = int(self._adaptive_med_rad * im.shape[1]) if rad % 2 == 0: rad += 1 med = np.median(grey) scale = 255/(med) cv2.multiply(grey,scale,dst=grey) bin = np.copy(grey) score_map = np.zeros_like(bin) for t in range(0, 255,5): cv2.threshold(grey, t, 255,cv2.THRESH_BINARY_INV,bin) if np.count_nonzero(bin) > 0.7 * im.shape[0] * im.shape[1]: continue if CV_VERSION == 3: _, contours, h = cv2.findContours(bin,cv2.RETR_EXTERNAL,CHAIN_APPROX_SIMPLE) else: contours, h = cv2.findContours(bin,cv2.RETR_EXTERNAL,CHAIN_APPROX_SIMPLE) bin.fill(0) for c in contours: score = scoring_fun(c, im) if score >0: cv2.drawContours(bin,[c],0,score,-1) cv2.add(bin, score_map,score_map) return score_map def _make_grid(self, n_col, n_row, top_margin=0.0, bottom_margin=0.0, left_margin=0.0, right_margin=0.0, horizontal_fill = 1.0, vertical_fill=1.0): y_positions = (np.arange(n_row) * 2.0 + 1) * (1-top_margin-bottom_margin)/(2*n_row) + top_margin x_positions = (np.arange(n_col) * 2.0 + 1) * (1-left_margin-right_margin)/(2*n_col) + left_margin all_centres = [np.array([x,y]) for x,y in itertools.product(x_positions, y_positions)] sign_mat = np.array([ [-1, -1], [+1, -1], [+1, +1], [-1, +1] ]) xy_size_vec = np.array([horizontal_fill/float(n_col), vertical_fill/float(n_row)]) / 2.0 rectangles = [sign_mat *xy_size_vec + c for c in all_centres] return rectangles def _points_distance(self, pt1, pt2): x1 , y1 = pt1 x2 , y2 = pt2 return np.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2) def _score_targets(self,contour, im): area = cv2.contourArea(contour) perim = cv2.arcLength(contour,True) if perim == 0: return 0 circul = 4 * np.pi * area / perim ** 2 if circul < .8: # fixme magic number return 0 return 1 def _find_target_coordinates(self, img): map = self._find_blobs(img, self._score_targets) bin = np.zeros_like(map) # as soon as we have three objects, we stop contours = [] for t in range(0, 255,1): cv2.threshold(map, t, 255,cv2.THRESH_BINARY ,bin) if CV_VERSION == 3: _, contours, h = cv2.findContours(bin,cv2.RETR_EXTERNAL, CHAIN_APPROX_SIMPLE) else: contours, h = cv2.findContours(bin, cv2.RETR_EXTERNAL, CHAIN_APPROX_SIMPLE) if len(contours) <3: raise EthoscopeException("There should be three targets. Only %i objects have been found" % (len(contours)), img) if len(contours) == 3: break target_diams = [cv2.boundingRect(c)[2] for c in contours] mean_diam = np.mean(target_diams) mean_sd = np.std(target_diams) if mean_sd/mean_diam > 0.10: raise EthoscopeException("Too much variation in the diameter of the targets. Something must be wrong since all target should have the same size", img) src_points = [] for c in contours: moms = cv2.moments(c) x , y = moms["m10"]/moms["m00"], moms["m01"]/moms["m00"] src_points.append((x,y)) a ,b, c = src_points pairs = [(a,b), (b,c), (a,c)] dists = [self._points_distance(*p) for p in pairs] # that is the AC pair hypo_vertices = pairs[np.argmax(dists)] # this is B : the only point not in (a,c) for sp in src_points: if not sp in hypo_vertices: break sorted_b = sp dist = 0 for sp in src_points: if sorted_b is sp: continue # b-c is the largest distance, so we can infer what point is c if self._points_distance(sp, sorted_b) > dist: dist = self._points_distance(sp, sorted_b) sorted_c = sp # the remaining point is a sorted_a = [sp for sp in src_points if not sp is sorted_b and not sp is sorted_c][0] sorted_src_pts = np.array([sorted_a, sorted_b, sorted_c], dtype=np.float32) return sorted_src_pts def _rois_from_img(self,img): sorted_src_pts = self._find_target_coordinates(img) dst_points = np.array([(0,-1), (0,0), (-1,0)], dtype=np.float32) wrap_mat = cv2.getAffineTransform(dst_points, sorted_src_pts) rectangles = self._make_grid(self._n_cols, self._n_rows, self._top_margin, self._bottom_margin, self._left_margin,self._right_margin, self._horizontal_fill, self._vertical_fill) shift = np.dot(wrap_mat, [1,1,0]) - sorted_src_pts[1] # point 1 is the ref, at 0,0 rois = [] for i,r in enumerate(rectangles): r = np.append(r, np.zeros((4,1)), axis=1) mapped_rectangle = np.dot(wrap_mat, r.T).T mapped_rectangle -= shift ct = mapped_rectangle.reshape((1,4,2)).astype(np.int32) cv2.drawContours(img,[ct], -1, (255,0,0),1,LINE_AA) rois.append(ROI(ct, idx=i+1)) # cv2.imshow("dbg",img) # cv2.waitKey(0) return rois
[docs]class SleepMonitorWithTargetROIBuilder(TargetGridROIBuilder): _description = {"overview": "The default sleep monitor arena with ten rows of two tubes.", "arguments": []} def __init__(self): r""" Class to build ROIs for a two-columns, ten-rows for the sleep monitor (`see here <https://github.com/gilestrolab/ethoscope_hardware/tree/master/arenas/arena_10x2_shortTubes>`_). """ #`sleep monitor tube holder arena <todo>`_ super(SleepMonitorWithTargetROIBuilder, self).__init__(n_rows=10, n_cols=2, top_margin= 6.99 / 111.00, bottom_margin = 6.99 / 111.00, left_margin = -.033, right_margin = -.033, horizontal_fill = .975, vertical_fill= .7 )
[docs]class OlfactionAssayROIBuilder(TargetGridROIBuilder): _description = {"overview": "The default odor assay roi layout with ten rows of single tubes.", "arguments": []} def __init__(self): """ Class to build ROIs for a one-column, ten-rows (`see here <https://github.com/gilestrolab/ethoscope_hardware/tree/master/arenas/arena_10x1_longTubes>`_) """ #`olfactory response arena <todo>`_ super(OlfactionAssayROIBuilder, self).__init__(n_rows=10, n_cols=1, top_margin=6.99 / 111.00, bottom_margin =6.99 / 111.00, left_margin = -.033, right_margin = -.033, horizontal_fill = .975, vertical_fill= .7 )
[docs]class HD12TubesRoiBuilder(TargetGridROIBuilder): _description = {"overview": "The default high resolution, 12 tubes (1 row) roi layout", "arguments": []} def __init__(self): r""" Class to build ROIs for a twelve columns, one row for the HD tracking arena (`see here <https://github.com/gilestrolab/ethoscope_hardware/tree/master/arenas/arena_mini_12_tubes>`_) """ super(HD12TubesRoiBuilder, self).__init__( n_rows=1, n_cols=12, top_margin= 1.5, bottom_margin= 1.5, left_margin=0.05, right_margin=0.05, horizontal_fill=.7, vertical_fill=1.4 )