function D=bwdistsc(bw,aspect) % D=BWDISTSC(BW,ASPECT) % BWDISTSC computes Euclidean distance transform of the binary 3D image % BW. For each pixel in BW, the distance transform assignes a number % that is the distance between that pixel and the nearest nonzero pixel % of BW. BW may be a single 2D image, 3D array or a cell array of % 2D slices. ASPECT is 3-component vector defining aspect ratio in % the dataset BW. If ASPECT is not specified, isotropic aspect % ratio [1 1 1] is assumed. % % BWDISTSC uses fast optimized scanning algorithm and cell-arrays to % represent internal data, so that it is less demanding to physical % memory. In many cases BWDISTSC is actually faster than MATLAB's % optimized kd-tree algorithm used for Euclidean distance % transform in 3D. % % BWDISTSC tries to use MATLAB bwdist for 2D scans if possible, which % is significantly faster. Otherwise BWDISTSC uses internal algorithm % to perform 2D scans. % % Yuriy Mishchenko JFRC HHMI Chklovskii Lab JUL 2007 % This code is free for use or modifications, just please give credit % where appropriate. And if you modify code or fix bugs, please drop % me a message at gmyuriy@hotmail.com. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Scan algorithms below use following Lema: % % LEMA: let F(X,z) be lower envelope of a family of parabola: % % F(X,z)=min_{i} [G_i(X)+(z-k_i)^2]; % % and let H_k(X,z)=A(X)+(z-k)^2 be a parabola. % % Then for H_k(X,z)==F(X,z) at each X there exist at most % % two solutions k1dtmp; D1(idx(L))=dtmp(L); % points which are still above the envelop but ik==i0, % will not get any better, so fix them as well L=L | (ik==i0(~L0)); % all other points are not OK, need new starting point: % starting point should be at least below parabola % beating us at current choice of i0 % solve quadratic equation to find where this happens ik=(ik-i); di=(D1(idx(~L))-dtmp(~L))./ik(~L)/2/aspect(2)^2; % should select next highest index to the equality di=fix(di)+sign(di); % the new starting points idx=find(~L0); i0(idx(~L))=i0(idx(~L))+di; % update L0 to indicate which points we've fixed L0(~L0)=L; L0(idx(~L))=(di==0); % points that went out of boundaries can't get better; % fix them as well idx=idx(~L); idx=idx((i0(idx)<1) | (i0(idx)>shape(2))); i0(idx)=i; L0(idx)=1; end % reduce out trivial points DXY(idx)0) | (i0==i); idx=idx(L); % these will keep track along which X should % keep updating distances map_lower=L; map_upper=L; idx_lower=idx; idx_upper=idx; % set trivial pixels D==0 in line #i: % this has to be done b/s we manually discarded them from L0 D1(d0==0,i)=0; % scan from starting points for each X,i0 in increments of 1 di=0; % distance from current y-line eols=2; % end-of-line-scan flag totlen=prod(shape(1:2)); while(eols) eols=2; di=di+1; % select X which can be updated for di<0; % i.e. X which had been below envelop all way till now if(~isempty(idx_lower)) % shift y by -1 idx_lower=idx_lower-shape(1); % prevent index dropping below 1st L=(idx_lower>=1); map_lower(map_lower)=L; idx_lower=idx_lower(L); if(~isempty(idx_lower)) dtmp=d0(map_lower)+... aspect(2)^2*(i0(map_lower)-di-i).^2; % these pixels are to be updated with i0-di L=D1(idx_lower)>dtmp & DXY(idx_lower)>0; map_lower(map_lower)=L; idx_lower=idx_lower(L); D1(idx_lower)=dtmp(L); DK(idx_lower)=i; end else % if this is empty, get ready to quit eols=eols-1; end % select X which can be updated for di>0; % i.e. X which had been below envelop all way till now if(~isempty(idx_upper)) % shift y by +1 idx_upper=idx_upper+shape(1); % prevent index from going over array limits L=(idx_upper<=totlen); map_upper(map_upper)=L; idx_upper=idx_upper(L); if(~isempty(idx_upper)) dtmp=d0(map_upper)+... aspect(2)^2*(i0(map_upper)+di-i).^2; % check which pixels are to be updated with i0+di L=D1(idx_upper)>dtmp & DXY(idx_upper)>0; map_upper(map_upper)=L; idx_upper=idx_upper(L); D1(idx_upper)=dtmp(L); DK(idx_upper)=i; end else % if this is empty, get ready to quit eols=eols-1; end end end end D{k}=D1; end %%%%%%%%%%%%% scan along Z %%%%%%%%%%%%%%%% % this will be the envelop: % envelop has to be initialized at Inf to ensure single direction of scan, % otherwise may end up in infinite loop when trying to find starting point D1=cell(size(D)); for k=1:shape(3) D1{k}=repmat(Inf,shape(1:2)); end % these will be the references to parabolas defining the envelop DK=cell(size(D)); for k=1:shape(3) DK{k}=repmat(Inf,shape(1:2)); end % start building the envelope for k=1:shape(3) % need to select starting point for each X: % * starting point should be below current envelop % * k0==k is not necessarily a starting point % * there may be no starting point % k0 is the starting points for each XY: k0(XY) is the first % z-index such that parabola from line k is below the envelop % initial starting point guess is current slice k0=repmat(k,shape(1:2)); % L0 indicates which starting points had been fixed L0=isinf(D{k}) | (D{k}==0); idxtot=find(~L0); while(~isempty(idxtot)) % because of using cells need to explicitly scan in Z % to avoid repetitious searches in k0, parse first ss=getregions(k0(idxtot)); sslen=length(ss); for kk=1:sslen % these are starting points @kk which had not been set idx=idxtot(ss(kk).PixelIdxList); % reduce out trivial points (D==0) if(kk~=k) L=(D{kk}(idx)==0); L0(idx)=L; idx=idx(~L); end if(isempty(idx)) continue; end % these are currently best parabolas for slice kk ik=DK{kk}(idx); % these are new values for slice kk from parabola from k dtmp=D{k}(idx)+aspect(3)^2*(kk-k)^2; % these points are OK - below current envelop L=D1{kk}(idx)>dtmp; D1{kk}(idx(L))=dtmp(L); % these points are not OK, but since ik==k0 % can't get any better L=L | (ik==kk); % all other points are not OK, need new starting point: % starting point should be at least below parabola % beating us at current choice of k0 % solve quadratic equation to find where this happens ik=(ik-k); dk=(D1{kk}(idx(~L))-dtmp(~L))./ik(~L)/2/aspect(3)^2; dk=fix(dk)+sign(dk); k0(idx(~L))=k0(idx(~L))+dk; % update starting points that had been set L0(idx)=L; L0(idx(~L))=(dk==0); % points that went out of boundaries can't get better idx=idx(~L); idx=idx((k0(idx)<1) | (k0(idx)>shape(3))); L0(idx)=1; k0(idx)=k; end idxtot=find(~L0); end % map_lower/map_upper keeps track of which pixels can be yet updated % with new distance, i.e. all such XY that had been below envelop for % all dk up to now for dk<0/dk>0 respectively map_lower=true(shape(1:2)); map_upper=true(shape(1:2)); % parse different values in k0 to avoid repetitious searching below ss=getregions(k0); sslen=length(ss); % reduce out trivially faulty starting points for kk=1:sslen if(kk==k) continue; end idx=ss(kk).PixelIdxList; L=D{kk}(idx)>D{k}(idx); map_lower(idx)=L; map_upper(idx)=L; end % these are maintained to keep fast track of whether maps are empty idx_lower=find(map_lower); idx_upper=find(map_upper); % set trivial pixels D==0 in slice k: % this has to be done b/s we manually discarded them from L0 D1{k}(D{k}==0)=0; % scan away from starting points in increments of 1 dk=0; % distance from current xy-slice eols=2; % end-of-scan flag while(eols) eols=2; dk=dk+1; if(~isempty(idx_lower)) % prevent index from going over the boundaries L=(k0(map_lower)-dk>=1); map_lower(map_lower)=L; % need to explicitly scan in Z because of using cell-arrays for kk=1:sslen-dk % get all XY such that k0-dk==kk idx=ss(kk+dk).PixelIdxList; L=map_lower(idx); idx=idx(L); if(~isempty(idx)) dtmp=D{k}(idx)+aspect(3)^2*(kk-k)^2; % these pixels are to be updated with k0-dk L=D1{kk}(idx)>dtmp & D{kk}(idx)>0; map_lower(idx)=L; D1{kk}(idx(L))=dtmp(L); % ridiculously, but this is faster than % direct assignment dtmp=idx(L); dtmp(:)=k; DK{kk}(idx(L))=k; end end idx_lower=idx_lower(map_lower(idx_lower)); else eols=eols-1; end if(~isempty(idx_upper)) % prevent index from going over the boundaries L=(k0(map_upper)+dk<=shape(3)); map_upper(map_upper)=L; % need to explicitly scan in Z because of using cell-arrays for kk=dk+1:min(shape(3),sslen+dk) % get all XY such that k0+dk==kk idx=ss(kk-dk).PixelIdxList; L=map_upper(idx); idx=idx(L); if(~isempty(idx)) dtmp=D{k}(idx)+aspect(3)^2*(kk-k)^2; % these pixels are to be updated with k0+dk L=D1{kk}(idx)>dtmp & D{kk}(idx)>0; map_upper(idx)=L; D1{kk}(idx(L))=dtmp(L); dtmp=idx(L); dtmp(:)=k; DK{kk}(idx(L))=dtmp; end end idx_upper=idx_upper(map_upper(idx_upper)); else eols=eols-1; end end end % the answer if(iscell(bw)) D=cell(size(bw)); for k=1:shape(3) D{k}=sqrt(D1{k}); end else D=zeros(shape); for k=1:shape(3) D(:,:,k)=sqrt(D1{k}); end end function s=getregions(map) % this function is replacer for regionprops(map,'PixelIdxList'); % it produces the list of different values along with list of % indexes of pixels in map with these values; 's' is struct-array % such that s(i).PixelIdxList contains list of pixels in map % with value i. % enable using regionprops if available, faster on 7.3 fregionprops=1; % version control for using regionprops v=version; v=str2num(v(1:3)); fregionprops=fregionprops & v>=7.3; % in later matlab regionprops is actually faster than this code if(exist('regionprops') & fregionprops) s=regionprops(map,'PixelIdxList'); return end idx=(1:prod(size(map)))'; dtmp=double(map(:)); [dtmp,ind]=sort(dtmp); idx=idx(ind); ind=[0;find([diff(dtmp(:));1])]; s=[]; for i=2:length(ind) if(dtmp(ind(i)))==0 continue; end s(dtmp(ind(i))).PixelIdxList=idx(ind(i-1)+1:ind(i)); end