{ Routines to Locate Bright Spots in Images Copyright (C) 2004-2009 Kevan Hashemi, hashemi@brandeis.edu, Brandeis 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. } unit spot; interface uses utils,images,transforms,image_manip; const spot_missing_string='-1 -1 0 0 0 0'; spot_missing_bounds_string='-1 -1 -1 -1'; spot_decreasing_intensity=1; spot_increasing_x=2; spot_increasing_y=3; spot_decreasing_x=4; spot_decreasing_y=5; spot_use_centroid=1; spot_use_ellipse=2; spot_use_vertical_line=3; type spot_type=record valid:boolean; color_code:integer;{color code used to mark pixels in overlay} x:real;{location in x (um) or orientation (mrad)} y:real;{location in y (um) or orientation (mrad)} accuracy:real;{um estimate of position accuracy} threshold:integer;{intensity threshold for noise and background} position_xy:xy_point_type;{position in image coordinates, units um} position_ij:ij_point_type;{position in ccd coordinates, units pixels} bounds:ij_rectangle_type;{boundries enclosing spot} num_pixels:integer;{number of pixels above threshold in bounds} sum_intensity:integer;{sum of above-threshold intensities of pixels in bounds} max_intensity:integer;{max intensity in bounds} min_intensity:integer;{min intensity in bounds} pixel_size_um:real;{width of pixels in um} ellipse:xy_ellipse_type;{ellipse for elliptical fitting} end; spot_ptr_type=^spot_type; spot_list_type(length:integer)=record ip:image_ptr_type;{for convenience of analysis} current_stack_depth:integer;{to avoid stack overflow} num_valid_spots:integer;{spots 1 to num_valid_spots are valid} num_requested_spots:integer;{spots looked for by image analysis} spots:array [1..length] of spot_type; end; spot_list_ptr_type=^spot_list_type; procedure spot_centroid(ip:image_ptr_type;var spot:spot_type); procedure spot_ellipse(ip:image_ptr_type;var spot:spot_type); procedure spot_vertical_line(ip:image_ptr_type;var spot:spot_type); procedure spot_list_display_bounds(ip:image_ptr_type;slp:spot_list_ptr_type; color:integer); procedure spot_list_display_vertical_lines(ip:image_ptr_type;slp:spot_list_ptr_type; color:integer); procedure spot_list_display_crosses(ip:image_ptr_type;slp:spot_list_ptr_type; color:integer); procedure spot_list_display_ellipses(ip:image_ptr_type;slp:spot_list_ptr_type; color:integer); function spot_list_find(ip:image_ptr_type; num_spots:integer;command:short_string;pixel_size_um:real):spot_list_ptr_type; procedure spot_list_sort(slp:spot_list_ptr_type; sort_code:integer); function new_spot_list_ptr(length:integer):spot_list_ptr_type; procedure dispose_spot_list_ptr(slp:spot_list_ptr_type); function string_from_spot(c:spot_type):short_string; function string_from_spot_list(slp:spot_list_ptr_type):short_string; function bounds_string_from_spot_list(slp:spot_list_ptr_type):short_string; function intensity_string_from_spot_list(slp:spot_list_ptr_type):short_string; implementation const {memory limits} max_stack_depth=10000; {found 20000 crashes XP and Vista} max_num_spots=100; {only rare, over-exposed images have more} { new_spot_list_ptr creates a new spot list with "length" entries. } function new_spot_list_ptr(length:integer):spot_list_ptr_type; var slp:spot_list_ptr_type; spot_num:integer; begin if length<=0 then length:=1; new(slp,length); if slp=nil then exit; inc_num_outstanding_ptrs(sizeof(slp^),CurrentRoutineName); with slp^ do begin num_valid_spots:=0; num_requested_spots:=0; current_stack_depth:=0; for spot_num:=1 to length do spots[spot_num].valid:=false; end; new_spot_list_ptr:=slp; end; { dispose_spot_list_ptr disposes of a spot list. } procedure dispose_spot_list_ptr(slp:spot_list_ptr_type); begin if slp=nil then exit; dec_num_outstanding_ptrs(sizeof(slp^),CurrentRoutineName); dispose(slp); end; { spot_centroid subtracts a threshold from the intensity within the spot bounds and determines the intensity centroid of the resulting pixels. The routine ignores pixels whose intensity is below threshold. The centroid position is in image coordinates, with units microns. The spot sensitivity is in microns per threshold count calculated at the specified threshold. The routine takes as input a spot_type and alters the x, y, num_pixels, sum_intensity, max_intensity, min_intensity, and valid fields to represent the result of the centroid-finding calculation. } procedure spot_centroid(ip:image_ptr_type;var spot:spot_type); const threshold_step=1; var i,j,sum,max,min,sum_i,sum_j,step_sum,step_sum_i,step_sum_j:integer; count,net_intensity:integer; step_position_xy:xy_point_type; begin if not spot.valid then exit; if ip=nil then exit; count:=0; sum:=0;sum_i:=0;sum_j:=0; max:=black_intensity;min:=white_intensity; step_sum:=0;step_sum_i:=0;step_sum_j:=0; with spot,spot.bounds do begin for j:=top to bottom do begin for i:=left to right do begin net_intensity:=ip^.intensity[j,i]-threshold; if net_intensity>0 then begin inc(count); sum:=sum+net_intensity; sum_i:=sum_i+i*net_intensity; sum_j:=sum_j+j*net_intensity; end; net_intensity:=ip^.intensity[j,i]-threshold-threshold_step; if net_intensity>0 then begin step_sum:=step_sum+net_intensity; step_sum_i:=step_sum_i+i*net_intensity; step_sum_j:=step_sum_j+j*net_intensity; end; if ip^.intensity[j,i]>max then max:=ip^.intensity[j,i]; if ip^.intensity[j,i]0 then accuracy:=residual*pixel_size_um/sqrt(pixel_num) else accuracy:=0; end; end; { spot_ellipse fits an ellipse to the border of a spot, and returns the coordinates of the center of the ellipse. It fills in the fields of the ellipse record in the spot. } procedure spot_ellipse(ip:image_ptr_type;var spot:spot_type); const num_parameters=5; max_iterations=40; bounds_extra=5; { border_match returns true iff point p is on the border of the specified ellipse, and is also on the border of the spot defined by spot.threshold. } function border_match(p:ij_point_type;e:xy_ellipse_type):boolean; const image_border=2; var i_min,i_max,j_min,j_max,i,j:integer; s:real; q:ij_point_type; on_image,on_fit:boolean; begin on_image:=false; on_fit:=false; s:=xy_separation(i_from_c(p),e.a)+xy_separation(i_from_c(p),e.b); if (s<=e.axis_length) and (s>=e.axis_length-image_border) and (ip^.intensity[p.j,p.i]>spot.threshold) then begin with ip^.analysis_bounds do begin if p.i>left then i_min:=p.i-1 else i_min:=left; if p.itop then j_min:=p.j-1 else j_min:=top; if p.je.axis_length then on_fit:=true; if (ip^.intensity[q.j,q.i]<=spot.threshold) then on_image:=true; end; end; end; border_match:=on_fit and on_image; end; { The error function we provide for the simplex fitter counts the number of pixels in the image analysis bounds that are on the border of the spot in the image and also on the border of the ellipse defined by vertex "v". The error is the negative of the number of border-coincident pixels. We took the idea for border-coincidence from "Robust Ellipse Detection by Fitting Randomly Selected Edge Patches" by Watcharin Kaewapichai, and Pakorn Kaewtrakulpong. We don't use their method, but we take from it the one idea of matching edge pixels to determine fitness of the ellipse. } function error(v:simplex_vertex_type):real; var fitness:real; p:ij_point_type; pp:xy_point_type; e:xy_ellipse_type; range,score:real; begin fitness:=0; with e do begin a.x:=v[1]; a.y:=v[2]; b.x:=v[3]; b.y:=v[4]; axis_length:=v[5]; end; for p.i:=ip^.analysis_bounds.left to ip^.analysis_bounds.right do for p.j:=ip^.analysis_bounds.top to ip^.analysis_bounds.bottom do if border_match(p,e) then fitness:=fitness+1; error:=-fitness; end; var simplex:simplex_type(num_parameters); saved_bounds:ij_rectangle_type; i:integer; begin if not spot.valid then exit; if ip=nil then exit; { We start with an ellipse that fills the spot bounds. } spot.ellipse:=xy_rectangle_ellipse(i_from_c_rectangle(spot.bounds)); { Set up the simplex we use with the simplex fitter. We assign the initial ellipse fields to the first vertex of the simplex, and construct the simplex from there. } with simplex,spot.ellipse do begin vertices[1,1]:=a.x; vertices[1,2]:=a.y; vertices[1,3]:=b.x; vertices[1,4]:=b.y; vertices[1,5]:=axis_length; construct_size:=1; done_counter:=0; max_done_counter:=2; end; simplex_construct(simplex,error); { Reduce the image's analysis boundaries to the small boundary around the spot, but add bounds_extra pixels on all sides to help the fitter. } saved_bounds:=ip^.analysis_bounds; ip^.analysis_bounds:=spot.bounds; with ip^.analysis_bounds do begin left:=left-bounds_extra; if leftsaved_bounds.right then right:=saved_bounds.right; top:=top-bounds_extra; if topsaved_bounds.bottom then bottom:=saved_bounds.bottom; end; { Apply the simplex fitter, one step at a time until it's done or we have exceeded the maximum number of iterations. } i:=0; repeat simplex_step(simplex,error); inc(i); until (simplex.done_counter>=simplex.max_done_counter) or (i>max_iterations); { Restore the image bounds. } ip^.analysis_bounds:=saved_bounds; { Fill the spot ellipse with the fitted values, determine the ellipse center and adjust the spot position accordingly. We estimate the fitting error by dividing the pixel size by the square root of the number of border pixels. } with simplex,spot do begin ellipse.a.x:=vertices[1,1]; ellipse.a.y:=vertices[1,2]; ellipse.b.x:=vertices[1,3]; ellipse.b.y:=vertices[1,4]; ellipse.axis_length:=vertices[1,5]; position_xy:=xy_scale(xy_sum(ellipse.a,ellipse.b),one_half); position_ij:=c_from_i(position_xy); x:=position_xy.x*pixel_size_um; y:=position_xy.y*pixel_size_um; accuracy:=pixel_size_um/sqrt(-error(vertices[1])); end; end; { string_from_spot returns a string expressing the most prominent elements of a spot record. } function string_from_spot(c:spot_type):short_string; var s:short_string; begin if c.valid then with c,c.bounds do writestr(s,x:4:2,' ',y:4:2,' ', num_pixels:1,' ',max_intensity:1,' ', accuracy:5:3,' ',threshold:1) else s:=spot_missing_string; string_from_spot:=s; end; { string_from_spot_list returns a string made by concatinating the string_from_spots for all the spots in the list. } function string_from_spot_list(slp:spot_list_ptr_type):short_string; var spot_num:integer; s:short_string; begin if slp=nil then exit; s:=''; for spot_num:=1 to slp^.num_requested_spots do begin writestr(s,s,string_from_spot(slp^.spots[spot_num])); if spot_num slp^.spots[spot_num+1].x; spot_increasing_y: swap_now:= slp^.spots[spot_num].y > slp^.spots[spot_num+1].y; spot_decreasing_x: swap_now:= slp^.spots[spot_num].x < slp^.spots[spot_num+1].x; spot_decreasing_y: swap_now:= slp^.spots[spot_num].y < slp^.spots[spot_num+1].y; otherwise swap_now:=false; end; if swap_now then begin temp_spot:=slp^.spots[spot_num]; slp^.spots[spot_num]:=slp^.spots[spot_num+1]; slp^.spots[spot_num+1]:=temp_spot; no_swaps:=false; inc(num_swaps); end; end; until no_swaps or (num_swaps>max_num_swaps); end; { do_pixel takes as input a spot list pointer and the coordinates of a pixel in an image. The spot list provides a pointer to the image and a list of spots. The last valid spot in the list is the one do_pixel operates upon. do_pixel checks to see if pixel (i,j) in the image has intensity above the threshold specified in the spot record. Pixel (i,j) is the pixel in the i'th column and j'th row. If this pixel is above threshold and has not yet been marked as belonging to any spot, do_pixel adds the pixel to the spot by setting the pixel's image overlay value equal to the spot's color code. After adding the pixel to the spot, do_pixel calls itself upon all the pixel's neighbors. These neighbors will always include the pixels above, below, to the left, and to the right of the pixel. If kitty_corner is true, the neighbors will also include the four other diagonal neighbor pixels. By these recursive calls, if the threshold is lower than the average image intensity, we can end up with spots that contain an entire image, which means our recursive calling of this routine will run the stack pointer up by a megabyte or more. We protect against memory access violation by limiting the depth of our do_pixel recursion with the max_stack_depth constant. } procedure do_pixel(slp:spot_list_ptr_type;j,i:integer); const kitty_corners=false; var net_intensity:integer; begin if slp=nil then exit; with slp^ do begin if current_stack_depth>max_stack_depth then exit; inc(current_stack_depth); with ip^,ip^.analysis_bounds,spots[num_valid_spots] do begin if (intensity[j,i]>threshold) and (overlay[j,i]=clear_color) then begin overlay[j,i]:=color_code; if j>bounds.bottom then bounds.bottom:=j; if jbounds.right then bounds.right:=i; if itop then do_pixel(slp,j-1,i); if i>left then do_pixel(slp,j,i-1); if ileft then do_pixel(slp,j+1,i-1); if itop then begin if i>left then do_pixel(slp,j-1,i-1); if imax_num_spots then begin report_error('num_spots>max_num_spots in '+CurrentRoutineName+'.'); exit; end; { Decode the command string. First we read the threshold, then we look for a valid threshold qualifier. We read the minimum number of pixels in each spot and the maximum eccentricity. } threshold:=read_integer(command); word:=read_word(command); if word='%' then threshold:= round((1-threshold/percent_unit)*image_minimum(ip) +threshold/percent_unit*image_maximum(ip)) else if word='#' then threshold:= round((1-threshold/percent_unit)*image_average(ip) +threshold/percent_unit*image_maximum(ip)) else if word='*' then threshold:=threshold else if word='$' then threshold:=round(image_average(ip)+threshold) else command:=word+' '+command; min_pixels:=read_integer(command); if min_pixels<1 then min_pixels:=1; max_eccentricity:=read_real(command); if max_eccentricity<1 then max_eccentricity:=0; { It's okay to pass empty strings to read_integer and read_real, but if we passed a non-numeric, non-empty string, these routines will have recorded and error, and we should now quit with this error before we run into trouble. } if error_string<>'' then exit; { Assign a new spot list to hold our maximum number of spots. } slp:=new_spot_list_ptr(max_num_spots); if slp=nil then begin report_error('Failed to allocate for slp in '+CurrentRoutineName+'.'); exit; end; slp^.ip:=ip; { Clear the image overlay so we can use it to mark pixels as belonging to the spots. } clear_overlay(ip); { Proceed through the entire image, calling do_pixel each time we find a pixel unmarked in the overlay whose intensity is above the threshold. Each time we call do_pixel we assume the routine gathers all the light spot's pixels together. } with ip^,ip^.analysis_bounds do begin for j:=top to bottom do begin for i:=left to right do begin if (intensity[j,i]>threshold) and (overlay[j,i]=clear_color) then begin with slp^ do begin if num_valid_spots=length then break; inc(num_valid_spots); current_stack_depth:=0; spots[num_valid_spots].color_code:= overlay_color_from_integer(num_valid_spots); spots[num_valid_spots].threshold:=threshold; spots[num_valid_spots].pixel_size_um:=pixel_size_um; with spots[num_valid_spots] do begin bounds.left:=ip^.analysis_bounds.right; bounds.right:=ip^.analysis_bounds.left; bounds.top:=ip^.analysis_bounds.bottom; bounds.bottom:=ip^.analysis_bounds.top; end; do_pixel(slp,j,i); spots[num_valid_spots].valid:=true; spot_centroid(ip,spots[num_valid_spots]); with spots[num_valid_spots] do begin if num_pixels0) then with bounds do if ((right-left)/(bottom-top)>max_eccentricity) or ((right-left)/(bottom-top)<1/max_eccentricity) then valid:=false; if not valid then begin for jj:=bounds.top to bounds.bottom do for ii:=bounds.left to bounds.right do if ip^.overlay[jj,ii]=color_code then ip^.overlay[jj,ii]:=white_color; dec(num_valid_spots); end; end; end; end; end; end; end; { Sort the spots in order of decreasing intensity. } spot_list_sort(slp,spot_decreasing_intensity); { If we have more than enough valid spots, remove the excess by setting num_valid_spots equal to the number of requested spots. } if slp^.num_valid_spots>num_spots then slp^.num_valid_spots:=num_spots; { Record the number of spots requested in the spot list. } slp^.num_requested_spots:=num_spots; { Return the list. The list may have fewer than num_spots valid entries. The routine that uses the spot list must check the valid flag on each spot before using it. } spot_list_find:=slp; end; { spot_list_display_bounds displays the bounds of a list of spots. } procedure spot_list_display_bounds(ip:image_ptr_type;slp:spot_list_ptr_type; color:integer); const min_square_width=10; extent=min_square_width div 2; var spot_num:integer; r:ij_rectangle_type; begin if slp=nil then exit; for spot_num:=1 to slp^.num_valid_spots do begin r:=slp^.spots[spot_num].bounds; with r,slp^.spots[spot_num].position_ij do begin if abs(right-left)