A fork of https://github.com/crosspoint-reader/crosspoint-reader
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at records-reader 322 lines 9.2 kB view raw
1#pragma once 2 3#include <cstdint> 4#include <cstring> 5 6struct BmpHeader; 7 8// Helper functions 9uint8_t quantize(int gray, int x, int y); 10uint8_t quantizeSimple(int gray); 11uint8_t quantize1bit(int gray, int x, int y); 12int adjustPixel(int gray); 13 14enum class BmpRowOrder { BottomUp, TopDown }; 15 16// Populates a 1-bit BMP header in the provided memory. 17void createBmpHeader(BmpHeader* bmpHeader, int width, int height, BmpRowOrder rowOrder); 18 19// 1-bit Atkinson dithering - better quality than noise dithering for thumbnails 20// Error distribution pattern (same as 2-bit but quantizes to 2 levels): 21// X 1/8 1/8 22// 1/8 1/8 1/8 23// 1/8 24class Atkinson1BitDitherer { 25 public: 26 explicit Atkinson1BitDitherer(int width) : width(width) { 27 errorRow0 = new int16_t[width + 4](); // Current row 28 errorRow1 = new int16_t[width + 4](); // Next row 29 errorRow2 = new int16_t[width + 4](); // Row after next 30 } 31 32 ~Atkinson1BitDitherer() { 33 delete[] errorRow0; 34 delete[] errorRow1; 35 delete[] errorRow2; 36 } 37 38 // EXPLICITLY DELETE THE COPY CONSTRUCTOR 39 Atkinson1BitDitherer(const Atkinson1BitDitherer& other) = delete; 40 41 // EXPLICITLY DELETE THE COPY ASSIGNMENT OPERATOR 42 Atkinson1BitDitherer& operator=(const Atkinson1BitDitherer& other) = delete; 43 44 uint8_t processPixel(int gray, int x) { 45 // Apply brightness/contrast/gamma adjustments 46 gray = adjustPixel(gray); 47 48 // Add accumulated error 49 int adjusted = gray + errorRow0[x + 2]; 50 if (adjusted < 0) adjusted = 0; 51 if (adjusted > 255) adjusted = 255; 52 53 // Quantize to 2 levels (1-bit): 0 = black, 1 = white 54 uint8_t quantized; 55 int quantizedValue; 56 if (adjusted < 128) { 57 quantized = 0; 58 quantizedValue = 0; 59 } else { 60 quantized = 1; 61 quantizedValue = 255; 62 } 63 64 // Calculate error (only distribute 6/8 = 75%) 65 int error = (adjusted - quantizedValue) >> 3; // error/8 66 67 // Distribute 1/8 to each of 6 neighbors 68 errorRow0[x + 3] += error; // Right 69 errorRow0[x + 4] += error; // Right+1 70 errorRow1[x + 1] += error; // Bottom-left 71 errorRow1[x + 2] += error; // Bottom 72 errorRow1[x + 3] += error; // Bottom-right 73 errorRow2[x + 2] += error; // Two rows down 74 75 return quantized; 76 } 77 78 void nextRow() { 79 int16_t* temp = errorRow0; 80 errorRow0 = errorRow1; 81 errorRow1 = errorRow2; 82 errorRow2 = temp; 83 memset(errorRow2, 0, (width + 4) * sizeof(int16_t)); 84 } 85 86 void reset() { 87 memset(errorRow0, 0, (width + 4) * sizeof(int16_t)); 88 memset(errorRow1, 0, (width + 4) * sizeof(int16_t)); 89 memset(errorRow2, 0, (width + 4) * sizeof(int16_t)); 90 } 91 92 private: 93 int width; 94 int16_t* errorRow0; 95 int16_t* errorRow1; 96 int16_t* errorRow2; 97}; 98 99// Atkinson dithering - distributes only 6/8 (75%) of error for cleaner results 100// Error distribution pattern: 101// X 1/8 1/8 102// 1/8 1/8 1/8 103// 1/8 104// Less error buildup = fewer artifacts than Floyd-Steinberg 105class AtkinsonDitherer { 106 public: 107 explicit AtkinsonDitherer(int width) : width(width) { 108 errorRow0 = new int16_t[width + 4](); // Current row 109 errorRow1 = new int16_t[width + 4](); // Next row 110 errorRow2 = new int16_t[width + 4](); // Row after next 111 } 112 113 ~AtkinsonDitherer() { 114 delete[] errorRow0; 115 delete[] errorRow1; 116 delete[] errorRow2; 117 } 118 // **1. EXPLICITLY DELETE THE COPY CONSTRUCTOR** 119 AtkinsonDitherer(const AtkinsonDitherer& other) = delete; 120 121 // **2. EXPLICITLY DELETE THE COPY ASSIGNMENT OPERATOR** 122 AtkinsonDitherer& operator=(const AtkinsonDitherer& other) = delete; 123 124 uint8_t processPixel(int gray, int x) { 125 // Add accumulated error 126 int adjusted = gray + errorRow0[x + 2]; 127 if (adjusted < 0) adjusted = 0; 128 if (adjusted > 255) adjusted = 255; 129 130 // Quantize to 4 levels 131 uint8_t quantized; 132 int quantizedValue; 133 if (false) { // original thresholds 134 if (adjusted < 43) { 135 quantized = 0; 136 quantizedValue = 0; 137 } else if (adjusted < 128) { 138 quantized = 1; 139 quantizedValue = 85; 140 } else if (adjusted < 213) { 141 quantized = 2; 142 quantizedValue = 170; 143 } else { 144 quantized = 3; 145 quantizedValue = 255; 146 } 147 } else { // fine-tuned to X4 eink display 148 if (adjusted < 30) { 149 quantized = 0; 150 quantizedValue = 15; 151 } else if (adjusted < 50) { 152 quantized = 1; 153 quantizedValue = 30; 154 } else if (adjusted < 140) { 155 quantized = 2; 156 quantizedValue = 80; 157 } else { 158 quantized = 3; 159 quantizedValue = 210; 160 } 161 } 162 163 // Calculate error (only distribute 6/8 = 75%) 164 int error = (adjusted - quantizedValue) >> 3; // error/8 165 166 // Distribute 1/8 to each of 6 neighbors 167 errorRow0[x + 3] += error; // Right 168 errorRow0[x + 4] += error; // Right+1 169 errorRow1[x + 1] += error; // Bottom-left 170 errorRow1[x + 2] += error; // Bottom 171 errorRow1[x + 3] += error; // Bottom-right 172 errorRow2[x + 2] += error; // Two rows down 173 174 return quantized; 175 } 176 177 void nextRow() { 178 int16_t* temp = errorRow0; 179 errorRow0 = errorRow1; 180 errorRow1 = errorRow2; 181 errorRow2 = temp; 182 memset(errorRow2, 0, (width + 4) * sizeof(int16_t)); 183 } 184 185 void reset() { 186 memset(errorRow0, 0, (width + 4) * sizeof(int16_t)); 187 memset(errorRow1, 0, (width + 4) * sizeof(int16_t)); 188 memset(errorRow2, 0, (width + 4) * sizeof(int16_t)); 189 } 190 191 private: 192 int width; 193 int16_t* errorRow0; 194 int16_t* errorRow1; 195 int16_t* errorRow2; 196}; 197 198// Floyd-Steinberg error diffusion dithering with serpentine scanning 199// Serpentine scanning alternates direction each row to reduce "worm" artifacts 200// Error distribution pattern (left-to-right): 201// X 7/16 202// 3/16 5/16 1/16 203// Error distribution pattern (right-to-left, mirrored): 204// 1/16 5/16 3/16 205// 7/16 X 206class FloydSteinbergDitherer { 207 public: 208 explicit FloydSteinbergDitherer(int width) : width(width), rowCount(0) { 209 errorCurRow = new int16_t[width + 2](); // +2 for boundary handling 210 errorNextRow = new int16_t[width + 2](); 211 } 212 213 ~FloydSteinbergDitherer() { 214 delete[] errorCurRow; 215 delete[] errorNextRow; 216 } 217 218 // **1. EXPLICITLY DELETE THE COPY CONSTRUCTOR** 219 FloydSteinbergDitherer(const FloydSteinbergDitherer& other) = delete; 220 221 // **2. EXPLICITLY DELETE THE COPY ASSIGNMENT OPERATOR** 222 FloydSteinbergDitherer& operator=(const FloydSteinbergDitherer& other) = delete; 223 224 // Process a single pixel and return quantized 2-bit value 225 // x is the logical x position (0 to width-1), direction handled internally 226 uint8_t processPixel(int gray, int x) { 227 // Add accumulated error to this pixel 228 int adjusted = gray + errorCurRow[x + 1]; 229 230 // Clamp to valid range 231 if (adjusted < 0) adjusted = 0; 232 if (adjusted > 255) adjusted = 255; 233 234 // Quantize to 4 levels (0, 85, 170, 255) 235 uint8_t quantized; 236 int quantizedValue; 237 if (false) { // original thresholds 238 if (adjusted < 43) { 239 quantized = 0; 240 quantizedValue = 0; 241 } else if (adjusted < 128) { 242 quantized = 1; 243 quantizedValue = 85; 244 } else if (adjusted < 213) { 245 quantized = 2; 246 quantizedValue = 170; 247 } else { 248 quantized = 3; 249 quantizedValue = 255; 250 } 251 } else { // fine-tuned to X4 eink display 252 if (adjusted < 30) { 253 quantized = 0; 254 quantizedValue = 15; 255 } else if (adjusted < 50) { 256 quantized = 1; 257 quantizedValue = 30; 258 } else if (adjusted < 140) { 259 quantized = 2; 260 quantizedValue = 80; 261 } else { 262 quantized = 3; 263 quantizedValue = 210; 264 } 265 } 266 267 // Calculate error 268 int error = adjusted - quantizedValue; 269 270 // Distribute error to neighbors (serpentine: direction-aware) 271 if (!isReverseRow()) { 272 // Left to right: standard distribution 273 // Right: 7/16 274 errorCurRow[x + 2] += (error * 7) >> 4; 275 // Bottom-left: 3/16 276 errorNextRow[x] += (error * 3) >> 4; 277 // Bottom: 5/16 278 errorNextRow[x + 1] += (error * 5) >> 4; 279 // Bottom-right: 1/16 280 errorNextRow[x + 2] += (error) >> 4; 281 } else { 282 // Right to left: mirrored distribution 283 // Left: 7/16 284 errorCurRow[x] += (error * 7) >> 4; 285 // Bottom-right: 3/16 286 errorNextRow[x + 2] += (error * 3) >> 4; 287 // Bottom: 5/16 288 errorNextRow[x + 1] += (error * 5) >> 4; 289 // Bottom-left: 1/16 290 errorNextRow[x] += (error) >> 4; 291 } 292 293 return quantized; 294 } 295 296 // Call at the end of each row to swap buffers 297 void nextRow() { 298 // Swap buffers 299 int16_t* temp = errorCurRow; 300 errorCurRow = errorNextRow; 301 errorNextRow = temp; 302 // Clear the next row buffer 303 memset(errorNextRow, 0, (width + 2) * sizeof(int16_t)); 304 rowCount++; 305 } 306 307 // Check if current row should be processed in reverse 308 bool isReverseRow() const { return (rowCount & 1) != 0; } 309 310 // Reset for a new image or MCU block 311 void reset() { 312 memset(errorCurRow, 0, (width + 2) * sizeof(int16_t)); 313 memset(errorNextRow, 0, (width + 2) * sizeof(int16_t)); 314 rowCount = 0; 315 } 316 317 private: 318 int width; 319 int rowCount; 320 int16_t* errorCurRow; 321 int16_t* errorNextRow; 322};