function im_filt = image_preprocess(im, filters)
	% apply preprocessing to image
	% by sequentially applying filters described by the struct vector
	% <filters>
	%
	% each element has at least one member, 'type', which determines what
	% type of filter is to be applied. it is a string which selects from a
	% number of different available filters
	%
	% Type/member	Default	Description
	% ----------------------------------------------------------------------
	% gaussian					gaussian filter 
	%	.size			[3 3]		filter size
	%	.sigma		1			filter sigma
	%
	% ssmin						subtract sliding minimum
	%	.size			[7 7]		filter size, must be odd
	%
	% ssbg						subtract background
	%	.bg						background image, must be same size as im
	%
	% norm						normalisation filter
	%	.size			[7 7]		filter size, must be odd
	%	.max_gain	1			maximum gain for normalisation filter
	%
	
	%% iteratively apply filters
	im_filt				= single(im);
	n_filters			= length(filters);
	
	for i = 1 : n_filters
		filter	= filters{i};
		if strcmp(filter.type, 'gaussian')
			%% gaussian filter
			sz			= [3 3];
			sigma		= 1;
			if isfield(filter, 'size'), sz = filter.size; end
			if isfield(filter, 'sigma'), sigma = filter.sigma; end
			G			= fspecial('gaussian', sz, sigma);
			im_filt		= filter2(G, im_filt);
		elseif strcmp(filter.type, 'custom')
			%% custom filter specified with fspecial
			G			= fspecial('gaussian', [3 3]);
			if isfield(filter, 'kernel'), G = filter.kernel; end
			im_filt		= filter2(G, im_filt);
		elseif strcmp(filter.type, 'ssmin')
			%% subtract sliding minimum
			sz			= [7 7];
			if isfield(filter, 'size'), sz = filter.size; end
			sz			= sz + mod(sz+1,2);
            minval      = medfilt2(im_filt, [3 3]); % median filter first to avoid noise
            % sliding minimum filter to get local minimum value
			minval		= minfiltnd(minval, sz);
            % box filter to smooth background image
            minval      = convn(minval, ones(1, sz(1))/sz(1), 'same');
            minval      = convn(minval, ones(sz(2), 1)/sz(2), 'same');
            % clipping operation to enforce positivity
			im_filt		= max(0, im_filt - minval);
		elseif strcmp(filter.type, 'lmax')
			%% local maximum
			sz			= [7 7];
			if isfield(filter, 'size'), sz = filter.size; end
			sz			= sz + mod(sz+1,2);
			im_filt		= maxfiltnd(im_filt, sz);
		elseif strcmp(filter.type, 'ssbg')
			%% subtract background
			bg				= zeros(size(im));
			if isfield(filter, 'bg'), bg = filter.bg; end
			im_filt			= max(0, im - bg);
		elseif strcmp(filter.type, 'levelize')
			%% levelize
			white			= ones(size(im));
			if isfield(filter, 'white'), white = filter.white; end
			im_filt			= im_filt ./ white;
		elseif strcmp(filter.type, 'median')
			%% median filtering
			sz				= [5 5];
			if isfield(filter, 'size'), sz = filter.size; end
			im_filt			= medfilt2(im_filt, sz, 'symmetric');
		elseif strcmp(filter.type, 'norm')
			%% normalisation filter
			sz				= [7 7];
			max_gain		= 1;
			
			if isfield(filter, 'size'), sz = filter.size; end
			if isfield(filter, 'max_gain'), max_gain = filter.max_gain; end
			sz				= sz + mod(sz+1,2);
			
			[minv, maxv]	= minmaxfiltnd(im_filt, sz);
			im_filt			= (im_filt - minv) ./ max(maxv-minv, 1/max_gain);
        elseif strcmp(filter.type, 'norm2')
			%% normalisation filter
			sz				= [7 7];
			max_gain		= 1;
			
			if isfield(filter, 'size'), sz = filter.size; end
			if isfield(filter, 'max_gain'), max_gain = filter.max_gain; end
			sz				= sz + mod(sz+1,2);
			
			[minv, maxv]	= minmaxfiltnd(im_filt, sz);
            % box filter local maximum
            maxv            = convn(maxv, ones(1, sz(1))/sz(1), 'same');
            maxv            = convn(maxv, ones(sz(2), 1)/sz(2), 'same');
            % box filter local minimum
            minv            = convn(minv, ones(1, sz(1))/sz(1), 'same');
            minv            = convn(minv, ones(sz(2), 1)/sz(2), 'same');
			im_filt			= (im_filt - minv) ./ max(maxv-minv, 1/max_gain);
        elseif strcmp(filter.type, 'mednorm')
            %% normalisation filter with median filtering
			sz				= [7 7];
			max_gain		= 1;
			
			if isfield(filter, 'size'), sz = filter.size; end
			if isfield(filter, 'max_gain'), max_gain = filter.max_gain; end
			sz				= sz + mod(sz+1,2);
			
            % local maximum
			maxv            = minmaxfiltnd(im_filt, sz);
            % box filter local maximum
            maxv            = convn(maxv, ones(1, sz(1))/sz(1), 'same');
            maxv            = convn(maxv, ones(sz(2), 1)/sz(2), 'same');
			im_filt			= max(0,im_filt) ./ max(maxv, 1/max_gain);
		elseif strcmp(filter.type, 'clip')
			%% clip filter, with automatic 
			if isfield(filter, 'threshold')
				% manually specified threshold
				threshold	= filter.threshold;
				im_filt		= max(threshold(1), min(threshold(2), im_filt));
			else
				% algorithmically specified threshold
				im_med			= median(im_filt(:));
				im_std			= std(im_filt(:));
				n				= 2;
				if isfield(filter, 'n'), n = filter.n; end
				thresh			= im_med + n*im_std;
				im_filt			= min(im_filt, thresh);
			end
        elseif strcmp(filter.type, 'transpose')
            im_filt         = im_filt.';
		elseif strcmp(filter.type, 'invert')
			offset			= 0;
			if isfield(filter, 'offset'), offset = filter.offset; end
			im_filt			= offset - im_filt;
		elseif strcmp(filter.type, 'null')
			%% null filter, do nothing
			im_filt			= im_filt;
		else
			error('unrecognised filter type %s', filter.type);
		end
	end
end
	