Logiciel de parasitémie lié au papier de Charles Ma.
authorGreg Burri <greg.burri@gmail.com>
Tue, 6 Oct 2015 06:00:11 +0000 (08:00 +0200)
committerGreg Burri <greg.burri@gmail.com>
Tue, 6 Oct 2015 06:00:11 +0000 (08:00 +0200)
Documentation/ma_autocount/plasmodium-autocount-0.1.py [new file with mode: 0644]

diff --git a/Documentation/ma_autocount/plasmodium-autocount-0.1.py b/Documentation/ma_autocount/plasmodium-autocount-0.1.py
new file mode 100644 (file)
index 0000000..b2fa3d2
--- /dev/null
@@ -0,0 +1,638 @@
+#!/usr/bin/env python
+
+#    Copyright (C) 2010 Monash University
+#    This program is free software; you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation; either version 2 of the License, or
+#    (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with this program; if not, write to the Free Software
+#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+
+"""
+
+Identify red blood cells and red blood cells infected with plasmodium in a set of images.
+
+"""
+
+from PIL import Image
+
+import sys, numpy, os, random
+from scipy import ndimage
+
+Help = """
+
+Usage:
+
+    python %s [parameter adjustments] <image_directory>
+
+Images should be in TIFF, PNG, or BMP format.
+
+Results will be placed in a new directory named <image_directory>_output.
+
+If the images were annotated using Cell Counting Aid, the hand annotations 
+will be shown as circles in the output.
+
+Parameter adjustments take the form
+
+  variable_name=value
+
+See the top of the source code for parameters and their default values.
+These have been tuned for our microscope setup, optimal parameters for
+your setup may differ.
+
+"""
+
+# How many subprocesses to run at once
+N_processors = 1
+
+# ===========
+# Diagnostics
+#
+# What to show in diagnostic output images
+
+Show_edges = False
+Show_stain = False
+Show_infection = False
+
+Show_hand_marks = True
+Show_computer_marks = True
+Show_cell_shapes = False
+
+
+# =================
+# Image scaling
+
+Resize_width = 680
+Resize_height = 512
+
+
+# ==========================================
+# Image enhancement and fg/bg discrimination
+#
+# Blur slightly to reduce noise, and unsharp-mask
+#
+# gaussian_blur(image, a) - b * gaussian_blur(image, c)
+
+Enhance_a = 1.0
+Enhance_b = 0.75
+Enhance_c = 20.0
+
+# Larger = enlarge background area, smaller = enlarge foreground area
+Background_weight = 0.915 #&median
+
+
+# =============================
+# Stain and infection detection
+
+Dark_stain_level = 0.839
+
+Stain_level = 0.9
+Stain_bg_blur = 10.0
+Stain_spread_required = 3.0
+
+Infection_level = 0.762
+Infection_bg_blur = 2.0
+Infection_pixels_required = 1
+
+
+
+# ====================
+# Cell size parameters
+
+# Size of cells, for Hough transform
+Hough_min_radius = 5  
+Hough_max_radius = 20 
+
+# Portion of cell used for classification as infected/weird
+# (may be abutting an infected cell, so be cautious)
+# (as a proportion of the true radius)
+Cell_inside_radius = 0.9
+
+# Do not allow another cell center to be detected within this radius
+Cell_suppression_radius = 1.25
+
+
+# =======================
+# Peculiar cell filtering
+
+# White blood cells appear to be composed entirely of dark staining, filter them out
+Max_dark_stain_proportion = 0.728
+
+# Minimum/maximum radius (as determined by Hough transform)
+Min_cell_radius = 9
+Max_cell_radius = 20
+
+# Proportion of radius that center of mass can be away from Hough determined center
+Max_offcenter = 0.5
+
+
+
+
+
+
+_offset_cache = { }
+def offsets(inner_radius, outer_radius):
+    """
+    Generate a list of offsets for pixels within a circular region
+    """
+    
+    key = (inner_radius,outer_radius)
+    if key not in _offset_cache:
+        result = [ ]
+        int_radius = int(outer_radius)+1
+        for y in xrange(-int_radius,int_radius+1):
+            for x in xrange(-int_radius,int_radius+1):
+                if inner_radius*inner_radius <= y*y+x*x <= outer_radius*outer_radius:
+                    result.append((y,x))
+        _offset_cache[key] = numpy.array(result)
+    return _offset_cache[key]
+
+
+def clipped_coordinates(inner_radius,outer_radius, y,x, height,width):
+    """
+    Coordinates contained within a ring or circle, clipped to rectangular bounds.
+    """
+
+    coords = offsets(inner_radius,outer_radius) + [[y,x]]
+    valid = (
+        (coords[:,0] >= 0) &
+        (coords[:,0] < height) &
+        (coords[:,1] >= 0) &
+        (coords[:,1] < width)
+    )
+    return coords[valid]
+
+           
+def set(array, y, x, value):
+    """
+    Set an element of an array, if it is in bounds, otherwise silently do nothing
+    """
+
+    if y >= 0 and x >= 0 and y < array.shape[0] and x < array.shape[1]:
+        array[y,x] = value
+
+
+def mark_hand(array, filename_in):
+    """    
+    If there is a file of hand marked cells from Cell Counting Aid, mark them.    
+    """
+
+    hand_name = os.path.splitext(filename_in)[0] + '.txt'
+    if not os.path.exists(hand_name): return
+    
+    f = open(hand_name,'rU')
+    
+    #Skip header line
+    f.readline()
+    
+    for line in f:
+        parts = line.rstrip('\n').split(',')
+        x,y,infected = parts[:3]
+        
+        x = int( int(x)/1005.0*array.shape[1] +0.5)
+        y = int( int(y)/592.0*array.shape[0] +0.5)
+        if infected == 'True':
+            if len(parts) == 3:
+                color = (196,0,0)
+            else:
+                infection_type = parts[3]
+                if infection_type == '1':
+                    color = (196, 196, 0)
+                elif infection_type == '3':
+                    color = (196, 0, 0)
+                else:
+                    raise Exception('Unknown infection type')            
+        else:
+            color = (0,0,196)
+        for i in (-2,-1,0,1,2):
+            set(array,y-3,x+i,color)
+            set(array,y+3,x+i,color)
+            set(array,y+i,x-3,color)
+            set(array,y+i,x+3,color)
+
+
+def gaussian_blur(array, amount):
+    """
+    Gaussian blur each channel of a color image.
+    """
+    output = numpy.empty(array.shape, array.dtype)
+    for i in xrange(array.shape[2]):
+        ndimage.gaussian_filter(array[:,:,i],amount,output=output[:,:,i])
+    return output
+
+
+def status(word):
+    pass
+    #sys.stdout.write(word+' ')
+    #sys.stdout.flush()
+
+
+def process(filename_in, filename_out, filename_coords, name):
+    # print 'File: ', filename_in
+   
+    # Load image
+    i = Image.open(filename_in)
+
+    print 'Processing', filename_in, '->', filename_out
+
+    # Resize (mostly done for speed increase)
+    i = i.resize((Resize_width,Resize_height), Image.ANTIALIAS)
+
+    # Convert to numpy array
+    a = numpy.asarray(i).astype('float64')
+    height = a.shape[0]
+    width = a.shape[1]
+
+    a_gray = numpy.sum(a, 2)
+    
+    # Make a copy of the array to doodle various diagnostic markings on
+    a_output = a.copy()    
+
+    # If there are hand-marked cells, show them in the output image
+    if Show_hand_marks:
+        mark_hand(a_output, filename_in)    
+
+
+    # Denoise/unsharp mask
+    a_enhanced = gaussian_blur(a, Enhance_a) - Enhance_b*gaussian_blur(a,Enhance_c)
+    
+    
+    # Split into foreground and background using k-medians
+    status('fg/bg')
+    
+    a_raveled = numpy.reshape(a_enhanced,(width*height,a_enhanced.shape[2]))
+    average = numpy.average(a_raveled, axis=0)
+    # Initial guess
+    color_bg = average*1.5
+    color_fg = average*0.5
+    
+    for i in xrange(5):
+        d_bg = a_raveled-color_bg[None,:]
+        d_bg *= d_bg
+        d_bg = numpy.sum(d_bg,1)
+        d_fg = a_raveled-color_fg[None,:]
+        d_fg *= d_fg
+        d_fg = numpy.sum(d_fg,1)
+        fg = d_fg*Background_weight < d_bg
+        color_fg = numpy.median(a_raveled[fg,:],axis=0)
+        color_bg = numpy.median(a_raveled[~fg,:],axis=0)
+        
+    fg = numpy.reshape(fg, (height,width))
+    
+    edge = ndimage.maximum_filter(fg,size=3) != ndimage.minimum_filter(fg,size=3)
+
+    d_fg = numpy.sqrt(numpy.reshape(d_fg, (height,width)))
+    d_bg = numpy.sqrt(numpy.reshape(d_bg, (height,width)))
+    
+    # Staining is taken as foreground pixels that differ markedly from the mean foreground color
+    # (also, they must be darker in the red and green channels)
+    # The background intensity is simply a useful basis for scaling
+    # (should be invariant under changes in brightness) 
+    mag_bg = numpy.average(color_bg)
+    
+    dark_stain = (
+        (d_fg > mag_bg*Dark_stain_level) &
+        (a_enhanced[:,:,0] < color_fg[0]) & 
+        (a_enhanced[:,:,1] < color_fg[1]) &
+        fg
+    )
+    
+    
+    fg_float = fg.astype('float64')
+    green = a[:,:,1] * fg_float
+    green_local_bg = ndimage.gaussian_filter(green, Stain_bg_blur)
+    green_local_bg_divisor = ndimage.gaussian_filter(fg_float, Stain_bg_blur)
+    stain = (
+        (green * green_local_bg_divisor < green_local_bg * Stain_level) &
+        fg 
+    ) 
+        
+    # Detect infection as lumpiness in the green channel (green is most affected by staining)
+    # Must also be within a stained region
+    green_local_bg = ndimage.gaussian_filter(green, Infection_bg_blur)
+    green_local_bg_divisor = ndimage.gaussian_filter(fg_float, Infection_bg_blur)
+    infection = (
+        (green * green_local_bg_divisor < green_local_bg * Infection_level) &
+        stain
+    ) 
+        
+    # Show edges and infection on output image
+    if Show_edges:
+        a_output[edge,:] += 64
+    if Show_stain:
+        a_output[stain,2] += 128
+    if Show_infection:
+        a_output[infection,0] += 128
+        
+        
+    # Hough transform
+    status('hough')
+    
+    x_edge = ndimage.convolve(a_gray,
+        [[-1,0,1],
+         [-2,0,2],
+         [-1,0,1]])    
+    y_edge = ndimage.convolve(a_gray,
+        [[-1,-2,-1],
+         [0,0,0],
+         [1,2,1]])
+    mag = numpy.sqrt(x_edge*x_edge+y_edge*y_edge)
+    x_dir = x_edge / mag
+    y_dir = y_edge / mag
+
+    # Vectorized for enhanced performance and incomprehensibility:
+    ys = []
+    xs = []
+    for y in xrange(height):
+        for x in xrange(width):
+            if edge[y,x]:
+               ys.append(y)
+               xs.append(x)
+    ys = numpy.array(ys)
+    xs = numpy.array(xs)
+    indexes = numpy.arange(len(ys))
+
+    def do_hough(radius):
+        hough = numpy.zeros((height,width), 'float64')
+        
+        oys = (ys + y_dir[ys,xs]*radius + 0.5).astype('int')
+        oxs = (xs + x_dir[ys,xs]*radius + 0.5).astype('int')
+        valid = (
+            (oys >= 0) &
+            (oys < height) &
+            (oxs >= 0) &
+            (oxs < width)
+        )
+        for i in indexes[valid]:
+            hough[oys[i],oxs[i]] += 1.
+        
+        #for y in xrange(height):
+        #    for x in xrange(width):
+        #        if edge[y,x]:
+        #            oy = y + y_dir[y,x]*radius
+        #            ox = x + x_dir[y,x]*radius
+        #            ioy = int(oy+0.5)
+        #            iox = int(ox+0.5)
+        #            if ioy>0 and iox>0 and ioy<height and iox<width:
+        #                hough[ioy,iox] += 1.                        
+        
+        # Blur the transformed image a little
+        # (more blurring allows greater deviation from circularity)
+        # (if you alter this you will also need to alter thresh, below)
+        
+        hough = ndimage.gaussian_filter(hough, 1.0)
+        
+        return hough
+
+    hough = numpy.zeros((height,width),'float64')
+    hough_radius = numpy.zeros((height,width),'int')
+    for i in xrange(Hough_min_radius,Hough_max_radius+1):
+        #print 'Hough radius', i
+        this_hough = do_hough(i)
+        better = this_hough > hough
+        hough_radius[better] = i
+        hough[better] = this_hough[better]
+
+    # Show result of Hough transform in output image
+    #a_output[:,:,:] = hough[:,:,None]*20
+
+    # Find and classify peaks in Hough transform image
+    thresh = 1.5 #Hmm
+    candidates = [ ]
+    for y in xrange(height):
+        for x in xrange(width):
+            if hough[y,x] > thresh:
+                candidates.append((hough[y,x],y,x))
+    candidates.sort(reverse=True)
+    
+    status('classify')
+    
+    file_coords = open(filename_coords,'wt')
+    file_coords.write('Autocount on %dx%d image\n' % (width,height))
+    
+    suppress = numpy.zeros((height,width), 'bool')
+    claim_strength = numpy.zeros((height,width), 'float64') + 1e30
+    cells = [ ] # [(y,x)]
+    n = 0
+    n_infected = 0
+    n_late = 0
+    for _,y,x in candidates:
+        radius = hough_radius[y,x]        
+
+        # Make sure candidate is not near a previously detected cell,
+        # and does not abut the edge of the image        
+        if not suppress[y,x] and \
+           y >= radius and x >= radius and y < height-radius and x < width-radius:
+            coords = clipped_coordinates(0,radius*Cell_inside_radius, y,x,height,width)
+            
+            #Only include foreground
+            this_fg = fg[coords[:,0],coords[:,1]]
+            coords = coords[this_fg]
+            
+            this_strength = ((coords[:,0]-y)**2 + (coords[:,1]-x)**2)
+            claim_strength[coords[:,0],coords[:,1]] = numpy.minimum(
+                claim_strength[coords[:,0],coords[:,1]],
+                this_strength
+            )
+            
+            cells.append((y,x, coords,this_strength))
+
+           
+        # Suppress detection of cells too close to here
+        # (note: we do this irrespective of whether we treated this location as a hit)
+        
+        coords = clipped_coordinates(0,radius*Cell_suppression_radius, y,x,height,width)
+        suppress[coords[:,0],coords[:,1]] = True
+        
+        #for oy, ox in offsets(0, radius*Cell_suppression_radius): #suppression_offsets:
+        #    ny = y+oy
+        #    nx = x+ox
+        #    if ny >= 0 and nx >= 0 and ny < height and nx < width:
+        #        suppress[ny,nx] = True
+
+    for y,x, coords, this_strength in cells:            
+        radius = hough_radius[y,x]
+        
+        if not len(coords): continue # No fg pixels within radius
+        
+        winners = claim_strength[coords[:,0],coords[:,1]] >= this_strength
+        coords = coords[winners]
+        
+        if not len(coords): continue # Did not win any foreground pixels
+                
+        infected_pixels = 0
+        stain_pixels = 0
+        dark_stain_pixels = 0
+        fg_pixels = 0
+        
+        stain_x = 0.0
+        stain_x2 = 0.0
+        stain_y = 0.0
+        stain_y2 = 0.0 
+        
+        for ny, nx in coords:
+            if infection[ny,nx]:
+                infected_pixels += 1
+            if stain[ny,nx]:
+                stain_pixels += 1
+                
+                stain_x += nx
+                stain_x2 += nx*nx
+                stain_y += ny
+                stain_y2 += ny*ny
+            if dark_stain[ny,nx]:
+                dark_stain_pixels += 1
+            fg_pixels += 1
+
+        mass_y = numpy.average(coords[:,0])
+        mass_x = numpy.average(coords[:,1])
+        mass_offset = numpy.sqrt( (mass_y-y)**2 + (mass_x-x)**2 )
+        
+        is_infected = (infected_pixels >= Infection_pixels_required and stain_pixels >= 1)
+         
+        if is_infected:
+            stain_spread = (
+                stain_x2/stain_pixels - (stain_x/stain_pixels)**2 +
+                stain_y2/stain_pixels - (stain_y/stain_pixels)**2
+            )
+            is_infected = stain_spread >= Stain_spread_required
+            
+        is_peculiar = (
+            (radius > Max_cell_radius) or
+            (radius < Min_cell_radius) or
+            (mass_offset > Max_offcenter * radius) or
+            (dark_stain_pixels >= fg_pixels*Max_dark_stain_proportion)
+        )
+        
+        if is_peculiar:
+            color = (0,0,0)
+            file_coords.write('%d,%d,Rejected\n' % (x,y))
+        elif is_infected:
+            n += 1
+            n_infected += 1
+            color = (255,0,0)
+            file_coords.write('%d,%d,Infected\n' % (x,y))
+        else:
+            n += 1
+            color = (0,0,255)
+            file_coords.write('%d,%d,Uninfected\n' % (x,y))
+        
+        # Mark detected cell on output image
+        if Show_computer_marks:
+            set(a_output,y,x,color)
+            for i in xrange(1,3):
+                set(a_output,y-i,x,color)  
+                set(a_output,y+i,x,color)  
+                set(a_output,y,x-i,color)  
+                set(a_output,y,x+i,color)
+
+        if Show_cell_shapes:            
+            a_output[coords[:,0],coords[:,1]] += [[ random.random()*64+32 for i in (0,1,2) ]]
+            
+        
+    
+    file_coords.close()
+    
+    # Save diagnostic output image
+    a_output = numpy.clip(a_output,0,255)
+    i2 = Image.fromarray(a_output.astype('uint8'))    
+    i2.save(filename_out)
+    
+
+
+def read_ac(filename):
+    f = open(filename,'rU')
+    f.readline()
+    n_total = 0
+    n_infected = 0
+    for line in f:
+        parts = line.rstrip().split(',')
+        if parts[2] == 'Uninfected':
+            n_total += 1
+        elif parts[2] == 'Infected':
+            n_total += 1
+            n_infected += 1
+    
+    return n_total, n_infected
+
+
+if __name__ == '__main__':
+    args = sys.argv[1:]
+    
+    while len(args) > 1:
+        name, value = args.pop(0).split('=')
+        value = float(value)
+        assert hasattr(sys.modules[__name__], name), 'Parameter named %s does not exist' % name
+        setattr(sys.modules[__name__], name, value)
+        print name, '=', value
+
+    if len(args) != 1:
+        print Help % sys.argv[0]
+        sys.exit(1)
+
+    if os.path.isfile(args[0]):
+        in_dir, filename = os.path.split(args[0])
+        filenames = [ filename ]
+        is_subprocess = True
+        
+    else:
+        in_dir = args[0]
+        filenames = os.listdir(in_dir)
+        filenames.sort()
+        is_subprocess = False
+    
+    out_dir = os.path.normpath(in_dir) + '_output'
+    if not os.path.exists(out_dir):
+       os.mkdir(out_dir)
+    
+    if not is_subprocess:
+        for filename in filenames:
+            root, ext = os.path.splitext(filename)
+            ac_filename = os.path.join(out_dir, root+'.ac')
+            if os.path.exists(ac_filename): os.unlink(ac_filename)
+    
+    coord_filenames = [ ]
+    
+    n_running = 0
+    
+    for filename in filenames:
+        root, ext = os.path.splitext(filename)
+        if ext.lower() not in ('.bmp','.tif','.tiff','.png'): continue
+        
+        filename_in = os.path.join(in_dir, filename)
+        filename_out = os.path.join(out_dir, root+'.tif')
+        filename_coords = os.path.join(out_dir, root+'.ac')
+        
+        if is_subprocess or N_processors == 1:
+            process(filename_in, filename_out, filename_coords, root)
+        else:
+            while n_running >= N_processors:
+                os.wait()
+                n_running -= 1
+            os.spawnl(os.P_NOWAIT, sys.executable, sys.executable, *(sys.argv[:-1]+[filename_in]))
+            n_running += 1
+        
+        coord_filenames.append((filename_coords, root))
+
+    while n_running:
+        os.wait()
+        n_running -= 1
+
+    if not is_subprocess:        
+        result_file = open(os.path.join(out_dir,'results.txt'),'wt')
+        
+        for filename_coords, root in coord_filenames:
+            n_total, n_infected = read_ac(filename_coords)
+            print >> result_file, root, n_total, n_infected
+            
+        result_file.close()
+    
+