Boost GIL


histogram.hpp
1 //
2 // Copyright 2020 Debabrata Mandal <mandaldebabrata123@gmail.com>
3 //
4 // Distributed under the Boost Software License, Version 1.0
5 // See accompanying file LICENSE_1_0.txt or copy at
6 // http://www.boost.org/LICENSE_1_0.txt
7 //
8 
9 #ifndef BOOST_GIL_HISTOGRAM_HPP
10 #define BOOST_GIL_HISTOGRAM_HPP
11 
12 #include <boost/gil/concepts/concept_check.hpp>
13 #include <boost/gil/metafunctions.hpp>
14 #include <boost/gil/pixel.hpp>
15 
16 #include <boost/mp11.hpp>
17 #include <boost/type_traits.hpp>
18 #include <boost/functional/hash.hpp>
19 
20 #include <array>
21 #include <iostream>
22 #include <tuple>
23 #include <utility>
24 #include <vector>
25 #include <type_traits>
26 #include <map>
27 #include <unordered_map>
28 
29 namespace boost { namespace gil {
30 
39 
40 namespace detail {
41 
44 
47 template <std::size_t Index, typename... T>
48 inline auto hash_tuple_impl(std::size_t&, std::tuple<T...> const&)
49  -> typename std::enable_if<Index == sizeof...(T), void>::type
50 {
51  // terminating case
52 }
53 
56 template <std::size_t Index, typename... T>
57 inline auto hash_tuple_impl(std::size_t& seed, std::tuple<T...> const& t)
58  -> typename std::enable_if<Index != sizeof...(T), void>::type
59 {
60  boost::hash_combine(seed, std::get<Index>(t));
61  hash_tuple_impl<Index + 1>(seed, t);
62 }
63 
73 template <typename... T>
74 struct hash_tuple
75 {
76  auto operator()(std::tuple<T...> const& t) const -> std::size_t
77  {
78  std::size_t seed = 0;
79  hash_tuple_impl<0>(seed, t);
80  return seed;
81  }
82 };
83 
87 template <typename Pixel, std::size_t... I>
88 auto pixel_to_tuple(Pixel const& p, boost::mp11::index_sequence<I...>)
89  -> decltype(std::make_tuple(p[I]...))
90 {
91  return std::make_tuple(p[I]...);
92 }
93 
97 template <typename Tuple, std::size_t... I>
98 auto tuple_to_tuple(Tuple const& t, boost::mp11::index_sequence<I...>)
99  -> decltype(std::make_tuple(std::get<I>(t)...))
100 {
101  return std::make_tuple(std::get<I>(t)...);
102 }
103 
106 template <typename Tuple, std::size_t... I>
107 bool tuple_compare(Tuple const& t1, Tuple const& t2, boost::mp11::index_sequence<I...>)
108 {
109  std::array<bool, std::tuple_size<Tuple>::value> comp_list;
110  comp_list = {std::get<I>(t1) <= std::get<I>(t2)...};
111  bool comp = true;
112  for (std::size_t i = 0; i < comp_list.size(); i++)
113  {
114  comp = comp & comp_list[i];
115  }
116  return comp;
117 }
118 
124 template <typename Tuple>
125 bool tuple_compare(Tuple const& t1, Tuple const& t2)
126 {
127  std::size_t const tuple_size = std::tuple_size<Tuple>::value;
128  auto index_list = boost::mp11::make_index_sequence<tuple_size>{};
129  return tuple_compare(t1, t2, index_list);
130 }
131 
136 template <typename Tuple>
138 {
139  static constexpr Tuple (min)()
140  {
141  return min_impl(boost::mp11::make_index_sequence<std::tuple_size<Tuple>::value>{});
142  }
143  static constexpr Tuple (max)()
144  {
145  return max_impl(boost::mp11::make_index_sequence<std::tuple_size<Tuple>::value>{});
146  }
147 
148 private:
149  template <std::size_t... I>
150  static constexpr Tuple min_impl(boost::mp11::index_sequence<I...>)
151  {
152  return std::make_tuple(
153  (std::numeric_limits<typename std::tuple_element<I, Tuple>::type>::min)()...);
154  }
155 
156  template <std::size_t... I>
157  static constexpr Tuple max_impl(boost::mp11::index_sequence<I...>)
158  {
159  return std::make_tuple(
160  (std::numeric_limits<typename std::tuple_element<I, Tuple>::type>::max)()...);
161  }
162 };
163 
171 template <std::size_t Dimension>
172 struct filler
173 {
174  template <typename Container, typename Tuple>
175  void operator()(Container&, Tuple&, Tuple&, std::size_t)
176  {
177  }
178 };
179 
182 template <>
183 struct filler<1>
184 {
185  template <typename Container, typename Tuple>
186  void operator()(Container& hist, Tuple& lower, Tuple& upper, std::size_t bin_width = 1)
187  {
188  for (auto i = std::get<0>(lower); static_cast<std::size_t>(std::get<0>(upper) - i) >= bin_width; i += bin_width)
189  {
190  hist(i / bin_width) = 0;
191  }
192  hist(std::get<0>(upper) / bin_width) = 0;
193  }
194 };
195 
196 } //namespace detail
197 
213 template <typename... T>
214 class histogram : public std::unordered_map<std::tuple<T...>, double, detail::hash_tuple<T...>>
215 {
216  using base_t = std::unordered_map<std::tuple<T...>, double, detail::hash_tuple<T...>>;
217  using bin_t = boost::mp11::mp_list<T...>;
218  using key_t = typename base_t::key_type;
219  using mapped_t = typename base_t::mapped_type;
220  using value_t = typename base_t::value_type;
221 
222 public:
223  histogram() = default;
224 
226  static constexpr std::size_t dimension()
227  {
228  return std::tuple_size<key_t>::value;
229  }
230 
232  mapped_t& operator()(T... indices)
233  {
234  auto key = std::make_tuple(indices...);
235  std::size_t const index_dimension = std::tuple_size<std::tuple<T...>>::value;
236  std::size_t const histogram_dimension = dimension();
237  static_assert(histogram_dimension == index_dimension, "Dimensions do not match.");
238 
239  return base_t::operator[](key);
240  }
241 
244  template <typename OtherType>
245  bool equals(OtherType const& otherhist) const
246  {
247  bool check = (dimension() == otherhist.dimension());
248 
249  using other_value_t = typename OtherType::value_type;
250  std::for_each(otherhist.begin(), otherhist.end(), [&](other_value_t const& v) {
251  key_t key = key_from_tuple(v.first);
252  if (base_t::find(key) != base_t::end())
253  {
254  check = check & (base_t::at(key) == otherhist.at(v.first));
255  }
256  else
257  {
258  check = false;
259  }
260  });
261  return check;
262  }
263 
266  static constexpr bool is_pixel_compatible()
267  {
268  using bin_types = boost::mp11::mp_list<T...>;
269  return boost::mp11::mp_all_of<bin_types, std::is_arithmetic>::value;
270  }
271 
274  template <typename Tuple>
275  bool is_tuple_compatible(Tuple const&)
276  {
277  std::size_t const tuple_size = std::tuple_size<Tuple>::value;
278  std::size_t const histogram_size = dimension();
279  // TODO : Explore consequence of using if-constexpr
280  using sequence_type = typename std::conditional
281  <
282  tuple_size >= histogram_size,
283  boost::mp11::make_index_sequence<histogram_size>,
284  boost::mp11::make_index_sequence<tuple_size>
285  >::type;
286 
287  if (is_tuple_size_compatible<Tuple>())
288  return is_tuple_type_compatible<Tuple>(sequence_type{});
289  else
290  return false;
291  }
292 
295  template <std::size_t... Dimensions, typename Tuple>
296  key_t key_from_tuple(Tuple const& t) const
297  {
298  using index_list = boost::mp11::mp_list_c<std::size_t, Dimensions...>;
299  std::size_t const index_list_size = boost::mp11::mp_size<index_list>::value;
300  std::size_t const tuple_size = std::tuple_size<Tuple>::value;
301  std::size_t const histogram_dimension = dimension();
302 
303  static_assert(
304  ((index_list_size != 0 && index_list_size == histogram_dimension) ||
305  (tuple_size == histogram_dimension)),
306  "Tuple and histogram key of different sizes");
307 
308  using new_index_list = typename std::conditional
309  <
310  index_list_size == 0,
311  boost::mp11::mp_list_c<std::size_t, 0>,
312  index_list
313  >::type;
314 
315  std::size_t const min =
316  boost::mp11::mp_min_element<new_index_list, boost::mp11::mp_less>::value;
317 
318  std::size_t const max =
319  boost::mp11::mp_max_element<new_index_list, boost::mp11::mp_less>::value;
320 
321  static_assert((0 <= min && max < tuple_size) || index_list_size == 0, "Index out of Range");
322 
323  using seq1 = boost::mp11::make_index_sequence<histogram_dimension>;
324  using seq2 = boost::mp11::index_sequence<Dimensions...>;
325  // TODO : Explore consequence of using if-constexpr
326  using sequence_type = typename std::conditional<index_list_size == 0, seq1, seq2>::type;
327 
328  auto key = detail::tuple_to_tuple(t, sequence_type{});
329  static_assert(
330  is_tuple_type_compatible<Tuple>(seq1{}),
331  "Tuple type and histogram type not compatible.");
332 
333  return make_histogram_key(key, seq1{});
334  }
335 
338  template <std::size_t... Dimensions, typename Pixel>
339  key_t key_from_pixel(Pixel const& p) const
340  {
341  using index_list = boost::mp11::mp_list_c<std::size_t, Dimensions...>;
342  std::size_t const index_list_size = boost::mp11::mp_size<index_list>::value;
343  std::size_t const pixel_dimension = num_channels<Pixel>::value;
344  std::size_t const histogram_dimension = dimension();
345 
346  static_assert(
347  ((index_list_size != 0 && index_list_size == histogram_dimension) ||
348  (index_list_size == 0 && pixel_dimension == histogram_dimension)) &&
349  is_pixel_compatible(),
350  "Pixels and histogram key are not compatible.");
351 
352  using new_index_list = typename std::conditional
353  <
354  index_list_size == 0,
355  boost::mp11::mp_list_c<std::size_t, 0>,
356  index_list
357  >::type;
358 
359  std::size_t const min =
360  boost::mp11::mp_min_element<new_index_list, boost::mp11::mp_less>::value;
361 
362  std::size_t const max =
363  boost::mp11::mp_max_element<new_index_list, boost::mp11::mp_less>::value;
364 
365  static_assert(
366  (0 <= min && max < pixel_dimension) || index_list_size == 0, "Index out of Range");
367 
368  using seq1 = boost::mp11::make_index_sequence<histogram_dimension>;
369  using seq2 = boost::mp11::index_sequence<Dimensions...>;
370  using sequence_type = typename std::conditional<index_list_size == 0, seq1, seq2>::type;
371 
372  auto key = detail::pixel_to_tuple(p, sequence_type{});
373  return make_histogram_key(key, seq1{});
374  }
375 
377  key_t nearest_key(key_t const& k) const
378  {
379  using check_list = boost::mp11::mp_list<boost::has_less<T>...>;
380  static_assert(
381  boost::mp11::mp_all_of<check_list, boost::mp11::mp_to_bool>::value,
382  "Keys are not comparable.");
383  auto nearest_k = k;
384  if (base_t::find(k) != base_t::end())
385  {
386  return nearest_k;
387  }
388  else
389  {
390  bool once = true;
391  std::for_each(base_t::begin(), base_t::end(), [&](value_t const& v) {
392  if (v.first <= k)
393  {
394  if (once)
395  {
396  once = !once;
397  nearest_k = v.first;
398  }
399  else if (nearest_k < v.first)
400  nearest_k = v.first;
401  }
402  });
403  return nearest_k;
404  }
405  }
406 
408  template <std::size_t... Dimensions, typename SrcView>
409  void fill(
410  SrcView const& srcview,
411  std::size_t bin_width = 1,
412  bool applymask = false,
413  std::vector<std::vector<bool>> mask = {},
414  key_t lower = key_t(),
415  key_t upper = key_t(),
416  bool setlimits = false)
417  {
418  gil_function_requires<ImageViewConcept<SrcView>>();
419  using channel_t = typename channel_type<SrcView>::type;
420 
421  for (std::ptrdiff_t src_y = 0; src_y < srcview.height(); ++src_y)
422  {
423  auto src_it = srcview.row_begin(src_y);
424  for (std::ptrdiff_t src_x = 0; src_x < srcview.width(); ++src_x)
425  {
426  if (applymask && !mask[src_y][src_x])
427  continue;
428  auto scaled_px = src_it[src_x];
429  static_for_each(scaled_px, [&](channel_t& ch) {
430  ch = ch / bin_width;
431  });
432  auto key = key_from_pixel<Dimensions...>(scaled_px);
433  if (!setlimits ||
434  (detail::tuple_compare(lower, key) && detail::tuple_compare(key, upper)))
435  base_t::operator[](key)++;
436  }
437  }
438  }
439 
441  template <std::size_t... Dimensions, typename Tuple>
442  histogram sub_histogram(Tuple const& t1, Tuple const& t2)
443  {
444  using index_list = boost::mp11::mp_list_c<std::size_t, Dimensions...>;
445  std::size_t const index_list_size = boost::mp11::mp_size<index_list>::value;
446  std::size_t const histogram_dimension = dimension();
447 
448  std::size_t const min =
449  boost::mp11::mp_min_element<index_list, boost::mp11::mp_less>::value;
450 
451  std::size_t const max =
452  boost::mp11::mp_max_element<index_list, boost::mp11::mp_less>::value;
453 
454  static_assert(
455  (0 <= min && max < histogram_dimension) && index_list_size < histogram_dimension,
456  "Index out of Range");
457 
458  using seq1 = boost::mp11::make_index_sequence<dimension()>;
459  using seq2 = boost::mp11::index_sequence<Dimensions...>;
460 
461  static_assert(
462  is_tuple_type_compatible<Tuple>(seq1{}),
463  "Tuple type and histogram type not compatible.");
464 
465  auto low = make_histogram_key(t1, seq1{});
466  auto low_key = detail::tuple_to_tuple(low, seq2{});
467  auto high = make_histogram_key(t2, seq1{});
468  auto high_key = detail::tuple_to_tuple(high, seq2{});
469 
470  histogram sub_h;
471  std::for_each(base_t::begin(), base_t::end(), [&](value_t const& k) {
472  auto tmp_key = detail::tuple_to_tuple(k.first, seq2{});
473  if (low_key <= tmp_key && tmp_key <= high_key)
474  sub_h[k.first] += base_t::operator[](k.first);
475  });
476  return sub_h;
477  }
478 
480  template <std::size_t... Dimensions>
482  {
483  using index_list = boost::mp11::mp_list_c<std::size_t, Dimensions...>;
484  std::size_t const index_list_size = boost::mp11::mp_size<index_list>::value;
485  std::size_t const histogram_dimension = dimension();
486 
487  std::size_t const min =
488  boost::mp11::mp_min_element<index_list, boost::mp11::mp_less>::value;
489 
490  std::size_t const max =
491  boost::mp11::mp_max_element<index_list, boost::mp11::mp_less>::value;
492 
493  static_assert(
494  (0 <= min && max < histogram_dimension) && index_list_size < histogram_dimension,
495  "Index out of Range");
496 
498 
499  std::for_each(base_t::begin(), base_t::end(), [&](value_t const& v) {
500  auto sub_key =
501  detail::tuple_to_tuple(v.first, boost::mp11::index_sequence<Dimensions...>{});
502  sub_h[sub_key] += base_t::operator[](v.first);
503  });
504  return sub_h;
505  }
506 
508  void normalize()
509  {
510  double sum = 0.0;
511  std::for_each(base_t::begin(), base_t::end(), [&](value_t const& v) {
512  sum += v.second;
513  });
514  // std::cout<<(long int)sum<<"asfe";
515  std::for_each(base_t::begin(), base_t::end(), [&](value_t const& v) {
516  base_t::operator[](v.first) = v.second / sum;
517  });
518  }
519 
521  double sum() const
522  {
523  double sum = 0.0;
524  std::for_each(base_t::begin(), base_t::end(), [&](value_t const& v) {
525  sum += v.second;
526  });
527  return sum;
528  }
529 
531  key_t min_key() const
532  {
533  key_t min_key = base_t::begin()->first;
534  std::for_each(base_t::begin(), base_t::end(), [&](value_t const& v) {
535  if (v.first < min_key)
536  min_key = v.first;
537  });
538  return min_key;
539  }
540 
542  key_t max_key() const
543  {
544  key_t max_key = base_t::begin()->first;
545  std::for_each(base_t::begin(), base_t::end(), [&](value_t const& v) {
546  if (v.first > max_key)
547  max_key = v.first;
548  });
549  return max_key;
550  }
551 
553  std::vector<key_t> sorted_keys() const
554  {
555  std::vector<key_t> sorted_keys;
556  std::for_each(base_t::begin(), base_t::end(), [&](value_t const& v) {
557  sorted_keys.push_back(v.first);
558  });
559  std::sort(sorted_keys.begin(), sorted_keys.end());
560  return sorted_keys;
561  }
562 
563 private:
564  template <typename Tuple, std::size_t... I>
565  key_t make_histogram_key(Tuple const& t, boost::mp11::index_sequence<I...>) const
566  {
567  return std::make_tuple(
568  static_cast<typename boost::mp11::mp_at<bin_t, boost::mp11::mp_size_t<I>>>(
569  std::get<I>(t))...);
570  }
571 
572  template <typename Tuple, std::size_t... I>
573  static constexpr bool is_tuple_type_compatible(boost::mp11::index_sequence<I...>)
574  {
575  using tp = boost::mp11::mp_list
576  <
577  typename std::is_convertible
578  <
579  boost::mp11::mp_at<bin_t, boost::mp11::mp_size_t<I>>,
580  typename std::tuple_element<I, Tuple>::type
581  >::type...
582  >;
583  return boost::mp11::mp_all_of<tp, boost::mp11::mp_to_bool>::value;
584  }
585 
586  template <typename Tuple>
587  static constexpr bool is_tuple_size_compatible()
588  {
589  return (std::tuple_size<Tuple>::value == dimension());
590  }
591 };
592 
607 template <typename SrcView, typename Container>
608 void fill_histogram(SrcView const&, Container&);
609 
631 template <std::size_t... Dimensions, typename SrcView, typename... T>
632 void fill_histogram(
633  SrcView const& srcview,
634  histogram<T...>& hist,
635  std::size_t bin_width = 1,
636  bool accumulate = false,
637  bool sparsefill = true,
638  bool applymask = false,
639  std::vector<std::vector<bool>> mask = {},
640  typename histogram<T...>::key_type lower =
641  (detail::tuple_limit<typename histogram<T...>::key_type>::min)(),
642  typename histogram<T...>::key_type upper =
643  (detail::tuple_limit<typename histogram<T...>::key_type>::max)(),
644  bool setlimits = false)
645 {
646  if (!accumulate)
647  hist.clear();
648 
649  detail::filler<histogram<T...>::dimension()> f;
650  if (!sparsefill)
651  f(hist, lower, upper, bin_width);
652 
653  hist.template fill<Dimensions...>(srcview, bin_width, applymask, mask, lower, upper, setlimits);
654 }
655 
668 template <typename Container>
669 auto cumulative_histogram(Container const&) -> Container;
670 
671 template <typename... T>
672 auto cumulative_histogram(histogram<T...> const& hist) -> histogram<T...>
673 {
674  using check_list = boost::mp11::mp_list<boost::has_less<T>...>;
675  static_assert(
676  boost::mp11::mp_all_of<check_list, boost::mp11::mp_to_bool>::value,
677  "Cumulative histogram not possible of this type");
678 
679  using histogram_t = histogram<T...>;
680  using pair_t = std::pair<typename histogram_t::key_type, typename histogram_t::mapped_type>;
681  using value_t = typename histogram_t::value_type;
682 
683  histogram_t cumulative_hist;
684  std::size_t const dims = histogram_t::dimension();
685  if (dims == 1)
686  {
687  std::vector<pair_t> sorted_keys(hist.size());
688  std::size_t counter = 0;
689  std::for_each(hist.begin(), hist.end(), [&](value_t const& v) {
690  sorted_keys[counter++] = std::make_pair(v.first, v.second);
691  });
692  std::sort(sorted_keys.begin(), sorted_keys.end());
693  auto cumulative_counter = static_cast<typename histogram_t::mapped_type>(0);
694  for (std::size_t i = 0; i < sorted_keys.size(); ++i)
695  {
696  cumulative_counter += sorted_keys[i].second;
697  cumulative_hist[(sorted_keys[i].first)] = cumulative_counter;
698  }
699  }
700  else
701  {
702  std::for_each(hist.begin(), hist.end(), [&](value_t const& v1) {
703  auto cumulative_counter = static_cast<typename histogram_t::mapped_type>(0);
704  std::for_each(hist.begin(), hist.end(), [&](value_t const& v2) {
705  bool comp = detail::tuple_compare(
706  v2.first, v1.first,
707  boost::mp11::make_index_sequence<histogram_t::dimension()>{});
708  if (comp)
709  cumulative_counter += hist.at(v2.first);
710  });
711  cumulative_hist[v1.first] = cumulative_counter;
712  });
713  }
714  return cumulative_hist;
715 }
716 
717 }} //namespace boost::gil
718 
719 #endif
Default histogram class provided by boost::gil.
Definition: histogram.hpp:215
static constexpr bool is_pixel_compatible()
Checks if the histogram class is compatible to be used with a GIL image type.
Definition: histogram.hpp:266
key_t key_from_tuple(Tuple const &t) const
Returns a key compatible to be used as the histogram key from the input tuple.
Definition: histogram.hpp:296
histogram< boost::mp11::mp_at< bin_t, boost::mp11::mp_size_t< Dimensions > >... > sub_histogram()
Returns a sub-histogram over specified axes.
Definition: histogram.hpp:481
mapped_t & operator()(T... indices)
Returns bin value corresponding to specified tuple.
Definition: histogram.hpp:232
static constexpr std::size_t dimension()
Returns the number of dimensions(axes) the class supports.
Definition: histogram.hpp:226
histogram sub_histogram(Tuple const &t1, Tuple const &t2)
Can return a subset or a mask over the current histogram.
Definition: histogram.hpp:442
key_t min_key() const
Return the minimum key in histogram.
Definition: histogram.hpp:531
key_t key_from_pixel(Pixel const &p) const
Returns a histogram compatible key from the input pixel which can be directly used.
Definition: histogram.hpp:339
std::vector< key_t > sorted_keys() const
Return sorted keys in a vector.
Definition: histogram.hpp:553
key_t nearest_key(key_t const &k) const
Return nearest smaller key to specified histogram key.
Definition: histogram.hpp:377
void normalize()
Normalize this histogram class.
Definition: histogram.hpp:508
void fill(SrcView const &srcview, std::size_t bin_width=1, bool applymask=false, std::vector< std::vector< bool >> mask={}, key_t lower=key_t(), key_t upper=key_t(), bool setlimits=false)
Fills the histogram with the input image view.
Definition: histogram.hpp:409
bool is_tuple_compatible(Tuple const &)
Checks if the histogram class is compatible to be used with the specified tuple type.
Definition: histogram.hpp:275
double sum() const
Return the sum count of all bins.
Definition: histogram.hpp:521
key_t max_key() const
Return the maximum key in histogram.
Definition: histogram.hpp:542
bool equals(OtherType const &otherhist) const
Checks if 2 histograms are equal. Ignores type, and checks if the keys (after type casting) match.
Definition: histogram.hpp:245
auto tuple_to_tuple(Tuple const &t, boost::mp11::index_sequence< I... >) -> decltype(std::make_tuple(std::get< I >(t)...))
Definition: histogram.hpp:98
auto pixel_to_tuple(Pixel const &p, boost::mp11::index_sequence< I... >) -> decltype(std::make_tuple(p[I]...))
Definition: histogram.hpp:88
bool tuple_compare(Tuple const &t1, Tuple const &t2)
Compares 2 tuples and outputs t1 <= t2 Comparison is not in a lexicographic manner but on every eleme...
Definition: histogram.hpp:125
void fill(boost::gil::iterator_from_2d< IL > first, boost::gil::iterator_from_2d< IL > last, const V &val)
std::fill(I,I,V) with I being a iterator_from_2d
Definition: algorithm.hpp:369
defined(BOOST_NO_CXX17_HDR_MEMORY_RESOURCE)
Definition: algorithm.hpp:36
Definition: color_convert.hpp:31
Filler is used to fill the histogram class with all values between a specified range This functor is ...
Definition: histogram.hpp:173
Functor provided for the hashing of tuples. The following approach makes use hash_combine from boost:...
Definition: histogram.hpp:75
Provides equivalent of std::numeric_limits for type std::tuple tuple_limit gets called with only tupl...
Definition: histogram.hpp:138
Returns the number of channels of a pixel-based GIL construct.
Definition: pixel.hpp:54