| Chris@1573 | 1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */ | 
| Chris@1573 | 2 | 
| Chris@1573 | 3 /* | 
| Chris@1573 | 4     Sonic Visualiser | 
| Chris@1573 | 5     An audio file viewer and annotation editor. | 
| Chris@1573 | 6     Centre for Digital Music, Queen Mary, University of London. | 
| Chris@1573 | 7     This file copyright 2007-2015 Particular Programs Ltd, | 
| Chris@1573 | 8     copyright 2018 Queen Mary University of London. | 
| Chris@1573 | 9 | 
| Chris@1573 | 10     This program is free software; you can redistribute it and/or | 
| Chris@1573 | 11     modify it under the terms of the GNU General Public License as | 
| Chris@1573 | 12     published by the Free Software Foundation; either version 2 of the | 
| Chris@1573 | 13     License, or (at your option) any later version.  See the file | 
| Chris@1573 | 14     COPYING included with this distribution for more information. | 
| Chris@1573 | 15 */ | 
| Chris@1573 | 16 | 
| Chris@1573 | 17 #ifndef SV_MOVING_MEDIAN_H | 
| Chris@1573 | 18 #define SV_MOVING_MEDIAN_H | 
| Chris@1573 | 19 | 
| Chris@1573 | 20 #include <bqvec/Allocators.h> | 
| Chris@1573 | 21 #include <bqvec/VectorOps.h> | 
| Chris@1573 | 22 | 
| Chris@1573 | 23 #include <algorithm> | 
| Chris@1573 | 24 #include <iostream> | 
| Chris@1573 | 25 #include <stdexcept> | 
| Chris@1573 | 26 | 
| Chris@1573 | 27 /** | 
| Chris@1573 | 28  * Obtain the median (or other percentile) of a moving window across a | 
| Chris@1573 | 29  * time series. Construct the MovingMedian object, then push() each | 
| Chris@1573 | 30  * new value in the time series and get() the median of the most | 
| Chris@1573 | 31  * recent window. The size of the window, and the percentile | 
| Chris@1573 | 32  * calculated, can both be changed after construction. | 
| Chris@1573 | 33  * | 
| Chris@1573 | 34  * Note that for even-sized windows, the "median" is taken to be the | 
| Chris@1573 | 35  * value at the start of the second half when sorted, e.g. for size 4, | 
| Chris@1573 | 36  * the element at index 2 (zero-based) in the sorted window. | 
| Chris@1573 | 37  * | 
| Chris@1573 | 38  * Not thread-safe. | 
| Chris@1573 | 39  */ | 
| Chris@1573 | 40 template <typename T> | 
| Chris@1573 | 41 class MovingMedian | 
| Chris@1573 | 42 { | 
| Chris@1573 | 43 public: | 
| Chris@1573 | 44     MovingMedian(int size, double percentile = 50.f) : | 
| Chris@1573 | 45         m_size(size), | 
| Chris@1573 | 46         m_percentile(percentile) { | 
| Chris@1573 | 47         if (size < 1) throw std::logic_error("size must be >= 1"); | 
| Chris@1573 | 48         m_frame = breakfastquay::allocate_and_zero<T>(size); | 
| Chris@1573 | 49 	m_sorted = breakfastquay::allocate_and_zero<T>(size); | 
| Chris@1573 | 50         calculateIndex(); | 
| Chris@1573 | 51     } | 
| Chris@1573 | 52 | 
| Chris@1573 | 53     ~MovingMedian() { | 
| Chris@1573 | 54         breakfastquay::deallocate(m_frame); | 
| Chris@1573 | 55         breakfastquay::deallocate(m_sorted); | 
| Chris@1573 | 56     } | 
| Chris@1573 | 57 | 
| Chris@1573 | 58     MovingMedian(const MovingMedian &) =delete; | 
| Chris@1573 | 59     MovingMedian &operator=(const MovingMedian &) =delete; | 
| Chris@1573 | 60 | 
| Chris@1573 | 61     void setPercentile(double p) { | 
| Chris@1573 | 62         m_percentile = p; | 
| Chris@1573 | 63         calculateIndex(); | 
| Chris@1573 | 64     } | 
| Chris@1573 | 65 | 
| Chris@1573 | 66     void push(T value) { | 
| Chris@1573 | 67         if (value != value) { | 
| Chris@1573 | 68             std::cerr << "WARNING: MovingMedian: NaN encountered" << std::endl; | 
| Chris@1573 | 69             value = T(); | 
| Chris@1573 | 70         } | 
| Chris@1573 | 71 	drop(m_frame[0]); | 
| Chris@1573 | 72         breakfastquay::v_move(m_frame, m_frame+1, m_size-1); | 
| Chris@1573 | 73 	m_frame[m_size-1] = value; | 
| Chris@1573 | 74 	put(value); | 
| Chris@1573 | 75     } | 
| Chris@1573 | 76 | 
| Chris@1573 | 77     T get() const { | 
| Chris@1573 | 78 	return m_sorted[m_index]; | 
| Chris@1573 | 79     } | 
| Chris@1573 | 80 | 
| Chris@1573 | 81     int size() const { | 
| Chris@1573 | 82         return m_size; | 
| Chris@1573 | 83     } | 
| Chris@1573 | 84 | 
| Chris@1573 | 85     void reset() { | 
| Chris@1573 | 86         breakfastquay::v_zero(m_frame, m_size); | 
| Chris@1573 | 87         breakfastquay::v_zero(m_sorted, m_size); | 
| Chris@1573 | 88     } | 
| Chris@1573 | 89 | 
| Chris@1573 | 90     void resize(int target) { | 
| Chris@1573 | 91         if (target == m_size) return; | 
| Chris@1573 | 92         int diff = std::abs(target - m_size); | 
| Chris@1573 | 93         if (target > m_size) { // grow | 
| Chris@1573 | 94             // we don't want to change the median, so fill spaces with it | 
| Chris@1573 | 95             T fillValue = get(); | 
| Chris@1573 | 96             m_frame = breakfastquay::reallocate(m_frame, m_size, target); | 
| Chris@1573 | 97             breakfastquay::v_move(m_frame + diff, m_frame, m_size); | 
| Chris@1573 | 98             breakfastquay::v_set(m_frame, fillValue, diff); | 
| Chris@1573 | 99             m_sorted = breakfastquay::reallocate(m_sorted, m_size, target); | 
| Chris@1573 | 100             for (int sz = m_size + 1; sz <= target; ++sz) { | 
| Chris@1573 | 101                 put(m_sorted, sz, fillValue); | 
| Chris@1573 | 102             } | 
| Chris@1573 | 103         } else { // shrink | 
| Chris@1573 | 104             for (int i = 0; i < diff; ++i) { | 
| Chris@1573 | 105                 drop(m_sorted, m_size - i, m_frame[i]); | 
| Chris@1573 | 106             } | 
| Chris@1573 | 107             m_sorted = breakfastquay::reallocate(m_sorted, m_size, target); | 
| Chris@1573 | 108             breakfastquay::v_move(m_frame, m_frame + diff, target); | 
| Chris@1573 | 109             m_frame = breakfastquay::reallocate(m_frame, m_size, target); | 
| Chris@1573 | 110         } | 
| Chris@1573 | 111 | 
| Chris@1573 | 112         m_size = target; | 
| Chris@1573 | 113         calculateIndex(); | 
| Chris@1573 | 114     } | 
| Chris@1573 | 115 | 
| Chris@1573 | 116     void checkIntegrity() const { | 
| Chris@1573 | 117         check(); | 
| Chris@1573 | 118     } | 
| Chris@1573 | 119 | 
| Chris@1573 | 120 private: | 
| Chris@1573 | 121     int m_size; | 
| Chris@1573 | 122     double m_percentile; | 
| Chris@1573 | 123     int m_index; | 
| Chris@1573 | 124     T *m_frame; | 
| Chris@1573 | 125     T *m_sorted; | 
| Chris@1573 | 126 | 
| Chris@1573 | 127     void calculateIndex() { | 
| Chris@1573 | 128         m_index = int((m_size * m_percentile) / 100.f); | 
| Chris@1573 | 129         if (m_index >= m_size) m_index = m_size-1; | 
| Chris@1573 | 130         if (m_index < 0) m_index = 0; | 
| Chris@1573 | 131     } | 
| Chris@1573 | 132 | 
| Chris@1573 | 133     void put(T value) { | 
| Chris@1573 | 134         put(m_sorted, m_size, value); | 
| Chris@1573 | 135     } | 
| Chris@1573 | 136 | 
| Chris@1573 | 137     static void put(T *const sorted, int size, T value) { | 
| Chris@1573 | 138 | 
| Chris@1573 | 139         // precondition: sorted points to size-1 sorted values, | 
| Chris@1573 | 140         // followed by an unused slot (i.e. only the first size-1 | 
| Chris@1573 | 141         // values of sorted are actually sorted) | 
| Chris@1573 | 142         // | 
| Chris@1573 | 143         // postcondition: sorted points to size sorted values | 
| Chris@1573 | 144 | 
| Chris@1573 | 145 	T *ptr = std::lower_bound(sorted, sorted + size - 1, value); | 
| Chris@1573 | 146         breakfastquay::v_move(ptr + 1, ptr, int(sorted + size - ptr) - 1); | 
| Chris@1573 | 147 	*ptr = value; | 
| Chris@1573 | 148     } | 
| Chris@1573 | 149 | 
| Chris@1573 | 150     void drop(T value) { | 
| Chris@1573 | 151         drop(m_sorted, m_size, value); | 
| Chris@1573 | 152     } | 
| Chris@1573 | 153 | 
| Chris@1573 | 154     static void drop(T *const sorted, int size, T value) { | 
| Chris@1573 | 155 | 
| Chris@1573 | 156         // precondition: sorted points to size sorted values, one of | 
| Chris@1573 | 157         // which is value | 
| Chris@1573 | 158         // | 
| Chris@1573 | 159         // postcondition: sorted points to size-1 sorted values, | 
| Chris@1573 | 160         // followed by a slot that has been reset to default value | 
| Chris@1573 | 161         // (i.e. only the first size-1 values of sorted are actually | 
| Chris@1573 | 162         // sorted) | 
| Chris@1573 | 163 | 
| Chris@1573 | 164 	T *ptr = std::lower_bound(sorted, sorted + size, value); | 
| Chris@1573 | 165 	if (*ptr != value) { | 
| Chris@1573 | 166             throw std::logic_error | 
| Chris@1573 | 167                 ("MovingMedian::drop: value being dropped is not in array"); | 
| Chris@1573 | 168         } | 
| Chris@1573 | 169         breakfastquay::v_move(ptr, ptr + 1, int(sorted + size - ptr) - 1); | 
| Chris@1573 | 170         sorted[size-1] = T(); | 
| Chris@1573 | 171     } | 
| Chris@1573 | 172 | 
| Chris@1573 | 173     void check() const { | 
| Chris@1573 | 174         bool good = true; | 
| Chris@1573 | 175         for (int i = 1; i < m_size; ++i) { | 
| Chris@1573 | 176             if (m_sorted[i] < m_sorted[i-1]) { | 
| Chris@1573 | 177                 std::cerr << "ERROR: MovingMedian::checkIntegrity: " | 
| Chris@1573 | 178                           << "mis-ordered elements in sorted array starting " | 
| Chris@1573 | 179                           << "at index " << i << std::endl; | 
| Chris@1573 | 180                 good = false; | 
| Chris@1573 | 181                 break; | 
| Chris@1573 | 182             } | 
| Chris@1573 | 183         } | 
| Chris@1573 | 184         for (int i = 0; i < m_size; ++i) { | 
| Chris@1573 | 185             bool found = false; | 
| Chris@1573 | 186             for (int j = 0; j < m_size; ++j) { | 
| Chris@1573 | 187                 if (m_sorted[j] == m_frame[i]) { | 
| Chris@1573 | 188                     found = true; | 
| Chris@1573 | 189                     break; | 
| Chris@1573 | 190                 } | 
| Chris@1573 | 191             } | 
| Chris@1573 | 192             if (!found) { | 
| Chris@1573 | 193                 std::cerr << "ERROR: MovingMedian::checkIntegrity: " | 
| Chris@1573 | 194                           << "element in frame at index " << i | 
| Chris@1573 | 195                           << " not found in sorted array" << std::endl; | 
| Chris@1573 | 196                 good = false; | 
| Chris@1573 | 197                 break; | 
| Chris@1573 | 198             } | 
| Chris@1573 | 199         } | 
| Chris@1573 | 200         for (int i = 0; i < m_size; ++i) { | 
| Chris@1573 | 201             bool found = false; | 
| Chris@1573 | 202             for (int j = 0; j < m_size; ++j) { | 
| Chris@1573 | 203                 if (m_sorted[i] == m_frame[j]) { | 
| Chris@1573 | 204                     found = true; | 
| Chris@1573 | 205                     break; | 
| Chris@1573 | 206                 } | 
| Chris@1573 | 207             } | 
| Chris@1573 | 208             if (!found) { | 
| Chris@1573 | 209                 std::cerr << "ERROR: MovingMedian::checkIntegrity: " | 
| Chris@1573 | 210                           << "element in sorted array at index " << i | 
| Chris@1573 | 211                           << " not found in source frame" << std::endl; | 
| Chris@1573 | 212                 good = false; | 
| Chris@1573 | 213                 break; | 
| Chris@1573 | 214             } | 
| Chris@1573 | 215         } | 
| Chris@1573 | 216         if (!good) { | 
| Chris@1573 | 217             std::cerr << "Frame contains:" << std::endl; | 
| Chris@1573 | 218             std::cerr << "[ "; | 
| Chris@1573 | 219             for (int j = 0; j < m_size; ++j) { | 
| Chris@1573 | 220                 std::cerr << m_frame[j] << " "; | 
| Chris@1573 | 221             } | 
| Chris@1573 | 222             std::cerr << "]" << std::endl; | 
| Chris@1573 | 223             std::cerr << "Sorted array contains:" << std::endl; | 
| Chris@1573 | 224             std::cerr << "[ "; | 
| Chris@1573 | 225             for (int j = 0; j < m_size; ++j) { | 
| Chris@1573 | 226                 std::cerr << m_sorted[j] << " "; | 
| Chris@1573 | 227             } | 
| Chris@1573 | 228             std::cerr << "]" << std::endl; | 
| Chris@1573 | 229             throw std::logic_error("MovingMedian failed integrity check"); | 
| Chris@1573 | 230         } | 
| Chris@1573 | 231     } | 
| Chris@1573 | 232 }; | 
| Chris@1573 | 233 | 
| Chris@1573 | 234 #endif | 
| Chris@1573 | 235 |