Perceptual Color

helperimage.cpp
1// SPDX-FileCopyrightText: Lukas Sommer <sommerluk@gmail.com>
2// SPDX-License-Identifier: BSD-2-Clause OR MIT
3
4// Own header
5#include "helperimage.h"
6
7#include "asyncimagerendercallback.h"
8#include "cielchd50values.h"
9#include "helperconstants.h"
10#include "helpermath.h"
11#include "helperqttypes.h"
12#include "interlacingpass.h"
13#include "rgbcolorspace.h"
14#include <lcms2.h>
15#include <qcolor.h>
16#include <qimage.h>
17#include <qmath.h>
18#include <qnamespace.h>
19#include <qpainter.h>
20#include <qrgb.h>
21#include <qsharedpointer.h>
22#include <qsize.h>
23#include <type_traits>
24
25namespace PerceptualColor
26{
27
28/**
29 * @brief Find boundaries between fully opaque and fully transparent pixels.
30 *
31 * @param image The image to be searched.
32 *
33 * @note There is no API guarantee regarding the handling of partially
34 * transparent pixels — they may be treated as fully opaque or
35 * fully transparent.
36 *
37 * @returns A list of all coordinate points on both sides of the boundary.
38 *
39 * @note This function is thread-save as long as there is no more than one
40 * thread of this function operating on the same data on the same time.
41 */
43{
44 QList<QPoint> coordinates;
45 int width = image.width();
46 int height = image.height();
47 for (int y = 0; y < height; ++y) {
48 for (int x = 0; x < width; ++x) {
49 if (image.pixelColor(x, y).alpha() != 0) { // gamut body
50 // We process only the pixels of the gamut body. A gamut body
51 // pixel is added if at least one of its neighbors is a
52 // background pixel, along with all neighboring background
53 // pixels. This eliminates the need for a second pass to test
54 // background pixels.
55 // NOTE: The background color may occasionally appear within
56 // the gamut body, but such instances are rare and therefore
57 // not computationally expensive to handle. In these cases,
58 // anti-aliasing has no effect, making it inconsequential to
59 // the final image.
60 bool hasTransparentNeighbor = false;
61 // Check 8 neighbors
62 for (int dy = -1; dy <= 1; ++dy) {
63 for (int dx = -1; dx <= 1; ++dx) {
64 if (dx == 0 && dy == 0) {
65 continue; // Skip the pixel itself
66 }
67 const auto xOutOfRange = //
68 ((x + dx < 0) || (x + dx >= image.width()));
69 const auto yOutOfRange = //
70 ((y + dy < 0) || (y + dy >= image.height()));
71 if (xOutOfRange || yOutOfRange) {
72 // Out of range
73 continue;
74 }
75 const auto myPixelColor = //
76 image.pixelColor(x + dx, y + dy);
77 if (myPixelColor.alpha() == 0) {
78 hasTransparentNeighbor = true;
79 const QPoint myNeighbor = QPoint(x + dx, y + dy);
80 if (!coordinates.contains(myNeighbor)) {
81 // Add transparent pixel
82 coordinates.append(myNeighbor);
83 }
84 }
85 }
86 }
87 if (hasTransparentNeighbor) {
88 const auto gamutPixel = QPoint(x, y);
89 if (!coordinates.contains(gamutPixel)) {
90 // Add the gamut body pixel itself
91 coordinates.append(gamutPixel);
92 }
93 }
94 }
95 }
96 }
97 return coordinates;
98}
99
100/**
101 * @brief Calculates anti-alias for gamut diagrams.
102 *
103 * Gamut images generated by this library typically exhibit sharp boundaries,
104 * where a pixel is either within the gamut (opaque color) or outside it
105 * (transparent color). The determination is based on the coordinates at the
106 * center of the pixel's square surface.
107 *
108 * This function is designed to perform anti-aliasing by smoothing the sharp
109 * gamut boundaries. To use this function, first obtain a list of candidate
110 * pixels for anti-aliasing. These are the pixels surrounding the sharp gamut
111 * border, which can be identified using @ref findBoundary(). This function
112 * then calculates, within the 1 px × 1 px area of each candidate pixel,
113 * multiple data points at a significantly higher resolution than the single
114 * data point in the original image. By analyzing this detailed data, the
115 * function applies anti-aliasing to smooth the boundary.
116 *
117 * @note Since this operation is computationally intensive, it is recommended
118 * to apply it only to the pixels returned by @ref findBoundary(), rather than
119 * the entire image.
120 *
121 * @param image The image that should be modified.
122 * @param antiAliasCoordinates A list of pixels for which anti-aliasing should
123 * be done.
124 * @param colorFunction A pointer to a function that returns the opaque color
125 * for the given coordinates, or a transparent color if out-of-gamut.
126 */
127void doAntialias(QImage &image, const QList<QPoint> &antiAliasCoordinates, const std::function<QRgb(const double x, const double y)> &colorFunction)
128{
129 QList<QColor> opaqueColors;
130 for (const auto myValue : antiAliasCoordinates) {
131 // Iterating over a square grid of data points within the given pixel.
132 // The side length of the square contains exactly “sideLength” data
133 // points. Its square represents the total number of data points,
134 // referred to here as “totalDataPoints”. The“ sideLength” is chosen so
135 // that the total number of data points is 256, corresponding to the
136 // number of possible alpha values in typical 4-byte colors
137 // (RGB+Alpha), which is sufficient for this case."
138 constexpr int sideLength = 16;
139 constexpr int totalDataPoints = sideLength * sideLength;
140 opaqueColors.clear();
141 opaqueColors.reserve(totalDataPoints);
142 constexpr double stepWidth = 1.0 / sideLength;
143 double x = myValue.x() - 0.5 + stepWidth / 2;
144 double y = myValue.y() - 0.5 + stepWidth / 2;
145 for (int i = 0; i < sideLength; ++i) {
146 for (int j = 0; j < sideLength; ++j) {
147 const QRgb tempColor = colorFunction(x + i * stepWidth, //
148 y + j * stepWidth);
149 if (qAlpha(tempColor) != 0) {
150 opaqueColors.append(tempColor);
151 }
152 }
153 }
154 if (opaqueColors.count() > 0) {
155 const QColorFloatType countF = //
156 static_cast<QColorFloatType>(opaqueColors.count());
157 QColor newPixel = image.pixelColor(myValue);
158 if (newPixel.alpha() == 0) {
159 // The center of the pixel is out-of-gamut. For anti-aliasing,
160 // we need a color, so we calculate the mean color of all other
161 // data points within the pixel that actually are in-gamut.
162 QColorFloatType r = 0;
163 QColorFloatType g = 0;
164 QColorFloatType b = 0;
165 for (const QColor &myColor : opaqueColors) {
166 r += myColor.redF();
167 g += myColor.greenF();
168 b += myColor.blueF();
169 }
170 r /= countF;
171 g /= countF;
172 b /= countF;
173 newPixel = QColor::fromRgbF(r, g, b);
174 }
175 newPixel.setAlphaF(countF / totalDataPoints);
176 image.setPixelColor(myValue, newPixel);
177 }
178 }
179}
180
181} // namespace PerceptualColor
The namespace of this library.
void doAntialias(QImage &image, const QList< QPoint > &antiAliasCoordinates, const std::function< QRgb(const double x, const double y)> &colorFunction)
Calculates anti-alias for gamut diagrams.
QList< QPoint > findBoundary(const QImage &image)
Find boundaries between fully opaque and fully transparent pixels.
int alpha() const const
QColor fromRgbF(float r, float g, float b, float a)
void setAlphaF(float alpha)
int height() const const
QColor pixelColor(const QPoint &position) const const
void setPixelColor(const QPoint &position, const QColor &color)
int width() const const
void append(QList< T > &&value)
void clear()
bool contains(const AT &value) const const
qsizetype count() const const
void reserve(qsizetype size)
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Apr 25 2025 12:03:13 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.