Skip to content

Commit 7563892

Browse files
authored
Merge pull request #151 from AdaptiveParticles/autoparameters
Add automatic parameter tuning based on minimum cross-entropy thresholding
2 parents 996b205 + bbac595 commit 7563892

File tree

2 files changed

+139
-64
lines changed

2 files changed

+139
-64
lines changed

src/algorithm/APRConverter.hpp

Lines changed: 129 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ class APRConverter {
113113
template<typename T,typename S>
114114
void autoParameters(const PixelData<T> &localIntensityScale,const PixelData<S> &grad);
115115

116+
template<typename T,typename S>
117+
void autoParametersLiEntropy(const PixelData<T> &image, const PixelData<T> &localIntensityScale, const PixelData<S> &grad);
118+
116119
template<typename T>
117120
bool check_input_dimensions(PixelData<T> &input_image);
118121

@@ -459,7 +462,11 @@ inline bool APRConverter<ImageType>::get_apr(APR &aAPR, PixelData<T>& input_imag
459462
computation_timer.start_timer("apply_parameters");
460463

461464
if( par.auto_parameters ) {
462-
autoParameters(local_scale_temp,grad_temp);
465+
method_timer.start_timer("autoParameters");
466+
// autoParameters(local_scale_temp,grad_temp);
467+
autoParametersLiEntropy(local_scale_temp2, local_scale_temp, grad_temp);
468+
aAPR.parameters = par;
469+
method_timer.stop_timer();
463470
}
464471

465472
applyParameters(aAPR,par);
@@ -587,92 +594,152 @@ inline bool APRConverter<ImageType>::get_apr(APR &aAPR, PixelData<T>& input_imag
587594
return true;
588595
}
589596

590-
template<typename ImageType>
591-
template<typename T,typename S>
592-
void APRConverter<ImageType>::autoParameters(const PixelData<T> &localIntensityScale,const PixelData<S> &grad){
593-
/*
594-
* Assumes a dark background. Please use the Python interactive parameter selection for more detailed approaches.
595-
*
596-
* Finds the flatest (1% of the image) and estimates the noise level in the gradient and sets the grad threshold from that.
597-
*/
597+
template<typename T>
598+
void compute_means(const std::vector<T>& data, float threshold, float& mean_back, float& mean_fore) {
599+
float sum_fore=0.f, sum_back=0.f;
600+
size_t count_fore=0, count_back=0;
598601

602+
#ifdef HAVE_OPENMP
603+
#pragma omp parallel for default(none) shared(data, threshold) reduction(+:sum_fore, sum_back, count_fore, count_back)
604+
#endif
605+
for(size_t idx = 0; idx < data.size(); ++idx) {
606+
if(data[idx] > threshold) {
607+
sum_fore += data[idx];
608+
count_fore++;
609+
} else {
610+
sum_back += data[idx];
611+
count_back++;
612+
}
613+
}
614+
mean_fore = sum_fore / count_fore;
615+
mean_back = sum_back / count_back;
616+
}
599617

600-
//need to select some pixels. we need some buffer room in the image.
601-
const size_t total_required_pixels = std::min((size_t) 10*512*512,localIntensityScale.mesh.size()/2);
602-
const size_t delta = localIntensityScale.mesh.size()/total_required_pixels - 1;
603618

604-
std::vector<T> lis_buffer(total_required_pixels);
605-
std::vector<S> grad_buffer(total_required_pixels);
619+
/**
620+
* Compute threshold value by Li's iterative Minimum Cross Entropy method [1]
621+
*
622+
* Note: it is assumed that the input elements are non-negative (as is the case for the gradient and local intensity
623+
* scale). To apply the method to data that may be negative, subtract the minimum value from the input, and then add
624+
* that value to the computed threshold.
625+
*
626+
* [1] Li, C. H., & Tam, P. K. S. (1998). "An iterative algorithm for minimum cross entropy thresholding."
627+
* Pattern recognition letters, 19(8), 771-776.
628+
*/
629+
template<typename T>
630+
float threshold_li(const std::vector<T> &input) {
606631

607-
uint64_t counter = 0;
608-
uint64_t counter_sampled = 0;
632+
if(input.empty()) { return 0.f; } // return 0 if input is empty
609633

610-
while((counter < localIntensityScale.mesh.size()) && (counter_sampled < total_required_pixels)){
634+
const T image_min = *std::min_element(input.begin(), input.end());
635+
const T image_max = *std::max_element(input.begin(), input.end());
611636

612-
lis_buffer[counter_sampled] = localIntensityScale.mesh[counter];
613-
grad_buffer[counter_sampled] = grad.mesh[counter];
637+
if(image_min == image_max) { return image_min; } // if all inputs are equal, return that value
614638

615-
counter+=delta;
616-
counter_sampled++;
639+
float tolerance = 0.5f; // tolerance 0.5 should suffice for integer inputs
617640

641+
// For floating point inputs we set the tolerance to the lesser of 0.01 and a fraction of the data range
642+
// This could be improved, by instead taking e.g. half the smallest difference between any two non-equal elements
643+
if(std::is_floating_point<T>::value) {
644+
float range = image_max - image_min;
645+
tolerance = std::min(0.01f, range*1e-4f);
618646
}
619647

648+
// Take the mean of the input as initial value
649+
float t_next = std::accumulate(input.begin(), input.end(), 0.f) / (float) input.size();
650+
float t_curr = -2.f * tolerance; //this ensures that the first iteration is performed, since t_next is non-negative
620651

621-
//float min_lis = *std::min_element(lis_buffer.begin(),lis_buffer.end());
622-
float max_lis = *std::max_element(lis_buffer.begin(),lis_buffer.end());
623-
624-
std::vector<uint64_t> hist_lis;
625-
hist_lis.resize(std::ceil(max_lis)+1,0);
626-
627-
for (uint64_t i = 0; i < total_required_pixels; ++i) {
628-
auto lis_val = std::floor(lis_buffer[i]);
629-
hist_lis[lis_val]++;
652+
// For integer inputs, we have to ensure a large enough initial value, such that mean_back > 0
653+
if(!std::is_floating_point<T>::value) {
654+
// if initial value is <1, try to increase it to 1.5 unless the range is too narrow
655+
if(t_next < 1.f) {
656+
t_next = std::min(1.5f, (image_min+image_max)/2.f);
657+
}
630658
}
631659

632-
//Then find 5% therhold, and take the grad values from that.
633-
uint64_t prop_values = 0.05*total_required_pixels;
660+
// Perform Li iterations until convergence
661+
while(std::abs(t_next - t_curr) > tolerance) {
662+
t_curr = t_next;
634663

635-
uint64_t cumsum = 0;
636-
uint64_t freq_val=0;
637-
while (cumsum < prop_values){
638-
cumsum+= hist_lis[freq_val];
639-
freq_val++;
664+
// Compute averages of values above and below the current threshold
665+
float mean_back, mean_fore;
666+
compute_means(input, t_curr, mean_back, mean_fore);
667+
668+
// Handle the edge case where all values < t_curr are 0
669+
if(mean_back == 0) {
670+
std::wcout << "log(0) encountered in APRConverter::threshold_li, returning current threshold" << std::endl;
671+
return t_curr;
672+
}
640673

674+
// Update the threshold (one-point iteration)
675+
t_next = (mean_fore - mean_back) / (std::log(mean_fore) - std::log(mean_back));
641676
}
677+
return t_next;
678+
}
642679

643-
std::vector<S> grad_hist;
644-
float grad_max = *std::max_element(grad_buffer.begin(),grad_buffer.end());
645-
//float grad_min = *std::max_element(grad_buffer.begin(),grad_buffer.end());
646680

647-
grad_hist.resize(std::ceil(grad_max));
648-
uint64_t grad_counter = 0;
681+
template<typename ImageType>
682+
template<typename T,typename S>
683+
void APRConverter<ImageType>::autoParametersLiEntropy(const PixelData<T> &image,
684+
const PixelData<T> &localIntensityScale,
685+
const PixelData<S> &grad) {
686+
687+
fine_grained_timer.start_timer("autoparameters: subsample buffers");
688+
689+
std::vector<S> grad_subsampled;
690+
std::vector<T> lis_subsampled;
691+
692+
{ // intentional scope
693+
/// First we extract the gradient and local intensity scale values at all locations where the image
694+
/// intensity exceeds the intensity threshold. This allows better adaptation in certain cases, e.g.
695+
/// when there is significant background AND signal noise/autofluorescence (provided that par.Ip_th
696+
/// is set appropriately).
697+
std::vector<S> grad_foreground(grad.mesh.size());
698+
std::vector<T> lis_foreground(localIntensityScale.mesh.size());
699+
700+
const auto threshold = par.Ip_th + bspline_offset;
701+
size_t counter = 0;
702+
for(size_t idx = 0; idx < grad.mesh.size(); ++idx) {
703+
if(image.mesh[idx] > threshold) {
704+
grad_foreground[counter] = grad.mesh[idx];
705+
lis_foreground[counter] = localIntensityScale.mesh[idx];
706+
counter++;
707+
}
708+
}
649709

650-
for (uint64_t i = 0; i < total_required_pixels; ++i) {
651-
auto val = lis_buffer[i];
710+
const size_t num_foreground_pixels = counter;
652711

653-
if(val <= freq_val){
654-
auto grad_val = grad_buffer[i];
655-
grad_hist[std::floor(grad_val)]++;
656-
grad_counter++;
657-
}
658-
}
712+
grad_foreground.resize(num_foreground_pixels); //setting size to non-zero elements.
713+
lis_foreground.resize(num_foreground_pixels);
659714

660-
auto max_it = std::max_element(grad_hist.begin(),grad_hist.end());
661-
uint64_t mode = std::distance(grad_hist.begin(),max_it);
715+
/// Then we uniformly subsample these signals, as we typically don't need all elements to compute the thresholds
716+
const size_t num_elements = std::min((size_t)32*512*512, num_foreground_pixels); //arbitrary number.
717+
const size_t delta = num_foreground_pixels / num_elements;
718+
grad_subsampled.resize(num_elements);
719+
lis_subsampled.resize(num_elements);
662720

663-
float grad_th = std::round(4*mode); //magic numbers.
721+
#ifdef HAVE_OPENMP
722+
#pragma omp parallel for default(shared)
723+
#endif
724+
for(size_t idx = 0; idx < num_elements; ++idx) {
725+
grad_subsampled[idx] = grad_foreground[idx*delta];
726+
lis_subsampled[idx] = lis_foreground[idx*delta];
727+
}
728+
}
729+
fine_grained_timer.stop_timer();
664730

665-
par.grad_th = grad_th;
666-
par.sigma_th = freq_val;
667-
par.sigma_th_max = 1;
731+
fine_grained_timer.start_timer("autoparameters: compute gradient threshold");
732+
par.grad_th = threshold_li(grad_subsampled);
733+
fine_grained_timer.stop_timer();
668734

669-
std::cout << "Used parameters: " << std::endl;
670-
std::cout << "I_th: " << par.Ip_th << std::endl;
671-
std::cout << "sigma_th: " << par.sigma_th << std::endl;
672-
std::cout << "grad_th: " << par.grad_th << std::endl;
673-
std::cout << "relative error (E): " << par.rel_error << std::endl;
674-
std::cout << "lambda: " << par.lambda << std::endl;
735+
fine_grained_timer.start_timer("autoparameters: compute sigma threshold");
736+
par.sigma_th = threshold_li(lis_subsampled);
737+
fine_grained_timer.stop_timer();
675738

739+
if(verbose) {
740+
std::cout << "Automatic parameter tuning found sigma_th = " << par.sigma_th <<
741+
" and grad_th = " << par.grad_th << std::endl;
742+
}
676743
}
677744

678745

test/APRTest.cpp

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,10 +287,18 @@ bool test_auto_parameters(TestData& test_data){
287287

288288
aprConverter.get_apr(apr, test_data.img_original);
289289

290-
//This test doesn't do anything except check if it runs.
290+
auto par = apr.get_apr_parameters();
291291

292-
return success;
292+
//checks if run, and the values are positive
293+
if(par.sigma_th <= 0){
294+
success = false;
295+
}
293296

297+
if(par.grad_th <= 0){
298+
success = false;
299+
}
300+
301+
return success;
294302

295303
}
296304

0 commit comments

Comments
 (0)