|
2 | 2 | #include "ColorConverter.hpp"
|
3 | 3 | #include <iostream>
|
4 | 4 | #include <algorithm>
|
| 5 | +#include <sstream> |
| 6 | +#include <unordered_map> |
5 | 7 |
|
6 | 8 | cmdplay::Asciifier::Asciifier(const std::string& brightnessLevels, int frameWidth, int frameHeight,
|
7 | 9 | bool useColors, bool useColorDithering, bool useTextDithering, bool useAccurateColors, bool useAccurateColorsFullPixel) :
|
@@ -32,6 +34,15 @@ cmdplay::Asciifier::Asciifier(const std::string& brightnessLevels, int frameWidt
|
32 | 34 |
|
33 | 35 | m_frameWidthWithStride = m_frameWidth * m_pixelStride;
|
34 | 36 | m_targetFramebufferSize = (m_frameWidthWithStride + 1) * m_frameHeight;
|
| 37 | + |
| 38 | + m_frameBuffer = (Pixel*)malloc(m_frameWidth * m_frameHeight * sizeof(Pixel)); |
| 39 | + m_backBuffer = (Pixel*)malloc(m_frameWidth * m_frameHeight * sizeof(Pixel)); |
| 40 | +} |
| 41 | + |
| 42 | +cmdplay::Asciifier::~Asciifier() |
| 43 | +{ |
| 44 | + free(m_frameBuffer); |
| 45 | + free(m_backBuffer); |
35 | 46 | }
|
36 | 47 |
|
37 | 48 | inline int16_t cmdplay::Asciifier::MapByteToArray(int16_t value)
|
@@ -129,9 +140,57 @@ inline std::string cmdplay::Asciifier::ByteAsPaddedString(uint8_t i)
|
129 | 140 | return out;
|
130 | 141 | }
|
131 | 142 |
|
132 |
| -inline bool cmdplay::Asciifier::ColorComponentNearlyEquals(uint8_t value, uint8_t other) |
| 143 | +inline bool cmdplay::Asciifier::ColorComponentNearlyEquals(uint8_t value, uint8_t other, uint8_t tolerance) |
133 | 144 | {
|
134 |
| - return std::abs(value - other) <= COLOR_BATCHING_TOLERANCE; |
| 145 | + return std::abs(value - other) <= tolerance; |
| 146 | +} |
| 147 | + |
| 148 | +float cmdplay::Asciifier::CalculateFrameBufferNoisiness() |
| 149 | +{ |
| 150 | + std::unordered_map<uint32_t, uint32_t> colorToOccurences; |
| 151 | + for (int i = 0; i < m_frameWidth * m_frameHeight; i++) |
| 152 | + { |
| 153 | + const uint8_t colorBatchedR = (m_frameBuffer[i].r / COLOR_BATCHING_TOLERANCE) * COLOR_BATCHING_TOLERANCE; |
| 154 | + const uint8_t colorBatchedG = (m_frameBuffer[i].g / COLOR_BATCHING_TOLERANCE) * COLOR_BATCHING_TOLERANCE; |
| 155 | + const uint8_t colorBatchedB = (m_frameBuffer[i].b / COLOR_BATCHING_TOLERANCE) * COLOR_BATCHING_TOLERANCE; |
| 156 | + uint32_t colorInt = colorBatchedR << 24 + colorBatchedG << 16 + colorBatchedB << 8; |
| 157 | + colorToOccurences[colorInt]++; |
| 158 | + } |
| 159 | + |
| 160 | + double entropy = 0.f; |
| 161 | + for (auto& kvp : colorToOccurences) |
| 162 | + { |
| 163 | + double p = (double)(kvp.second) / (m_frameWidth * m_frameHeight); |
| 164 | + if (p > 0) |
| 165 | + { |
| 166 | + entropy -= p * log2(p); |
| 167 | + } |
| 168 | + } |
| 169 | + |
| 170 | + const double linearAlpha = entropy / log2(2 ^ 24); // divide through the maximum amount of colors; |
| 171 | + const double expoAlpha = 1 - pow(1 - linearAlpha, 1.5f); |
| 172 | + return expoAlpha; |
| 173 | +} |
| 174 | + |
| 175 | +float cmdplay::Asciifier::CalulateFrameBufferToBackBufferDifference() |
| 176 | +{ |
| 177 | + double differenceR = 0.f; |
| 178 | + double differenceG = 0.f; |
| 179 | + double differenceB = 0.f; |
| 180 | + for (int i = 0; i < m_frameWidth * m_frameHeight; i++) |
| 181 | + { |
| 182 | + differenceR += std::abs(m_frameBuffer[i].r - m_backBuffer[i].r); |
| 183 | + differenceG += std::abs(m_frameBuffer[i].g - m_backBuffer[i].g); |
| 184 | + differenceB += std::abs(m_frameBuffer[i].b - m_backBuffer[i].b); |
| 185 | + } |
| 186 | + |
| 187 | + differenceR /= m_frameWidth * m_frameHeight * 255; |
| 188 | + differenceG /= m_frameWidth * m_frameHeight * 255; |
| 189 | + differenceB /= m_frameWidth * m_frameHeight * 255; |
| 190 | + |
| 191 | + double totalDifference = differenceR + differenceG + differenceB; |
| 192 | + totalDifference /= 3; |
| 193 | + return totalDifference; |
135 | 194 | }
|
136 | 195 |
|
137 | 196 | void cmdplay::Asciifier::ClearDitherErrors(float* buffer)
|
@@ -161,111 +220,164 @@ void cmdplay::Asciifier::WriteDitherError(int x, int y, float error, float* buff
|
161 | 220 | }
|
162 | 221 | }
|
163 | 222 |
|
164 |
| -std::string cmdplay::Asciifier::BuildFrame(const uint8_t* rgbData) |
| 223 | +std::string cmdplay::Asciifier::BuildFrame(const uint8_t* rgbData, bool fullRedraw) |
165 | 224 | {
|
166 |
| - if (m_useColorDithering) |
167 |
| - ClearDitherErrors(m_hDitherErrors.get()); |
168 |
| - |
169 |
| - if (m_useTextDithering) |
170 |
| - ClearDitherErrors(m_textDitherErrors.get()); |
| 225 | + if (m_useColors && m_useAccurateColors) |
| 226 | + { |
| 227 | + std::stringstream ss; |
171 | 228 |
|
172 |
| - auto asciiData = std::make_unique<char[]>(m_targetFramebufferSize + 1); |
173 |
| - char* asciiDataArr = asciiData.get(); |
174 |
| - for (int i = 0, scanX = 0; i < m_targetFramebufferSize; ++i) |
175 |
| - if (++scanX == m_frameWidthWithStride + 1) |
176 | 229 | {
|
177 |
| - scanX = 0; |
178 |
| - asciiDataArr[i] = '\n'; |
| 230 | + Pixel* temp = m_frameBuffer; |
| 231 | + m_frameBuffer = m_backBuffer; |
| 232 | + m_backBuffer = temp; |
179 | 233 | }
|
180 |
| - |
181 |
| - // Set null-terminator |
182 |
| - asciiData[m_targetFramebufferSize] = 0; |
183 |
| - |
184 |
| - int scanX = 0; |
185 |
| - int rowOffset = 0; |
186 |
| - int col = 0; |
187 |
| - int row = 0; |
188 |
| - for (int i = 0; i < m_frameSubpixelCount; i += 3) |
189 |
| - { |
190 |
| - // Max value = 255.000 |
191 |
| - int32_t pixelBrightness = |
192 |
| - rgbData[i] * PERCEIVED_LUMINANCE_R_FACTOR + |
193 |
| - rgbData[i + 1] * PERCEIVED_LUMINANCE_G_FACTOR + |
194 |
| - rgbData[i + 2] * PERCEIVED_LUMINANCE_B_FACTOR; |
195 | 234 |
|
196 |
| - int16_t brightnessIndex; |
197 |
| - if (m_useTextDithering) |
| 235 | + bool repositionCursor = false; |
| 236 | + int lastRow = 0; |
| 237 | + for (int i = 0; i < m_frameWidth * m_frameHeight; i++) |
198 | 238 | {
|
199 |
| - pixelBrightness += static_cast<int>(1000 * m_textDitherErrors[col + row * m_frameWidth]); |
200 |
| - int16_t trueBrightnessByte = pixelBrightness / 1000; |
201 |
| - brightnessIndex = MapByteToArray(trueBrightnessByte); |
202 |
| - int actualBrightnessByte = brightnessIndex * 255 / (m_brightnessLevelCount - 1); |
203 |
| - float brightnessError = (static_cast<int>(trueBrightnessByte) - actualBrightnessByte) * DITHER_FACTOR; |
204 |
| - WriteDitherError(col, row, brightnessError, m_textDitherErrors.get()); |
| 239 | + int32_t pixelBrightness = |
| 240 | + rgbData[i] * PERCEIVED_LUMINANCE_R_FACTOR + |
| 241 | + rgbData[i + 1] * PERCEIVED_LUMINANCE_G_FACTOR + |
| 242 | + rgbData[i + 2] * PERCEIVED_LUMINANCE_B_FACTOR; |
| 243 | + |
| 244 | + int16_t brightnessIndex = MapByteToArray(pixelBrightness / 1000); |
| 245 | + |
| 246 | + if (m_useAccurateColorsFullPixel) |
| 247 | + m_frameBuffer[i].c = ' '; |
| 248 | + else |
| 249 | + m_frameBuffer[i].c = ToCharUnchecked(brightnessIndex); |
| 250 | + |
| 251 | + m_frameBuffer[i].r = rgbData[i * 3]; |
| 252 | + m_frameBuffer[i].g = rgbData[i * 3 + 1]; |
| 253 | + m_frameBuffer[i].b = rgbData[i * 3 + 2]; |
205 | 254 | }
|
206 |
| - else |
207 |
| - brightnessIndex = MapByteToArray(pixelBrightness / 1000); |
208 | 255 |
|
209 |
| - if (m_useColors && m_useAccurateColors) |
| 256 | + const double frameNoisinessAlpha = CalculateFrameBufferNoisiness(); |
| 257 | + const double frameDifferenceAlpha = CalulateFrameBufferToBackBufferDifference(); |
| 258 | + const double totalAlpha = frameNoisinessAlpha * frameDifferenceAlpha; |
| 259 | + |
| 260 | + for (int i = 0; i < m_frameWidth * m_frameHeight; i++) |
210 | 261 | {
|
211 |
| - std::string outString; |
212 |
| - |
213 |
| - // if the color we have last set rougly equals our current color, |
214 |
| - // we don't set our new color to reduce the stress on the console color interpreter |
215 |
| - if (ColorComponentNearlyEquals(rgbData[i], m_lastSetColor[0]) && |
216 |
| - ColorComponentNearlyEquals(rgbData[i + 1], m_lastSetColor[1]) && |
217 |
| - ColorComponentNearlyEquals(rgbData[i + 2], m_lastSetColor[2])) |
218 |
| - outString = "\x1BX000000000000000\x1B\\"; // string message that the console ignores |
219 |
| - else |
| 262 | + if (ColorComponentNearlyEquals(m_frameBuffer[i].r, m_backBuffer[i].r, COLOR_REDRAW_TOLERANCE * totalAlpha) && |
| 263 | + ColorComponentNearlyEquals(m_frameBuffer[i].g, m_backBuffer[i].g, COLOR_REDRAW_TOLERANCE * totalAlpha) && |
| 264 | + ColorComponentNearlyEquals(m_frameBuffer[i].b, m_backBuffer[i].b, COLOR_REDRAW_TOLERANCE * totalAlpha) && |
| 265 | + !fullRedraw) |
220 | 266 | {
|
221 |
| - if (m_useAccurateColorsFullPixel) |
222 |
| - outString = "\x1B[48;2;" + ByteAsPaddedString(rgbData[i]) + ";" + |
223 |
| - ByteAsPaddedString(rgbData[i + 1]) + ";" + ByteAsPaddedString(rgbData[i + 2]) + "m"; |
224 |
| - else |
225 |
| - outString = "\x1B[38;2;" + ByteAsPaddedString(rgbData[i]) + ";" + |
226 |
| - ByteAsPaddedString(rgbData[i + 1]) + ";" + ByteAsPaddedString(rgbData[i + 2]) + "m"; |
227 |
| - |
228 |
| - m_lastSetColor[0] = rgbData[i]; |
229 |
| - m_lastSetColor[1] = rgbData[i + 1]; |
230 |
| - m_lastSetColor[2] = rgbData[i + 2]; |
| 267 | + // we skip this pixel as the color on the screen is close enough |
| 268 | + repositionCursor = true; |
| 269 | + |
| 270 | + // set the color of the framebuffer back to the color that's on the screen |
| 271 | + m_frameBuffer[i].c = m_frameBuffer[i].c; |
| 272 | + m_frameBuffer[i].r = m_frameBuffer[i].r; |
| 273 | + m_frameBuffer[i].g = m_frameBuffer[i].g; |
| 274 | + m_frameBuffer[i].b = m_frameBuffer[i].b; |
| 275 | + |
| 276 | + //ss << "B"; |
| 277 | + continue; |
231 | 278 | }
|
232 | 279 |
|
233 |
| - for (int i = 0; i < outString.size(); ++i) |
234 |
| - asciiDataArr[rowOffset + scanX + i] = outString.at(i); |
| 280 | + if (repositionCursor) |
| 281 | + { |
| 282 | + ss << "\x1B[" << std::to_string(i / m_frameWidth + 1) << ";" << std::to_string(i % m_frameWidth + 1) << "H"; |
| 283 | + repositionCursor = false; |
| 284 | + } |
235 | 285 |
|
236 |
| - if (m_useAccurateColorsFullPixel) |
237 |
| - asciiDataArr[rowOffset + scanX + outString.size()] = ' '; |
238 |
| - else |
239 |
| - asciiDataArr[rowOffset + scanX + outString.size()] = ToCharUnchecked(brightnessIndex); |
| 286 | + if (!(ColorComponentNearlyEquals(m_frameBuffer[i].r, m_lastSetColor[0], COLOR_BATCHING_TOLERANCE) && |
| 287 | + ColorComponentNearlyEquals(m_frameBuffer[i].g, m_lastSetColor[1], COLOR_BATCHING_TOLERANCE) && |
| 288 | + ColorComponentNearlyEquals(m_frameBuffer[i].b, m_lastSetColor[2], COLOR_BATCHING_TOLERANCE))) |
| 289 | + { |
| 290 | + ss << "\x1B[48;2;" << std::to_string(m_frameBuffer[i].r) << ";" << std::to_string(m_frameBuffer[i].g) << ";" << std::to_string(m_frameBuffer[i].b) << "m"; |
| 291 | + m_lastSetColor[0] = m_frameBuffer[i].r; |
| 292 | + m_lastSetColor[1] = m_frameBuffer[i].g; |
| 293 | + m_lastSetColor[2] = m_frameBuffer[i].b; |
| 294 | + } |
| 295 | + |
| 296 | + ss << m_frameBuffer[i].c; |
| 297 | + |
| 298 | + if (i / m_frameWidth != lastRow) |
| 299 | + { |
| 300 | + repositionCursor = true; |
| 301 | + lastRow = i / m_frameWidth; |
| 302 | + } |
240 | 303 | }
|
241 |
| - else if (m_useColors) |
| 304 | + |
| 305 | + return ss.str(); |
| 306 | + } |
| 307 | + else // I cant port that shit over man |
| 308 | + { |
| 309 | + if (m_useColorDithering) |
| 310 | + ClearDitherErrors(m_hDitherErrors.get()); |
| 311 | + |
| 312 | + if (m_useTextDithering) |
| 313 | + ClearDitherErrors(m_textDitherErrors.get()); |
| 314 | + |
| 315 | + auto asciiData = std::make_unique<char[]>(m_targetFramebufferSize + 1); |
| 316 | + char* asciiDataArr = asciiData.get(); |
| 317 | + for (int i = 0, scanX = 0; i < m_targetFramebufferSize; ++i) |
| 318 | + if (++scanX == m_frameWidthWithStride + 1) |
| 319 | + { |
| 320 | + scanX = 0; |
| 321 | + asciiDataArr[i] = '\n'; |
| 322 | + } |
| 323 | + |
| 324 | + // Set null-terminator |
| 325 | + asciiData[m_targetFramebufferSize] = 0; |
| 326 | + |
| 327 | + int scanX = 0; |
| 328 | + int rowOffset = 0; |
| 329 | + int col = 0; |
| 330 | + int row = 0; |
| 331 | + for (int i = 0; i < m_frameSubpixelCount; i += 3) |
242 | 332 | {
|
243 |
| - auto color = |
244 |
| - m_useColorDithering ? |
245 |
| - GetColorDithered(rgbData[i], rgbData[i + 1], rgbData[i + 2], col, row) : |
246 |
| - GetColor(rgbData[i], rgbData[i + 1], rgbData[i + 2]); |
247 |
| - asciiDataArr[rowOffset + scanX] = '\x1B'; |
248 |
| - asciiDataArr[rowOffset + scanX + 1] = '['; |
249 |
| - asciiDataArr[rowOffset + scanX + 2] = color[0]; |
250 |
| - asciiDataArr[rowOffset + scanX + 3] = color[1]; |
251 |
| - asciiDataArr[rowOffset + scanX + 4] = 'm'; |
252 |
| - asciiDataArr[rowOffset + scanX + 5] = ToChar(brightnessIndex); |
253 |
| - } |
254 |
| - else |
255 |
| - asciiDataArr[rowOffset + scanX] = m_useTextDithering ? |
| 333 | + // Max value = 255.000 |
| 334 | + int32_t pixelBrightness = |
| 335 | + rgbData[i] * PERCEIVED_LUMINANCE_R_FACTOR + |
| 336 | + rgbData[i + 1] * PERCEIVED_LUMINANCE_G_FACTOR + |
| 337 | + rgbData[i + 2] * PERCEIVED_LUMINANCE_B_FACTOR; |
| 338 | + |
| 339 | + int16_t brightnessIndex; |
| 340 | + if (m_useTextDithering) |
| 341 | + { |
| 342 | + pixelBrightness += static_cast<int>(1000 * m_textDitherErrors[col + row * m_frameWidth]); |
| 343 | + int16_t trueBrightnessByte = pixelBrightness / 1000; |
| 344 | + brightnessIndex = MapByteToArray(trueBrightnessByte); |
| 345 | + int actualBrightnessByte = brightnessIndex * 255 / (m_brightnessLevelCount - 1); |
| 346 | + float brightnessError = (static_cast<int>(trueBrightnessByte) - actualBrightnessByte) * DITHER_FACTOR; |
| 347 | + WriteDitherError(col, row, brightnessError, m_textDitherErrors.get()); |
| 348 | + } |
| 349 | + else |
| 350 | + brightnessIndex = MapByteToArray(pixelBrightness / 1000); |
| 351 | + |
| 352 | + if (m_useColors) |
| 353 | + { |
| 354 | + auto color = |
| 355 | + m_useColorDithering ? |
| 356 | + GetColorDithered(rgbData[i], rgbData[i + 1], rgbData[i + 2], col, row) : |
| 357 | + GetColor(rgbData[i], rgbData[i + 1], rgbData[i + 2]); |
| 358 | + asciiDataArr[rowOffset + scanX] = '\x1B'; |
| 359 | + asciiDataArr[rowOffset + scanX + 1] = '['; |
| 360 | + asciiDataArr[rowOffset + scanX + 2] = color[0]; |
| 361 | + asciiDataArr[rowOffset + scanX + 3] = color[1]; |
| 362 | + asciiDataArr[rowOffset + scanX + 4] = 'm'; |
| 363 | + asciiDataArr[rowOffset + scanX + 5] = ToChar(brightnessIndex); |
| 364 | + } |
| 365 | + else |
| 366 | + asciiDataArr[rowOffset + scanX] = m_useTextDithering ? |
256 | 367 | ToChar(brightnessIndex) : ToCharUnchecked(brightnessIndex);
|
257 | 368 |
|
258 |
| - scanX += m_pixelStride; |
259 |
| - ++col; |
260 |
| - if (scanX == m_frameWidthWithStride) |
261 |
| - { |
262 |
| - scanX = 0; |
263 |
| - rowOffset += m_frameWidthWithStride + 1; |
264 |
| - ++row; |
265 |
| - col = 0; |
266 |
| - m_lastBrightnessError = 0; |
| 369 | + scanX += m_pixelStride; |
| 370 | + ++col; |
| 371 | + if (scanX == m_frameWidthWithStride) |
| 372 | + { |
| 373 | + scanX = 0; |
| 374 | + rowOffset += m_frameWidthWithStride + 1; |
| 375 | + ++row; |
| 376 | + col = 0; |
| 377 | + m_lastBrightnessError = 0; |
| 378 | + } |
267 | 379 | }
|
268 |
| - } |
269 | 380 |
|
270 |
| - return std::string(asciiDataArr); |
| 381 | + return std::string(asciiDataArr); |
| 382 | + } |
271 | 383 | }
|
0 commit comments