Beispiel #1
0
def segment_image(image,
                  pmask=None,
                  root_max_radius=15,
                  min_dimension=50,
                  smooth=1,
                  verbose=False):
    if pmask is not None:
        pmask = pmask == pmask.max()

        # find the bounding box, and crop image and pmask
        bbox = _nd.find_objects(pmask)[0]
        img = image[bbox]
        pmask = pmask[bbox]

    else:
        img = image
        bbox = map(slice, [0, 0], image.shape)

    if smooth:
        if pmask is None:
            img = _nd.gaussian_filter(img, sigma=smooth)
        else:
            smooth_img = _nd.gaussian_filter(img * pmask, sigma=smooth)
            smooth_img /= _np.maximum(
                _nd.gaussian_filter(pmask.astype('f'), sigma=smooth), 2**-10)
            img[pmask] = smooth_img[pmask]

    # background removal
    _print_state(verbose, 'remove background')
    img = _remove_background(img, distance=root_max_radius, smooth=1)
    if pmask is not None:
        img *= _nd.binary_erosion(pmask, iterations=root_max_radius)

    # image binary segmentation
    _print_state(verbose, 'segment binary mask')
    rmask = _segment_root(img)
    rmask = _nd.binary_closing(rmask, structure=_np.ones(
        (3, 3)))  # smooth/close a little
    if pmask is not None:
        rmask[-pmask] = 0
    if min_dimension > 0:
        cluster = _nd.label(rmask)[0]
        cluster = _clean_label(cluster, min_dim=min_dimension)
        rmask = cluster > 0

    # config root mask serialization
    rmask = rmask.view(_Image)
    rmask.set_serializer(pil_format='PNG', ser_dtype='uint8', ser_scale=255)

    return rmask, bbox
Beispiel #2
0
    def segment_image(filename,
                      image,
                      mask,
                      root_max_radius=15,
                      min_dimension=50,
                      smooth=1):
        #mask = _nd.binary_erosion(mask==mask.max(), iterations=
        mask = mask == mask.max()

        # find the bounding box, and crop image and mask
        bbox = _nd.find_objects(mask)[0]
        mask = mask[bbox]
        img = image[bbox]

        if smooth:
            smooth_img = _nd.gaussian_filter(img * mask, sigma=smooth)
            smooth_img /= _np.maximum(
                _nd.gaussian_filter(mask.astype('f'), sigma=smooth), 2**-10)
            img[mask] = smooth_img[mask]

        # background removal
        _print_state(verbose, 'remove background')
        img = _remove_background(img, distance=root_max_radius, smooth=1)
        img *= _nd.binary_erosion(mask, iterations=root_max_radius)

        # image binary segmentation
        _print_state(verbose, 'segment binary mask')
        rmask = _segment_root(img)
        rmask[-mask] = 0
        if min_dimension > 0:
            cluster = _nd.label(rmask)[0]
            cluster = _clean_label(cluster, min_dim=min_dimension)
            rmask = cluster > 0

        # save the mask, and bbox
        from PIL.PngImagePlugin import PngInfo
        meta = PngInfo()
        meta.add_text(
            'bbox',
            repr([(bbox[0].start, bbox[0].stop),
                  (bbox[1].start, bbox[1].stop)]))
        _Image(rmask).save(filename, dtype='uint8', scale=255, pnginfo=meta)

        return rmask, bbox
Beispiel #3
0
def detect_marked_plate(image,
                        border_width=0.03,
                        plate_size=120,
                        marker_threshold=0.6,
                        marker_min_size=100):
    mask = image > marker_threshold
    cluster = _clean_label(_nd.label(mask)[0], min_dim=marker_min_size)

    # find plate mask and hull
    pmask, hull = hull_mask(cluster > 0)
    pwidth = pmask.sum(
    )**.5  # estimated plate width in pixels of a square shape
    border = pwidth * border_width
    two = _np.array(2, dtype='uint8')
    pmask = pmask.astype('uint8', copy=False) + two * _nd.binary_erosion(
        mask > 0, iterations=int(border))
    px_scale = plate_size / pwidth

    return pmask, px_scale, hull
Beispiel #4
0
    def compute_circle(filename, image, circle_number):
        from ..image.circle import detect_circles
        cmask = _nd.binary_opening(image > .45, iterations=5)
        cluster = _nd.label(cmask)[0]
        cluster = _clean_label(cluster, min_dim=30)

        ind, qual, c, dmap = detect_circles(n=circle_number, cluster=cluster)
        radius = qual**.5 / _np.pi

        #cind = _np.zeros(c.max()+1)
        #cind[ind] = _np.arange(1,len(ind)+1)
        #cmask = cind[c]

        from PIL.PngImagePlugin import PngInfo
        meta = PngInfo()
        meta.add_text('circles', repr(ind.tolist()))
        meta.add_text('radius', repr(radius.tolist()))
        _Image(cluster).save(filename, dtype='uint8', scale=1, pnginfo=meta)
        #_Image(cmask).save(filename, dtype='uint8', scale=1)

        return cluster, ind, radius
Beispiel #5
0
    def find_plate(filename, image, plate_width):
        from ..image.circle import detect_circles
        mask = image > .6  ## constant
        cluster = _clean_label(_nd.label(mask)[0], min_dim=100)  ## cosntant

        # find plate mask and hull
        pmask, phull = mask_fillhull(cluster > 0)
        pwidth = pmask.sum()**.5  # estimated plate width in pixels
        border = pwidth * .03  ## constant 3% of plate width
        pmask = pmask + 2 * _nd.binary_erosion(pmask > 0,
                                               iterations=int(border))
        px_scale = plate_width / pwidth

        # detect codebar box as the biggest connex cluster
        cbmask = _nd.label(
            _nd.binary_closing((cluster > 0) & (pmask == 3), iterations=5))[0]
        obj = _nd.find_objects(cbmask)
        cbbox = _np.argmax([max([o.stop - o.start for o in ob])
                            for ob in obj]) + 1

        # find codebar mask and hull
        cbmask, cbhull = mask_fillhull(cbmask == cbbox)

        # stack masks such that
        #   pixels to process = 3,
        #   codebar pixels = 2
        #   plate border = 1
        ##mask = pmask + 1*cbmask + 1*_nd.binary_erosion(pmask, iterations=int(border))
        pmask[cbmask > 0] = 2

        # save plate mask
        from PIL.PngImagePlugin import PngInfo
        meta = PngInfo()
        meta.add_text('px_scale', repr(px_scale))
        _Image(pmask).save(filename, dtype='uint8', scale=85,
                           pnginfo=meta)  # 85 = 255/pmask.max()

        return pmask, px_scale
Beispiel #6
0
def segment_root_in_petri_frame(image, plate_border=0.06, plate_width=1, min_dimension=5, filtering=1, is_segmented=False):
    """
    Segment root image and detect a petri plate
    
    Once segmented (using segment_root_image), the petri plate is detected as 
    the biggest contiguous area and a linear homogeneous transformation is 
    fitted (homography).
    
    Input:
    ------
        image:         an image of root system in a petri plate
        plate_border:  the size of the plate border (overlap of the top part on 
                       the bottom), it should be given as a percentage of the 
                       plate width
        plate_width:   the plate size in your unit of choice (see output) 
        min_dimension: remove pixel area that have less that this number of 
                       pixels in width or height.
        is_segmented:  if True, the input image is already segmented.
        
    Output:
    -------
      - The segmented (labeled) root mask cropped around the petri plate.
        The petri plate are removed
      - The 3x3 transformation matrix that represents the mapping of image 
        coordinates into the plate frame with origin at the top-left corner, 
        y-axis pointing downward, and of size given by plate_width
      - The bounding box of the detected plate w.r.t the original mask shape
        given as a tuple pair of slices (the cropping used)
        
    ##todo: to be moved to root.image.plate (which could be renamed to petri or petri_plate)
    """
    from .plate import find_plate
    
    if is_segmented: mask = image
    else:            mask = segment_root_image(image)
    
    # cluster mask & remove border 
    cluster = _nd.label(mask, structure=_np.ones((3,3)))[0]
    cluster[:,0] = 0; cluster[:,-1] = 0
    cluster[0,:] = 0; cluster[-1,:] = 0

    # find objects
    obj   = _nd.find_objects(cluster)
    obj   = [o if o is not None else (slice(0,0),)*2 for o in obj]
    osize = _np.array([(o[0].stop-o[0].start)*(o[1].stop-o[1].start) for o in obj])
    # slightly more robust than argmax only (?)
    pid   = (osize>0.9*osize.max()).nonzero()[0]+1  
    ##old: pid   = _np.argmax([(o[0].stop-o[0].start)*(o[1].stop-o[1].start) for o in obj])+1
    
    # get slice of plate object, and dilate it by 1 pixels
    #   > always possible because cluster border have been set to zero
    ##old: plate_box = [slice(o.start-1,o.stop+1) for o in obj[pid-1]]
    obj_plate = [obj[i] for i in pid-1]  
    min0 = min([o[0].start-1 for o in obj_plate])
    max0 = max([o[0].stop +1 for o in obj_plate])
    min1 = min([o[1].start-1 for o in obj_plate])
    max1 = max([o[1].stop +1 for o in obj_plate])
    plate_box = [slice(min0,max0),slice(min1,max1)]
    cluster   = cluster[plate_box]
    plate = reduce(_np.add,[cluster==id for id in pid])
    # quicker and more robust than fill_holes
    #   => only works because plate has a convex shape
    ##old: plate = _nd.binary_fill_holes(plate)
    def cs(x,axis,reverse):
        if reverse: sl = [slice(None)]*axis + [slice(None,None,-1)]
        else:       sl = slice(None)
        return _np.cumsum(x[sl],axis=axis,dtype=bool)[sl]
    plate = cs(plate,1,1)&cs(plate,1,0)&cs(plate,0,1)&cs(plate,0,0)
    
    # remove cluster not *inside* the plate 
    d2out    = _nd.distance_transform_edt(plate)
    border   = plate_border*2*d2out.max() # d2out.max: plate radius
    in_plate = d2out >= border
    cluster  = cluster*in_plate
        
    # remove not big enough clusters
    cluster  = _clean_label(cluster,min_dim=min_dimension)
    if filtering>0:
        mask = _nd.binary_dilation(cluster>0,iterations=filtering)
        mask = _nd.binary_closing(mask,structure=_np.ones((3,3)),iterations=filtering)
    else:
        mask = cluster>0
        
    plate = find_plate(plate.astype('f'),border=border, plate_width=plate_width)

    return mask, plate, plate_box
Beispiel #7
0
def segment_root_in_circle_frame(image, n=4, pixel_size=1, min_dimension=5, is_segmented=False):
    """
    Segment root image and detect a petri plate
    
    Once segmented (using segment_root_image), the reference frame is detected 
    as made of circular shapes.
    
    Input:
    ------
      - image:         an image of root system in a petri plate
      - n:             the number of circles to be found
      - pixel_size:    size of a pixel in the unit of your choice
                         e.g. 1/(25.4*scaned-dpi) for millimeter unit
      - min_dimension: remove pixel area that have less that this number of 
                       pixels in width or height.
      - is_segmented:  if True, the input image is already segmented.
    Output:
    -------
      - The segmented (labeled) root mask cropped around the circle frame.
        The frame are removed.
      - The 3x3 transformation matrix that represents the mapping of image 
        coordinates into the detected frame: the origin is the top-left circle, 
        x-axis pointing toward the top-right circle, and the size (scale) is 
        computed based on the given 'pixel_size'. 
      - The bounding box containing all detected circles w.r.t the original mask 
        shape. Given as a tuple pair of slices (the cropping used)
        *** If n<=2, do not crop images ***
        
    ##Warning: currently it only crop vertically
    """
    if is_segmented: mask = image
    else:            mask = segment_root_image(image)
    
    d = _nd.distance_transform_edt(mask)
    cluster = _nd.label(d>0)[0]
        
    # remove not big enough clusters
    cluster[:,0] = 0; cluster[:,-1] = 0   # just in case, 
    cluster[0,:] = 0; cluster[-1,:] = 0   # remove border
    if min_dimension>=0:
        cluster  = _clean_label(cluster,min_dim=min_dimension)

    # detect frame circles
    area1 = _np.pi*_nd.maximum(d,cluster,index=_np.arange(cluster.max()+1))**2
    area2 = _label_size(cluster)
    fitv  = 2*area1 - area2 # area1 - abs(area1 - area2) ##?
    fitv[0] = 0
    index = _np.argsort(fitv)[-n:]
    
    if _np.sum(fitv[index]>0)<n:
        index = index[fitv[index]>0]
        print '  Warning, only %d reference circles detected, instead of %d' % (index.size,n)
        
    # find circles position and bbox in image
    obj = _np.asarray(_nd.find_objects(cluster))[index-1,:] 
    start = _np.vectorize(lambda o: o.start)(obj).min(axis=0)
    stop  = _np.vectorize(lambda o: o.stop )(obj).max(axis=0)
    pos   = _np.asarray(_nd.center_of_mass(_virtual_arr(shape=cluster.shape), labels=cluster, index=index))
    
    # remove circle mask from cluster
    for o,i in enumerate(index):
        subm = cluster[obj[o][0], obj[o][1]]
        subm[subm==i] = 0
        
    # crop cluster map, if possible
    if index.size>2:
        circle_box = map(slice,start,stop)
        circle_box[1] = slice(0,cluster.shape[1]) ## only crop vertically
        cluster    = cluster[circle_box]
    
        # detect x-coordinates: top circles are on the x-axis
        order = _np.argsort(pos[:,0])
        pos   = pos[order][:2]           # keep the top two circles
        order = _np.argsort(pos[:,1])
        y,x   = pos[order].T             # sort by x-coordinates
        angle = _np.arctan2(y[1]-y[0], x[1]-x[0]) # angle to horizontal
        
        # create affine transorm  - coord as in [y,x,1] order !!
        sa = _np.sin(angle)
        ca = _np.cos(angle)
        R  = _np.array([[ca,-sa, 0],[sa, ca, 0],[0,0,1]])
        T  = _np.array([[1,0,-y[0]],[0,1,-x[0]],[0,0,1]])
        T  = _np.dot(R,T)*pixel_size
        T[-1,-1] = 1
    else:
        T = _np.eye(3)
        circle_box = map(slice,cluster.shape)
    

    return cluster, T, circle_box
Beispiel #8
0
def linear_label(mask, seed_map=None, compute_segment_map=True):
    """
    Partition mask as a set of linear segments
    
    Input:
    ------
        mask: 
            a binary map of linear structure such as it is return by 
            segment_root_* functions
        seed_map:
            optional map of seed map - an integer area of contiguous labels. 
            Seed area are removed from output maps, but additional nodes are 
            added at cossing of skeleton and seed area. Also fill 'seed' output.
        compute_segment_map:
            If True, the returned segment_map is the input mask labeled with the
            computed segment_skeleton id.
        
    Output:
    -------
        segment_skl:  (*)
            The map of segments pixels of the skeleton
        node_map:     (*)
            The map of nodes    pixels of the skeleton
        segment_map:  (*) 
            The label map of all linear segment - cluster divided in segments
        seed: 
            None if seed_map is None
            Otherwise return a dictionary which contains the following items: 
              'position': the y-x coordinates of the seed center
              'nodes':    the list of nodes id (in node_map) at the seed area
                          border (in no particular order)
              'sid':      the seed id of all these nodes
           
    (*) the labels are contiguous integer starting at 1 (background is 0)
    
    *** Require scikits.image ***
    """
    # compute labeled skeleton
    segment_skl, node_map = _skeleton_label(mask,
                                            closing=1,
                                            fill_node=False,
                                            terminal=True)[:2]
    # don't use file_node=True:
    #   it created segments pixels in the middle of "long" node because it
    #   touched a segments. however the segments then touches the 2 (splitted)
    #   nodes on one side...

    ##tmp_node_map = node_map.copy()
    ##tmp_segment_skl = segment_skl.copy()
    if seed_map is not None:
        # remove segment and node from seed area
        seed_mask = seed_map > 0
        segment_skl[seed_mask] = 0
        node_map[seed_mask] = 0
        # make it contiguous labels
        tmp = segment_skl > 0
        segment_skl[tmp] = _clean_label(segment_skl[tmp])
        ##tmp = node_map>0
        ##node_map[tmp]    = _clean_label(node_map[tmp])

        # find connections with seed area
        #   and add them to node
        dil_smap = _nd.grey_dilation(seed_map, size=(3, 3))
        contact = (dil_smap > 0) & ((segment_skl | node_map) > 0)
        node_map = _nd.label((node_map > 0) | contact)[0]

        # identify node connection
        ny, nx = ((dil_smap > 0) & (node_map > 0)).nonzero()
        # keep a unique (arbitrary) px per node
        s_nid = dict(zip(node_map[ny, nx], dil_smap[ny, nx]))
        n_sid = s_nid.values()
        s_nid = s_nid.keys()

        ##    # 1. seed pixel touching seed area
        ##    #    replace segment px touching seed area by new nodes
        ##dil_smap = _nd.grey_dilation(seed_map,size=(3,3))
        ##ny,nx = ((dil_smap>0)&(segment_skl>0)).nonzero()
        ##s_nid = node_map.max() + _np.arange(1,ny.size+1)    # id of seed *nodes*
        ##n_sid = dil_smap[ny,nx]                             # id of *seed* node
        ##node_map[ny,nx] = s_nid
        ##segment_skl[ny,nx] = 0
        ##    ## new node don't touch existing nodes?
        ##    ##   two connected nodes is not a problem (...?)
        ##    ## any other unexpected error ?
        ##
        ##    # 2. node touching seed area
        ##ny,nx  = ((dil_smap>0)&(node_map>0)).nonzero()
        ##    # keep a unique (arbitrary) px per node
        ##s_nid2 = dict(zip(node_map[ny,nx],dil_smap[ny,nx]))
        ##n_sid2 = s_nid2.values()
        ##s_nid2 = s_nid2.keys()
        ##print s_nid2, len(s_nid2)
        ##print n_sid2, len(n_sid2)
        ##s_nid  = _np.hstack((s_nid, s_nid2))
        ##n_sid  = _np.hstack((n_sid, n_sid2))

        # create output seed structure
        seed = dict()
        # compute seed  positions
        obj = _nd.find_objects(seed_map)
        spos = _np.array([[(sl.stop + sl.start) / 2. for sl in o]
                          for o in obj])
        seed['position'] = spos
        seed['nodes'] = _np.array(s_nid)
        seed['sid'] = _np.array(n_sid)

        if compute_segment_map:
            mask = mask - seed_mask
    else:
        seed = None

    if compute_segment_map:
        # compute segment map by dilation of segment_skeleton into mask
        segment_map = _nd.distance_transform_edt(segment_skl == 0,
                                                 return_indices=True,
                                                 return_distances=False)
        segment_map = segment_skl[tuple(segment_map)] * (mask)
    else:
        segment_map = None

    return segment_skl, node_map, segment_map, seed
Beispiel #9
0
def detect_circles(n,
                   mask=None,
                   cluster=None,
                   dmap=None,
                   min_dimension=None,
                   min_quality=0):
    """
    Find 'n' circles in mask (or cluster)
    
    For all connected components of mask (or cluster), this function compute a 
    "quality" coefficient as the area of the biggest inscribed circles minus 
    the area of the cluster out of that circle. 
    It then returns the indices of the n clusters with highest coefficient 
    
    The quality coefficient can be seen as the size of the circles, minus the 
    error to being a circle. The min_quality parameter would mean the minimum 
    radius of detected circles, subestimated by accepted error area.
    
    :Input:
        n:  
            Number of circles to be found
        mask:    
            Binary array to find the circles in (not used if cluster is given)
        cluster:
            (optional) Label map of mask. If given, mask is  not used. Otherwise 
            it is computed from mask            
        dmap:
            (optional) Distance map of mask
        min_dimension: 
            Does not process clusters that don't have at least one dimension
            bigger than 'min_dimension'
        min_quality:
            If not None, filter out cluster with quality less than this.
            If less than n cluster respect that rule, and n>0, print a warning.
            
    :Output:
        - indices of the 'n' detected "best" circles in clsuter map
        - estimated "quality" of the circles
        - cluster map  (updated in-place if given)
        - distance map (same as input if given)
    """
    if cluster is None:
        if mask is None:
            raise TypeError('either mask of cluster should be given')
        cluster = _nd.label(mask)[0]

    # remove not big enough clusters
    cluster[:, 0] = 0
    cluster[:, -1] = 0  # remove borders
    cluster[0, :] = 0
    cluster[-1, :] = 0  # --------------
    if min_dimension >= 0:
        cluster[:] = _clean_label(cluster, min_dim=min_dimension)

    if dmap is None:
        dmap = _nd.distance_transform_edt(cluster > 0)

    # detect frame circles
    area1 = _np.pi * _nd.maximum(
        dmap, cluster, index=_np.arange(cluster.max() + 1))**2
    area2 = _label_size(cluster)
    fitv = 2 * area1 - area2  # area1 - abs(area1 - area2) ##?
    fitv[0] = 0
    index = _np.argsort(fitv)[-n:][::-1]

    if min_quality is not None:
        index = index[fitv[index] > min_quality]
        if n > 0 and index.size < n:
            print '  Warning, only %d circles detected, instead of %d' % (
                index.size, n)

    return index, fitv[index], cluster, dmap