Haio 0.2.X
Homebrew Asset IO/CDN
Loading...
Searching...
No Matches
quantise.cpp
Go to the documentation of this file.
1#include <haio_palette.hpp>
2#include <haio_util.hpp>
3
4#include <algorithm>
5#include <array>
6#include <cmath>
7#include <limits>
8#include <string>
9
10namespace {
11
12struct Rgb {
13 int r = 0;
14 int g = 0;
15 int b = 0;
16};
17
18Rgb unpack(uint32_t colour) {
19 return Rgb{static_cast<int>((colour >> 16) & 0xFF),
20 static_cast<int>((colour >> 8) & 0xFF),
21 static_cast<int>(colour & 0xFF)};
22}
23
24/**
25 * squared distance, weighted the way the eye weighs the channels.
26 *
27 * plain rgb distance calls a dark blue and a dark green neighbours, because it counts
28 * every channel the same while the eye counts green about six times more than blue.
29 */
30int distance(const Rgb& a, const Rgb& b) {
31 const auto dr = a.r - b.r;
32 const auto dg = a.g - b.g;
33 const auto db = a.b - b.b;
34 return 2 * dr * dr + 4 * dg * dg + 3 * db * db;
35}
36
37size_t nearest(const std::vector<Rgb>& palette, const Rgb& wanted) {
38 size_t best = 0;
39 int closest = distance(palette[0], wanted);
40
41 for (size_t at = 1; at < palette.size(); at++) {
42 if (const auto here = distance(palette[at], wanted); here < closest) {
43 closest = here;
44 best = at;
45 }
46 }
47 return best;
48}
49
50int clampByte(int value) { return value < 0 ? 0 : value > 255 ? 255 : value; }
51
52}
53
54namespace Haio {
55
56std::optional<Dither> ditherNamed(std::string_view name) {
57 if (name == "nearest") return Dither::Nearest;
58 if (name == "bayer") return Dither::Bayer;
59 if (name == "floyd") return Dither::Floyd;
60 if (name == "error") return Dither::Strict;
61 return std::nullopt;
62}
63
64std::optional<Limit> limitNamed(std::string_view name) {
65 if (name == "sort") return Limit::Sort;
66 if (name == "spread") return Limit::Spread;
67 return std::nullopt;
68}
69
71 std::vector<uint32_t> entries, size_t most, Limit how) {
72 if (most == 0) {
73 return std::unexpected(Error{ErrorCode::InvalidInput, "a limit of zero leaves no colours to draw with"});
74 }
75 if (entries.size() <= most) return entries;
76
77 std::vector<Rgb> palette;
78 palette.reserve(entries.size());
79 for (const auto colour : entries) palette.push_back(unpack(colour));
80
81 // how many pixels would land on each colour if the whole palette were available
82 std::vector<size_t> votes(entries.size(), 0);
83 for (size_t at = 0; at + 3 < image.data.size(); at += 4) {
84 votes[nearest(palette, Rgb{image.data[at + 0], image.data[at + 1], image.data[at + 2]})]++;
85 }
86
87 std::vector<size_t> order;
88 order.reserve(most);
89
90 if (how == Limit::Sort) {
91 std::vector<size_t> all(entries.size());
92 for (size_t at = 0; at < all.size(); at++) all[at] = at;
93
94 // most used first, and the original order breaks ties so the result is the
95 // same every run rather than whatever the sort happened to do
96 std::ranges::stable_sort(all, [&](size_t a, size_t b) { return votes[a] > votes[b]; });
97 order.assign(all.begin(), all.begin() + most);
98 } else {
99 std::vector<bool> taken(entries.size(), false);
100
101 // the first is simply the most used: there is nothing yet to be far from
102 size_t first = 0;
103 for (size_t at = 1; at < votes.size(); at++) {
104 if (votes[at] > votes[first]) first = at;
105 }
106 order.push_back(first);
107 taken[first] = true;
108
109 while (order.size() < most) {
110 size_t best = entries.size();
111 double bestScore = -1;
112
113 for (size_t at = 0; at < entries.size(); at++) {
114 if (taken[at]) continue;
115
116 // how far this colour is from the nearest one already kept
117 int apart = std::numeric_limits<int>::max();
118 for (const auto chosen : order) {
119 apart = std::min(apart, distance(palette[at], palette[chosen]));
120 }
121
122 // count times distance: rare and distinct beats common and duplicate
123 const auto score = static_cast<double>(votes[at]) * static_cast<double>(apart);
124 if (score > bestScore) {
125 bestScore = score;
126 best = at;
127 }
128 }
129
130 // every remaining colour is unused and identical to something kept
131 if (best == entries.size() || bestScore <= 0) break;
132 order.push_back(best);
133 taken[best] = true;
134 }
135 }
136
137 // back into palette order, so a picture keeps the sequence its colours came in
138 std::ranges::sort(order);
139
140 std::vector<uint32_t> kept;
141 kept.reserve(most);
142 for (const auto at : order) kept.push_back(entries[at]);
143 return kept;
144}
145
147 std::vector<uint32_t> entries, Dither how) {
148 if (entries.empty()) {
149 return std::unexpected(Error{ErrorCode::InvalidInput, "a palette with no colours cannot hold a picture"});
150 }
151 if (entries.size() > 256) {
152 return std::unexpected(Error{ErrorCode::InvalidInput, "a palette holds at most 256 colours"});
153 }
154
155 const auto width = static_cast<size_t>(image.width);
156 const auto height = static_cast<size_t>(image.height);
157 if (image.data.size() != width * height * 4) {
158 return std::unexpected(Error{ErrorCode::InvalidInput, "invalid rgba8888 image for palette conversion"});
159 }
160
161 std::vector<Rgb> palette;
162 palette.reserve(entries.size());
163 for (const auto colour : entries) palette.push_back(unpack(colour));
164
165 std::vector<uint8_t> indices(width * height);
166
167 // floyd carries the error forward, so it needs somewhere to put it; the others
168 // decide each pixel on its own and never look at this
169 std::vector<Rgb> carried(how == Dither::Floyd ? width * height : 0);
170
171 // how far a threshold may push a channel: the average gap between neighbouring
172 // colours, so a dense palette dithers gently and a sparse one hard
173 const int spread = 256 / static_cast<int>(std::max<size_t>(entries.size(), 2));
174
175 // a sparse palette needs a coarse matrix and a dense one a fine matrix: with four
176 // colours a 2x2 is already enough steps, while 256 wants all of an 8x8
177 const int matrix = Util::Dither::bayerSideFor(static_cast<int>(entries.size()));
178
179 for (size_t y = 0; y < height; y++) {
180 for (size_t x = 0; x < width; x++) {
181 const auto at = (y * width + x) * 4;
182 Rgb wanted{image.data[at + 0], image.data[at + 1], image.data[at + 2]};
183
184 if (how == Dither::Bayer) {
185 // centred on zero, so the matrix nudges a colour both ways rather
186 // than only ever making it brighter
187 const auto half = matrix * matrix / 2;
188 const auto threshold = Util::Dither::bayer(matrix, static_cast<int>(x % matrix),
189 static_cast<int>(y % matrix)) - half;
190 wanted.r = clampByte(wanted.r + threshold * spread / half);
191 wanted.g = clampByte(wanted.g + threshold * spread / half);
192 wanted.b = clampByte(wanted.b + threshold * spread / half);
193 } else if (how == Dither::Floyd) {
194 const auto& error = carried[y * width + x];
195 wanted.r = clampByte(wanted.r + error.r);
196 wanted.g = clampByte(wanted.g + error.g);
197 wanted.b = clampByte(wanted.b + error.b);
198 }
199
200 const auto chosen = nearest(palette, wanted);
201
202 if (how == Dither::Strict && distance(palette[chosen], wanted) != 0) {
203 return std::unexpected(Error{ErrorCode::InvalidInput,
204 "the colour at " + std::to_string(x) + "," + std::to_string(y)
205 + " is not in the palette, and -filter error means every one must be"});
206 }
207 indices[y * width + x] = static_cast<uint8_t>(chosen);
208
209 if (how != Dither::Floyd) continue;
210
211 // the part that did not fit goes to the neighbours, in the proportions
212 // floyd and steinberg landed on: 7 right, 3 below left, 5 below, 1 after
213 const auto& got = palette[chosen];
214 const Rgb left{wanted.r - got.r, wanted.g - got.g, wanted.b - got.b};
215
216 const auto spill = [&](size_t nx, size_t ny, int part) {
217 if (nx >= width || ny >= height) return;
218 auto& into = carried[ny * width + nx];
219 into.r += left.r * part / 16;
220 into.g += left.g * part / 16;
221 into.b += left.b * part / 16;
222 };
223 spill(x + 1, y, 7);
224 if (x > 0) spill(x - 1, y + 1, 3);
225 spill(x, y + 1, 5);
226 spill(x + 1, y + 1, 1);
227 }
228 }
229
230 return Image<Color::PALETTE>{image.width, image.height, std::move(indices), std::move(entries)};
231}
232
233}
constexpr int bayer(int side, int x, int y)
Definition haio_util.hpp:71
constexpr int bayerSideFor(int levels)
Definition haio_util.hpp:84
Definition haio.hpp:11
Result< std::vector< uint32_t > > limitPalette(const Image< Color::RGBA8888 > &image, std::vector< uint32_t > entries, size_t most, Limit how)
Definition quantise.cpp:70
Result< Image< Color::PALETTE > > toPalette(const Image< Color::RGBA8888 > &image, std::vector< uint32_t > entries, Dither how)
Definition quantise.cpp:146
std::expected< T, Error > Result
std::optional< Dither > ditherNamed(std::string_view name)
Definition quantise.cpp:56
std::optional< Limit > limitNamed(std::string_view name)
Definition quantise.cpp:64
std::vector< uint8_t > data