Browse Source
Uses a k-d tree to quickly find the best match for a color when generating the palette blending lookup table. https://en.wikipedia.org/wiki/K-d_tree This is 3x faster than the previous naive approach: ``` Benchmark Time CPU Time Old Time New CPU Old CPU New ----------------------------------------------------------------------------------------------------------------------------------- BM_GenerateBlendedLookupTable_pvalue 0.0002 0.0002 U Test, Repetitions: 10 vs 10 BM_GenerateBlendedLookupTable_mean -0.7153 -0.7153 18402641 5239051 18399111 5238025 BM_GenerateBlendedLookupTable_median -0.7153 -0.7153 18403261 5239042 18398841 5237497 BM_GenerateBlendedLookupTable_stddev -0.2775 +0.3858 2257 1631 1347 1867 BM_GenerateBlendedLookupTable_cv +1.5379 +3.8677 0 0 0 0 OVERALL_GEOMEAN -0.7153 -0.7153 0 0 0 0 ``` The distribution is somewhat poor with just 3 levels, so this can be improved further. For example, here is the leaf size distribution in the cathedral: ``` r0.g0.b0: 88 r0.g0.b1: 10 r0.g1.b0: 2 r0.g1.b1: 32 r1.g0.b0: 27 r1.g0.b1: 4 r1.g1.b0: 12 r1.g1.b1: 81 ```pull/8028/head
3 changed files with 183 additions and 17 deletions
@ -0,0 +1,171 @@
|
||||
#pragma once |
||||
|
||||
#include <algorithm> |
||||
#include <array> |
||||
#include <cstdint> |
||||
#include <limits> |
||||
#include <span> |
||||
#include <utility> |
||||
|
||||
#include <SDL.h> |
||||
|
||||
#include "utils/algorithm/container.hpp" |
||||
#include "utils/static_vector.hpp" |
||||
|
||||
namespace devilution { |
||||
|
||||
[[nodiscard]] inline uint32_t GetColorDistance(const SDL_Color &a, const std::array<uint8_t, 3> &b) |
||||
{ |
||||
const int diffr = a.r - b[0]; |
||||
const int diffg = a.g - b[1]; |
||||
const int diffb = a.b - b[2]; |
||||
return (diffr * diffr) + (diffg * diffg) + (diffb * diffb); |
||||
} |
||||
|
||||
/**
|
||||
* @brief A 3-level kd-tree used to find the nearest neighbor in the color space. |
||||
* |
||||
* Each level splits the space in half by red, green, and blue respectively. |
||||
*/ |
||||
class PaletteKdTree { |
||||
using RGB = std::array<uint8_t, 3>; |
||||
|
||||
public: |
||||
explicit PaletteKdTree(const SDL_Color palette[256]) |
||||
: palette_(palette) |
||||
, pivots_(getPivots(palette)) |
||||
{ |
||||
for (unsigned i = 0; i < 256; ++i) { |
||||
const SDL_Color &color = palette[i]; |
||||
auto &level1 = color.r < pivots_[0] ? tree_.first : tree_.second; |
||||
auto &level2 = color.g < pivots_[1] ? level1.first : level1.second; |
||||
auto &level3 = color.b < pivots_[2] ? level2.first : level2.second; |
||||
level3.emplace_back(i); |
||||
} |
||||
|
||||
// Uncomment the loop below to print the node distribution:
|
||||
// for (const bool r : { false, true }) {
|
||||
// for (const bool g : { false, true }) {
|
||||
// for (const bool b : { false, true }) {
|
||||
// printf("r%d.g%d.b%d: %d\n",
|
||||
// static_cast<int>(r), static_cast<int>(g), static_cast<int>(b),
|
||||
// static_cast<int>(getLeaf(r, g, b).size()));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
} |
||||
|
||||
[[nodiscard]] uint8_t findNearestNeighbor(const RGB &rgb) const |
||||
{ |
||||
const bool compR = rgb[0] < pivots_[0]; |
||||
const bool compG = rgb[1] < pivots_[1]; |
||||
const bool compB = rgb[2] < pivots_[2]; |
||||
|
||||
// Conceptually, we visit the tree recursively.
|
||||
// As the tree only has 3 levels, we fully unroll
|
||||
// the recursion here.
|
||||
uint8_t best; |
||||
uint32_t bestDiff = std::numeric_limits<uint32_t>::max(); |
||||
checkLeaf(compR, compG, compB, rgb, best, bestDiff); |
||||
if (shouldCheckNode(best, bestDiff, /*coord=*/2, rgb)) { |
||||
checkLeaf(compR, compG, !compB, rgb, best, bestDiff); |
||||
} |
||||
if (shouldCheckNode(best, bestDiff, /*coord=*/1, rgb)) { |
||||
checkLeaf(compR, !compG, compB, rgb, best, bestDiff); |
||||
if (shouldCheckNode(best, bestDiff, /*coord=*/2, rgb)) { |
||||
checkLeaf(compR, !compG, !compB, rgb, best, bestDiff); |
||||
} |
||||
} |
||||
if (shouldCheckNode(best, bestDiff, /*coord=*/0, rgb)) { |
||||
checkLeaf(!compR, compG, compB, rgb, best, bestDiff); |
||||
if (shouldCheckNode(best, bestDiff, /*coord=*/1, rgb)) { |
||||
checkLeaf(!compR, !compG, compB, rgb, best, bestDiff); |
||||
if (shouldCheckNode(best, bestDiff, /*coord=*/2, rgb)) { |
||||
checkLeaf(!compR, !compG, !compB, rgb, best, bestDiff); |
||||
} |
||||
} |
||||
if (shouldCheckNode(best, bestDiff, /*coord=*/2, rgb)) { |
||||
checkLeaf(!compR, compG, !compB, rgb, best, bestDiff); |
||||
} |
||||
} |
||||
return best; |
||||
} |
||||
|
||||
private: |
||||
static uint8_t getMedian(std::span<uint8_t, 256> elements) |
||||
{ |
||||
const auto middleItr = elements.begin() + (elements.size() / 2); |
||||
std::nth_element(elements.begin(), middleItr, elements.end()); |
||||
if (elements.size() % 2 == 0) { |
||||
const auto leftMiddleItr = std::max_element(elements.begin(), middleItr); |
||||
return (*leftMiddleItr + *middleItr) / 2; |
||||
} |
||||
return *middleItr; |
||||
} |
||||
|
||||
static std::array<uint8_t, 3> getPivots(const SDL_Color palette[256]) |
||||
{ |
||||
std::array<std::array<uint8_t, 256>, 3> coords; |
||||
for (unsigned i = 0; i < 256; ++i) { |
||||
coords[0][i] = palette[i].r; |
||||
coords[1][i] = palette[i].g; |
||||
coords[2][i] = palette[i].b; |
||||
} |
||||
return { getMedian(coords[0]), getMedian(coords[1]), getMedian(coords[2]) }; |
||||
} |
||||
|
||||
void checkLeaf(bool compR, bool compG, bool compB, const RGB &rgb, uint8_t &best, uint32_t &bestDiff) const |
||||
{ |
||||
const std::span<const uint8_t> leaf = getLeaf(compR, compG, compB); |
||||
uint8_t leafBest; |
||||
uint32_t leafBestDiff = bestDiff; |
||||
for (const uint8_t paletteIndex : leaf) { |
||||
const uint32_t diff = GetColorDistance(palette_[paletteIndex], rgb); |
||||
if (diff < leafBestDiff) { |
||||
leafBest = paletteIndex; |
||||
leafBestDiff = diff; |
||||
} |
||||
} |
||||
if (leafBestDiff < bestDiff) { |
||||
best = leafBest; |
||||
bestDiff = leafBestDiff; |
||||
} |
||||
} |
||||
|
||||
[[nodiscard]] bool shouldCheckNode(uint8_t best, uint32_t bestDiff, unsigned coord, const RGB &rgb) const |
||||
{ |
||||
// To see if we need to check a node's subtree, we compare the distance from the query
|
||||
// to the current best candidate vs the distance to the edge of the half-space represented
|
||||
// by the node.
|
||||
if (bestDiff == std::numeric_limits<uint32_t>::max()) return true; |
||||
const int delta = static_cast<int>(pivots_[coord]) - static_cast<int>(rgb[coord]); |
||||
return delta * delta < GetColorDistance(palette_[best], rgb); |
||||
} |
||||
|
||||
[[nodiscard]] std::span<const uint8_t> getLeaf(bool r, bool g, bool b) const |
||||
{ |
||||
const auto &level1 = r ? tree_.first : tree_.second; |
||||
const auto &level2 = g ? level1.first : level1.second; |
||||
const auto &level3 = b ? level2.first : level2.second; |
||||
return { level3 }; |
||||
} |
||||
|
||||
const SDL_Color *palette_; |
||||
std::array<uint8_t, 3> pivots_; |
||||
std::pair< |
||||
// r0
|
||||
std::pair< |
||||
// r0.g0.b{0, 1}
|
||||
std::pair<StaticVector<uint8_t, 256>, StaticVector<uint8_t, 256>>, |
||||
// r0.g1.b{0, 1}
|
||||
std::pair<StaticVector<uint8_t, 256>, StaticVector<uint8_t, 256>>>, |
||||
// r1
|
||||
std::pair< |
||||
// r1.g0.b{0, 1}
|
||||
std::pair<StaticVector<uint8_t, 256>, StaticVector<uint8_t, 256>>, |
||||
// r1.g1.b{0, 1}
|
||||
std::pair<StaticVector<uint8_t, 256>, StaticVector<uint8_t, 256>>>> |
||||
tree_; |
||||
}; |
||||
|
||||
} // namespace devilution
|
||||
Loading…
Reference in new issue