// Copyright (C) 2006 Davis E. King (davis@dlib.net) // License: Boost Software License See LICENSE.txt for the full license. #ifndef DLIB_SPATIAL_FILTERINg_H_ #define DLIB_SPATIAL_FILTERINg_H_ #include "../pixel.h" #include "spatial_filtering_abstract.h" #include "../algs.h" #include "../assert.h" #include "../array2d.h" #include "../matrix.h" #include "../geometry/border_enumerator.h" #include <limits> namespace dlib { // ---------------------------------------------------------------------------------------- template < typename in_image_type, typename out_image_type, typename EXP, typename T > rectangle spatially_filter_image ( const in_image_type& in_img, out_image_type& out_img, const matrix_exp<EXP>& filter, T scale, bool use_abs = false, bool add_to = false ) { COMPILE_TIME_ASSERT( pixel_traits<typename in_image_type::type>::has_alpha == false ); COMPILE_TIME_ASSERT( pixel_traits<typename out_image_type::type>::has_alpha == false ); DLIB_ASSERT(scale != 0 && filter.nr()%2 == 1 && filter.nc()%2 == 1, "\tvoid spatially_filter_image()" << "\n\t You can't give a scale of zero or a filter with even dimensions" << "\n\t scale: "<< scale << "\n\t filter.nr(): "<< filter.nr() << "\n\t filter.nc(): "<< filter.nc() ); DLIB_ASSERT(is_same_object(in_img, out_img) == false, "\tvoid spatially_filter_image()" << "\n\tYou must give two different image objects" ); // if there isn't any input image then don't do anything if (in_img.size() == 0) { out_img.clear(); return rectangle(); } out_img.set_size(in_img.nr(),in_img.nc()); zero_border_pixels(out_img, filter.nc()/2, filter.nr()/2); // figure out the range that we should apply the filter to const long first_row = filter.nr()/2; const long first_col = filter.nc()/2; const long last_row = in_img.nr() - filter.nr()/2; const long last_col = in_img.nc() - filter.nc()/2; const rectangle non_border = rectangle(first_col, first_row, last_col-1, last_row-1); // apply the filter to the image for (long r = first_row; r < last_row; ++r) { for (long c = first_col; c < last_col; ++c) { typedef typename EXP::type ptype; ptype p; ptype temp = 0; for (long m = 0; m < filter.nr(); ++m) { for (long n = 0; n < filter.nc(); ++n) { // pull out the current pixel and put it into p p = get_pixel_intensity(in_img[r-filter.nr()/2+m][c-filter.nc()/2+n]); temp += p*filter(m,n); } } temp /= scale; if (use_abs && temp < 0) { temp = -temp; } // save this pixel to the output image if (add_to == false) { assign_pixel(out_img[r][c], in_img[r][c]); assign_pixel_intensity(out_img[r][c], temp); } else { assign_pixel(out_img[r][c], temp + get_pixel_intensity(out_img[r][c])); } } } return non_border; } template < typename in_image_type, typename out_image_type, typename EXP > rectangle spatially_filter_image ( const in_image_type& in_img, out_image_type& out_img, const matrix_exp<EXP>& filter ) { return spatially_filter_image(in_img,out_img,filter,1); } // ---------------------------------------------------------------------------------------- template < typename in_image_type, typename out_image_type, typename EXP1, typename EXP2, typename T > rectangle spatially_filter_image_separable ( const in_image_type& in_img, out_image_type& out_img, const matrix_exp<EXP1>& row_filter, const matrix_exp<EXP2>& col_filter, T scale, bool use_abs = false, bool add_to = false ) { COMPILE_TIME_ASSERT( pixel_traits<typename in_image_type::type>::has_alpha == false ); COMPILE_TIME_ASSERT( pixel_traits<typename out_image_type::type>::has_alpha == false ); DLIB_ASSERT(scale != 0 && row_filter.size()%2 == 1 && col_filter.size()%2 == 1 && is_vector(row_filter) && is_vector(col_filter), "\tvoid spatially_filter_image_separable()" << "\n\t Invalid inputs were given to this function." << "\n\t scale: "<< scale << "\n\t row_filter.size(): "<< row_filter.size() << "\n\t col_filter.size(): "<< col_filter.size() << "\n\t is_vector(row_filter): "<< is_vector(row_filter) << "\n\t is_vector(col_filter): "<< is_vector(col_filter) ); DLIB_ASSERT(is_same_object(in_img, out_img) == false, "\tvoid spatially_filter_image_separable()" << "\n\tYou must give two different image objects" ); // if there isn't any input image then don't do anything if (in_img.size() == 0) { out_img.clear(); return rectangle(); } out_img.set_size(in_img.nr(),in_img.nc()); zero_border_pixels(out_img, row_filter.size()/2, col_filter.size()/2); // figure out the range that we should apply the filter to const long first_row = col_filter.size()/2; const long first_col = row_filter.size()/2; const long last_row = in_img.nr() - col_filter.size()/2; const long last_col = in_img.nc() - row_filter.size()/2; const rectangle non_border = rectangle(first_col, first_row, last_col-1, last_row-1); typedef typename out_image_type::mem_manager_type mem_manager_type; typedef typename EXP1::type ptype; array2d<ptype,mem_manager_type> temp_img; temp_img.set_size(in_img.nr(), in_img.nc()); // apply the row filter for (long r = 0; r < in_img.nr(); ++r) { for (long c = first_col; c < last_col; ++c) { ptype p; ptype temp = 0; for (long n = 0; n < row_filter.size(); ++n) { // pull out the current pixel and put it into p p = get_pixel_intensity(in_img[r][c-row_filter.size()/2+n]); temp += p*row_filter(n); } temp_img[r][c] = temp; } } // apply the column filter for (long r = first_row; r < last_row; ++r) { for (long c = first_col; c < last_col; ++c) { ptype temp = 0; for (long m = 0; m < col_filter.size(); ++m) { temp += temp_img[r-col_filter.size()/2+m][c]*col_filter(m); } temp /= scale; if (use_abs && temp < 0) { temp = -temp; } // save this pixel to the output image if (add_to == false) { assign_pixel(out_img[r][c], in_img[r][c]); assign_pixel_intensity(out_img[r][c], temp); } else { assign_pixel(out_img[r][c], temp + get_pixel_intensity(out_img[r][c])); } } } return non_border; } template < typename in_image_type, typename out_image_type, typename EXP1, typename EXP2 > rectangle spatially_filter_image_separable ( const in_image_type& in_img, out_image_type& out_img, const matrix_exp<EXP1>& row_filter, const matrix_exp<EXP2>& col_filter ) { return spatially_filter_image_separable(in_img,out_img,row_filter,col_filter,1); } // ---------------------------------------------------------------------------------------- template < typename in_image_type, typename out_image_type, typename EXP1, typename EXP2, typename T > rectangle spatially_filter_image_separable_down ( const unsigned long downsample, const in_image_type& in_img, out_image_type& out_img, const matrix_exp<EXP1>& row_filter, const matrix_exp<EXP2>& col_filter, T scale, bool use_abs = false, bool add_to = false ) { COMPILE_TIME_ASSERT( pixel_traits<typename in_image_type::type>::has_alpha == false ); COMPILE_TIME_ASSERT( pixel_traits<typename out_image_type::type>::has_alpha == false ); DLIB_ASSERT(downsample > 0 && scale != 0 && row_filter.size()%2 == 1 && col_filter.size()%2 == 1 && is_vector(row_filter) && is_vector(col_filter), "\tvoid spatially_filter_image_separable_down()" << "\n\t Invalid inputs were given to this function." << "\n\t downsample: "<< downsample << "\n\t scale: "<< scale << "\n\t row_filter.size(): "<< row_filter.size() << "\n\t col_filter.size(): "<< col_filter.size() << "\n\t is_vector(row_filter): "<< is_vector(row_filter) << "\n\t is_vector(col_filter): "<< is_vector(col_filter) ); DLIB_ASSERT(is_same_object(in_img, out_img) == false, "\tvoid spatially_filter_image_separable_down()" << "\n\tYou must give two different image objects" ); // if there isn't any input image then don't do anything if (in_img.size() == 0) { out_img.clear(); return rectangle(); } out_img.set_size((long)(std::ceil((double)in_img.nr()/downsample)), (long)(std::ceil((double)in_img.nc()/downsample))); const double col_border = std::floor(col_filter.size()/2.0); const double row_border = std::floor(row_filter.size()/2.0); // figure out the range that we should apply the filter to const long first_row = (long)std::ceil(col_border/downsample); const long first_col = (long)std::ceil(row_border/downsample); const long last_row = (long)std::ceil((in_img.nr() - col_border)/downsample) - 1; const long last_col = (long)std::ceil((in_img.nc() - row_border)/downsample) - 1; // zero border pixels const rectangle non_border = rectangle(first_col, first_row, last_col, last_row); border_enumerator be(get_rect(out_img), non_border ); while (be.move_next()) { out_img[be.element().y()][be.element().x()] = 0; } typedef typename EXP1::type ptype; typedef typename out_image_type::mem_manager_type mem_manager_type; array2d<ptype,mem_manager_type> temp_img; temp_img.set_size(in_img.nr(), out_img.nc()); // apply the row filter for (long r = 0; r < temp_img.nr(); ++r) { for (long c = non_border.left(); c <= non_border.right(); ++c) { ptype p; ptype temp = 0; for (long n = 0; n < row_filter.size(); ++n) { // pull out the current pixel and put it into p p = get_pixel_intensity(in_img[r][c*downsample-row_filter.size()/2+n]); temp += p*row_filter(n); } temp_img[r][c] = temp; } } // apply the column filter for (long r = non_border.top(); r <= non_border.bottom(); ++r) { for (long c = non_border.left(); c <= non_border.right(); ++c) { ptype temp = 0; for (long m = 0; m < col_filter.size(); ++m) { temp += temp_img[r*downsample-col_filter.size()/2+m][c]*col_filter(m); } temp /= scale; if (use_abs && temp < 0) { temp = -temp; } // save this pixel to the output image if (add_to == false) { assign_pixel(out_img[r][c], in_img[r*downsample][c*downsample]); assign_pixel_intensity(out_img[r][c], temp); } else { assign_pixel(out_img[r][c], temp + get_pixel_intensity(out_img[r][c])); } } } return non_border; } template < typename in_image_type, typename out_image_type, typename EXP1, typename EXP2 > rectangle spatially_filter_image_separable_down ( const unsigned long downsample, const in_image_type& in_img, out_image_type& out_img, const matrix_exp<EXP1>& row_filter, const matrix_exp<EXP2>& col_filter ) { return spatially_filter_image_separable_down(downsample,in_img,out_img,row_filter,col_filter,1); } // ---------------------------------------------------------------------------------------- template < long NR, long NC, typename T, typename U, typename in_image_type > inline void separable_3x3_filter_block_grayscale ( T (&block)[NR][NC], const in_image_type& img, const long& r, const long& c, const U& fe1, // separable filter end const U& fm, // separable filter middle const U& fe2 // separable filter end 2 ) { // make sure requires clause is not broken DLIB_ASSERT(shrink_rect(get_rect(img),1).contains(c,r) && shrink_rect(get_rect(img),1).contains(c+NC-1,r+NR-1), "\t void separable_3x3_filter_block_grayscale()" << "\n\t The sub-window doesn't fit inside the given image." << "\n\t get_rect(img): " << get_rect(img) << "\n\t (c,r): " << point(c,r) << "\n\t (c+NC-1,r+NR-1): " << point(c+NC-1,r+NR-1) ); T row_filt[NR+2][NC]; for (long rr = 0; rr < NR+2; ++rr) { for (long cc = 0; cc < NC; ++cc) { row_filt[rr][cc] = get_pixel_intensity(img[r+rr-1][c+cc-1])*fe1 + get_pixel_intensity(img[r+rr-1][c+cc])*fm + get_pixel_intensity(img[r+rr-1][c+cc+1])*fe2; } } for (long rr = 0; rr < NR; ++rr) { for (long cc = 0; cc < NC; ++cc) { block[rr][cc] = (row_filt[rr][cc]*fe1 + row_filt[rr+1][cc]*fm + row_filt[rr+2][cc]*fe2); } } } // ---------------------------------------------------------------------------------------- template < long NR, long NC, typename T, typename U, typename in_image_type > inline void separable_3x3_filter_block_rgb ( T (&block)[NR][NC], const in_image_type& img, const long& r, const long& c, const U& fe1, // separable filter end const U& fm, // separable filter middle const U& fe2 // separable filter end 2 ) { // make sure requires clause is not broken DLIB_ASSERT(shrink_rect(get_rect(img),1).contains(c,r) && shrink_rect(get_rect(img),1).contains(c+NC-1,r+NR-1), "\t void separable_3x3_filter_block_rgb()" << "\n\t The sub-window doesn't fit inside the given image." << "\n\t get_rect(img): " << get_rect(img) << "\n\t (c,r): " << point(c,r) << "\n\t (c+NC-1,r+NR-1): " << point(c+NC-1,r+NR-1) ); T row_filt[NR+2][NC]; for (long rr = 0; rr < NR+2; ++rr) { for (long cc = 0; cc < NC; ++cc) { row_filt[rr][cc].red = img[r+rr-1][c+cc-1].red*fe1 + img[r+rr-1][c+cc].red*fm + img[r+rr-1][c+cc+1].red*fe2; row_filt[rr][cc].green = img[r+rr-1][c+cc-1].green*fe1 + img[r+rr-1][c+cc].green*fm + img[r+rr-1][c+cc+1].green*fe2; row_filt[rr][cc].blue = img[r+rr-1][c+cc-1].blue*fe1 + img[r+rr-1][c+cc].blue*fm + img[r+rr-1][c+cc+1].blue*fe2; } } for (long rr = 0; rr < NR; ++rr) { for (long cc = 0; cc < NC; ++cc) { block[rr][cc].red = row_filt[rr][cc].red*fe1 + row_filt[rr+1][cc].red*fm + row_filt[rr+2][cc].red*fe2; block[rr][cc].green = row_filt[rr][cc].green*fe1 + row_filt[rr+1][cc].green*fm + row_filt[rr+2][cc].green*fe2; block[rr][cc].blue = row_filt[rr][cc].blue*fe1 + row_filt[rr+1][cc].blue*fm + row_filt[rr+2][cc].blue*fe2; } } } // ---------------------------------------------------------------------------------------- inline double gaussian ( double x, double sigma ) /*! requires - sigma > 0 ensures - computes and returns the value of a 1D Gaussian function with mean 0 and standard deviation sigma at the given x value. !*/ { DLIB_ASSERT(sigma > 0, "\tdouble gaussian(x)" << "\n\t sigma must be bigger than 0" << "\n\t sigma: " << sigma ); const double sqrt_2_pi = 2.5066282746310002416123552393401041626930; return 1.0/(sigma*sqrt_2_pi) * std::exp( -(x*x)/(2*sigma*sigma)); } // ---------------------------------------------------------------------------------------- template < typename T > matrix<T,0,1> create_gaussian_filter ( double sigma, int max_size ) /*! requires - sigma > 0 - max_size > 0 - max_size is an odd number ensures - returns a separable Gaussian filter F such that: - is_vector(F) == true - F.size() <= max_size - F is suitable for use with the spatially_filter_image_separable() routine and its use with this function corresponds to running a Gaussian filter of sigma width over an image. !*/ { DLIB_ASSERT(sigma > 0 && max_size > 0 && (max_size%2)==1, "\t matrix<T,0,1> create_gaussian_filter()" << "\n\t Invalid inputs were given to this function." << "\n\t sigma: " << sigma << "\n\t max_size: " << max_size ); // Adjust the size so that the ratio of the gaussian values isn't huge. // This only matters when T is an integer type. However, we do it for // all types so that the behavior of this function is always relatively // the same. while (gaussian(0,sigma)/gaussian(max_size/2,sigma) > 50) --max_size; matrix<double,0,1> f(max_size); for (long i = 0; i < f.size(); ++i) { f(i) = gaussian(i-max_size/2, sigma); } if (is_float_type<T>::value == false) { f /= f(0); return matrix_cast<T>(round(f)); } else { return matrix_cast<T>(f); } } // ---------------------------------------------------------------------------------------- template < typename in_image_type, typename out_image_type > void gaussian_blur ( const in_image_type& in_img, out_image_type& out_img, double sigma = 1, int max_size = 1001 ) { DLIB_ASSERT(sigma > 0 && max_size > 0 && (max_size%2)==1 && is_same_object(in_img, out_img) == false, "\t void gaussian_blur()" << "\n\t Invalid inputs were given to this function." << "\n\t sigma: " << sigma << "\n\t max_size: " << max_size << "\n\t is_same_object(in_img,out_img): " << is_same_object(in_img,out_img) ); typedef typename pixel_traits<typename out_image_type::type>::basic_pixel_type type; typedef typename promote<type>::type ptype; const matrix<ptype,0,1>& filt = create_gaussian_filter<ptype>(sigma, max_size); ptype scale = sum(filt); scale = scale*scale; spatially_filter_image_separable(in_img, out_img, filt, filt, scale); } // ---------------------------------------------------------------------------------------- namespace impl { template < bool add_to, typename image_type1, typename image_type2 > void sum_filter ( const image_type1& img, image_type2& out, const rectangle& rect ) { DLIB_ASSERT(img.nr() == out.nr() && img.nc() == out.nc() && is_same_object(img,out) == false, "\t void sum_filter()" << "\n\t Invalid arguments given to this function." << "\n\t img.nr(): " << img.nr() << "\n\t img.nc(): " << img.nc() << "\n\t out.nr(): " << out.nr() << "\n\t out.nc(): " << out.nc() << "\n\t is_same_object(img,out): " << is_same_object(img,out) ); typedef typename image_type1::type pixel_type; typedef typename promote<pixel_type>::type ptype; std::vector<ptype> column_sum; column_sum.resize(img.nc() + rect.width(),0); const long top = -1 + rect.top(); const long bottom = -1 + rect.bottom(); long left = rect.left()-1; // initialize column_sum at row -1 for (unsigned long j = 0; j < column_sum.size(); ++j) { rectangle strip(left,top,left,bottom); strip = strip.intersect(get_rect(img)); if (!strip.is_empty()) { column_sum[j] = sum(matrix_cast<ptype>(subm(mat(img),strip))); } ++left; } const rectangle area = get_rect(img); // Save width to avoid computing it over and over. const long width = rect.width(); // Now do the bulk of the filtering work. for (long r = 0; r < img.nr(); ++r) { // set to sum at point(-1,r). i.e. should be equal to sum(mat(img), translate_rect(rect, point(-1,r))) // We compute it's value in the next loop. ptype cur_sum = 0; // Update the first part of column_sum since we only work on the c+width part of column_sum // in the main loop. const long top = r + rect.top() - 1; const long bottom = r + rect.bottom(); for (long k = 0; k < width; ++k) { const long right = k-width + rect.right(); const ptype br_corner = area.contains(right,bottom) ? img[bottom][right] : 0; const ptype tr_corner = area.contains(right,top) ? img[top][right] : 0; // update the sum in this column now that we are on the next row column_sum[k] = column_sum[k] + br_corner - tr_corner; cur_sum += column_sum[k]; } for (long c = 0; c < img.nc(); ++c) { const long top = r + rect.top() - 1; const long bottom = r + rect.bottom(); const long right = c + rect.right(); const ptype br_corner = area.contains(right,bottom) ? img[bottom][right] : 0; const ptype tr_corner = area.contains(right,top) ? img[top][right] : 0; // update the sum in this column now that we are on the next row column_sum[c+width] = column_sum[c+width] + br_corner - tr_corner; // add in the new right side of the rect and subtract the old right side. cur_sum = cur_sum + column_sum[c+width] - column_sum[c]; if (add_to) out[r][c] += static_cast<typename image_type2::type>(cur_sum); else out[r][c] = static_cast<typename image_type2::type>(cur_sum); } } } } template < typename image_type1, typename image_type2 > void sum_filter ( const image_type1& img, image_type2& out, const rectangle& rect ) { impl::sum_filter<true>(img,out,rect); } template < typename image_type1, typename image_type2 > void sum_filter_assign ( const image_type1& img, image_type2& out, const rectangle& rect ) { impl::sum_filter<false>(img,out,rect); } // ---------------------------------------------------------------------------------------- namespace impl { template <typename T> class fast_deque { /* This is a fast and minimal implementation of std::deque for use with the max_filter. This object assumes that no more than max_size elements will ever be pushed into it at a time. */ public: explicit fast_deque(unsigned long max_size) { // find a power of two that upper bounds max_size mask = 2; while (mask < max_size) mask *= 2; clear(); data.resize(mask); --mask; // make into bit mask } void clear() { first = 1; last = 0; size = 0; } bool empty() const { return size == 0; } void pop_back() { last = (last-1)&mask; --size; } void push_back(const T& item) { last = (last+1)&mask; ++size; data[last] = item; } void pop_front() { first = (first+1)&mask; --size; } const T& front() const { return data[first]; } const T& back() const { return data[last]; } private: std::vector<T> data; unsigned long mask; unsigned long first; unsigned long last; unsigned long size; }; } // ---------------------------------------------------------------------------------------- template < typename image_type1, typename image_type2 > void max_filter ( image_type1& img, image_type2& out, const long width, const long height, const typename image_type1::type& thresh ) { DLIB_ASSERT( width > 0 && height > 0 && out.nr() == img.nr() && out.nc() == img.nc() && is_same_object(img,out) == false, "\t void max_filter()" << "\n\t Invalid arguments given to this function." << "\n\t img.nr(): " << img.nr() << "\n\t img.nc(): " << img.nc() << "\n\t out.nr(): " << out.nr() << "\n\t out.nc(): " << out.nc() << "\n\t width: " << width << "\n\t height: " << height << "\n\t is_same_object(img,out): " << is_same_object(img,out) ); typedef typename image_type1::type pixel_type; dlib::impl::fast_deque<std::pair<long,pixel_type> > Q(std::max(width,height)); const long last_col = std::max(img.nc(), ((width-1)/2)); const long last_row = std::max(img.nr(), ((height-1)/2)); // run max filter along rows of img for (long r = 0; r < img.nr(); ++r) { Q.clear(); for (long c = 0; c < (width-1)/2 && c < img.nc(); ++c) { while (!Q.empty() && img[r][c] >= Q.back().second) Q.pop_back(); Q.push_back(std::make_pair(c,img[r][c])); } for (long c = (width-1)/2; c < img.nc(); ++c) { while (!Q.empty() && img[r][c] >= Q.back().second) Q.pop_back(); while (!Q.empty() && Q.front().first <= c-width) Q.pop_front(); Q.push_back(std::make_pair(c,img[r][c])); img[r][c-((width-1)/2)] = Q.front().second; } for (long c = last_col; c < img.nc() + ((width-1)/2); ++c) { while (!Q.empty() && Q.front().first <= c-width) Q.pop_front(); img[r][c-((width-1)/2)] = Q.front().second; } } // run max filter along columns of img. Store result in out. for (long cc = 0; cc < img.nc(); ++cc) { Q.clear(); for (long rr = 0; rr < (height-1)/2 && rr < img.nr(); ++rr) { while (!Q.empty() && img[rr][cc] >= Q.back().second) Q.pop_back(); Q.push_back(std::make_pair(rr,img[rr][cc])); } for (long rr = (height-1)/2; rr < img.nr(); ++rr) { while (!Q.empty() && img[rr][cc] >= Q.back().second) Q.pop_back(); while (!Q.empty() && Q.front().first <= rr-height) Q.pop_front(); Q.push_back(std::make_pair(rr,img[rr][cc])); out[rr-((height-1)/2)][cc] += std::max(Q.front().second, thresh); } for (long rr = last_row; rr < img.nr() + ((height-1)/2); ++rr) { while (!Q.empty() && Q.front().first <= rr-height) Q.pop_front(); out[rr-((height-1)/2)][cc] += std::max(Q.front().second, thresh); } } } // ---------------------------------------------------------------------------------------- } #endif // DLIB_SPATIAL_FILTERINg_H_