function track(obj, src_ary)
	% main routine for particle tracking
	% perform particle tracking on input images specified in img_ary
	%
	% input:
	%	src_ary		cell array for particle tracking images, formatted as
	%	one of
	%		n_t x n_cameras cell array of n_i x n_j images for particle tracking
	%		n_t x n_cameras cell array of filenames, which can be loaded with imread
	%		1   x n_cameras cell array of filenames of
	%		multipage TIFF files which could be loaded with imread
	%		
	
	%% initialise tracks
	obj.tracks				= [];

	%% main loop
	t_start					= tic;
	for tidx = 1 : obj.n_t
		%% update index of time step
		t_step				= tic;
		obj.tidx			= tidx;
		% get active tracks
		n_active_start		= obj.count();

		%% load images from each camera with preprocessing
		obj.img_ary			= obj.read_image(src_ary, tidx);

		%% search for particles in images
		particles2d			= cell(1, obj.n_cameras);
		n_particles_2d		= zeros(1, obj.n_cameras);
		for c = 1 : obj.n_cameras
			particles2d{c}	= obj.find_particles(obj.img_ary{c});
			n_particles_2d(c)		= numel(particles2d{c});
        end
        
        %% triangulate particles in object space
        [particles3d, matches]= obj.match(particles2d);
        n_particles_3d = numel(particles3d);
        
        %% link predicted tracks to particle centers
        % link active tracks to particles as
        % i_active(links(:,1)) -> particles(links(:,2))
        links       = obj.link(particles3d, tidx);
        n_links     = size(links,1);
        frac_linked = n_links / n_active_start;

        %% create new tracks from unmatched particles
        % new particles should not share image space coordinates 
        % with any particle that has already been linked
        i_prt_used  = matches(links(:,2), :);
        b_used      = false(n_particles_3d, 1);
        for c = 1 : obj.n_cameras
           b_used   = b_used | ismember(matches(:,c), i_prt_used(:,c)); 
        end
        i_unlinked  = find(~b_used).';
		n_unlinked  = numel(i_unlinked);
		new_tracks  = cell(1, n_unlinked);
        for n = 1 : n_unlinked
           new_tracks{n} = obj.new_track(particles3d(i_unlinked(n)), tidx);
		end
		if n_unlinked > 0
			obj.add_active(cell2mat(new_tracks));
		end

		%% predict current set of tracks at this timestep
        % predicts the object space and image space positition at time
        % tidx+1 based on the current position
		n_active_end = obj.count();
		obj.update_velocity(tidx);

		%% remove inactive tracks
		if n_active_end > 0
			% add 
			b_active	 = cat(2, obj.active.life) > 0;
			t_length	 = arrayfun(@(t) nnz(~t.ext), obj.active);
			% save inactive tracks which are above length threshold
			% ensure we save all tracks at final timestep
			b_save       = ~b_active | tidx == obj.n_t;
			b_save		 = b_save & t_length > obj.opts.save_threshold;
			obj.add_track(obj.active(b_save));
			% remove inactive tracks
			obj.select_active(b_active);
		end

		%% debug plot
		if obj.opts.b_debug_plot
			plot_frame(obj, obj.img_ary, particles3d, particles2d, tidx);
		end

		%% report
		if mod(tidx,5) == 1
			fprintf(' Frame  |');
			fprintf(' #p%d    |', 1 : obj.n_cameras);
			fprintf(' Active |');
			fprintf(' Link%%  |');
			%fprintf(' Lost %% |');
			%fprintf(' Left %% |');
			fprintf(' Added  |');
			fprintf(' Took   |');
			fprintf(' Elapsed\n');
		end
		fprintf('%07d |', tidx);							% time step
		fprintf('%7d |', n_particles_2d);					% number of particle centers found
		fprintf('%7d |', n_active_start);					% # active tracks at start
		fprintf('%7.2f |', 100*frac_linked);				% # tracks linked
		%fprintf('%7.2f |', 100*frac_lost);					% fraction of tracks that are lost inside volume
		%fprintf('%7.2f |', 100*frac_left);					% fraction of tracks that leave volume
		fprintf('%7d |', n_unlinked);						% # of tracks added from residuals
		fprintf('%6.3fs |', toc(t_step));					% time taken for this step
		fprintf(' %dh %dm %ds\n', hms(toc(t_start)));		% time taken so far
    end
end