diff --git a/assets/webconfig/i18n/en.json b/assets/webconfig/i18n/en.json index 774e9cf9..14aea881 100644 --- a/assets/webconfig/i18n/en.json +++ b/assets/webconfig/i18n/en.json @@ -330,6 +330,7 @@ "edt_conf_enum_dl_verbose2": "Verbosity level 2", "edt_conf_enum_dl_verbose3": "Verbosity level 3", "edt_conf_enum_dominant_color": "Dominant Color - per LED", + "edt_conf_enum_dominant_color_advanced": "Dominant Color Advanced - per LED", "edt_conf_enum_effect": "Effect", "edt_conf_enum_gbr": "GBR", "edt_conf_enum_grb": "GRB", @@ -969,6 +970,7 @@ "remote_maptype_intro": "Usually the LED layout defines which LED covers a specific picture area. You can change it here: $1.", "remote_maptype_label": "Mapping type", "remote_maptype_label_dominant_color": "Dominant Color", + "remote_maptype_label_dominant_color_advanced": "Dominant Color Advanced", "remote_maptype_label_multicolor_mean": "Mean Color Simple", "remote_maptype_label_multicolor_mean_squared": "Mean Color Squared", "remote_maptype_label_unicolor_mean": "Mean Color Image", diff --git a/include/hyperion/ImageProcessor.h b/include/hyperion/ImageProcessor.h index c96e888c..18eac24c 100644 --- a/include/hyperion/ImageProcessor.h +++ b/include/hyperion/ImageProcessor.h @@ -129,6 +129,9 @@ public: case 3: colors = _imageToLeds->getDominantLedColor(image); break; + case 4: + colors = _imageToLeds->getDominantLedColorAdv(image); + break; default: colors = _imageToLeds->getMeanLedColor(image); } @@ -171,6 +174,9 @@ public: case 3: _imageToLeds->getDominantLedColor(image, ledColors); break; + case 4: + _imageToLeds->getDominantLedColorAdv(image, ledColors); + break; default: _imageToLeds->getMeanLedColor(image, ledColors); } diff --git a/include/hyperion/ImageToLedsMap.h b/include/hyperion/ImageToLedsMap.h index 9607d5a6..f7977f01 100644 --- a/include/hyperion/ImageToLedsMap.h +++ b/include/hyperion/ImageToLedsMap.h @@ -9,12 +9,17 @@ // hyperion-utils includes #include #include +#include +#include // hyperion includes #include namespace hyperion { + /// Number of clusters for k-means calculation + const int CLUSTER_COUNT {5}; + /// /// The ImageToLedsMap holds a mapping of indices into an image to LEDs. It can be used to /// calculate the average (aka mean) or dominant color per LED for a given region. @@ -220,6 +225,48 @@ namespace hyperion } } + /// + /// Determines the dominant color using a k-means algorithm for each LED using the LED area mapping given + /// at construction. + /// + /// @param[in] image The image from which to extract the LED color + /// + /// @return The vector containing the output + /// + template + std::vector getDominantLedColorAdv(const Image & image) const + { + std::vector colors(_colorsMap.size(), ColorRgb{0,0,0}); + getDominantLedColorAdv(image, colors); + return colors; + } + + /// + /// Determines the dominant color using a k-means algorithm for each LED using the LED area mapping given + /// at construction. + /// + /// @param[in] image The image from which to extract the LED colors + /// @param[out] ledColors The vector containing the output + /// + template + void getDominantLedColorAdv(const Image & image, std::vector & ledColors) const + { + // Sanity check for the number of LEDs + if(_colorsMap.size() != ledColors.size()) + { + Debug(Logger::getInstance("HYPERION"), "ImageToLedsMap: colorsMap.size != ledColors.size -> %d != %d", _colorsMap.size(), ledColors.size()); + return; + } + + // Iterate each led and compute the dominant color + auto led = ledColors.begin(); + for (auto colors = _colorsMap.begin(); colors != _colorsMap.end(); ++colors, ++led) + { + const ColorRgb color = calculateDominantColorAdv(image, *colors); + *led = color; + } + } + private: /// The width of the indexed image const int _width; @@ -446,6 +493,140 @@ namespace hyperion return calculateDominantColor(image, pixels); } + + template + struct ColorCluster { + + ColorCluster():count(0) {} + + Pixel_T color; + Pixel_T newColor; + int count; + }; + + /// + /// Calculates the 'dominant color' of an image area defined by a list of pixel indices + /// using a k-means algorithm (https://robocraft.ru/computervision/1063) + /// + /// @param[in] image The image for which a dominant color is to be computed + /// @param[in] pixels The list of pixel indices for the given image to be evaluated + /// + /// @return The image area's dominant color or black, if no pixel indices provided + /// + template + ColorRgb calculateDominantColorAdv(const Image & image, const std::vector & pixels) const + { + ColorRgb dominantColor {ColorRgb::BLACK}; + + const auto pixelNum = pixels.size(); + if (pixelNum > 0) + { + ColorCluster clusters[CLUSTER_COUNT]; + + // initial cluster colors + for(int k = 0; k < CLUSTER_COUNT; ++k) + { + int randomRed = rand() % static_cast(256); + int randomGreen = rand() % static_cast(256); + int randomBlue = rand() % static_cast(256); + + clusters[k].newColor = ColorRgbScalar(randomRed, randomGreen, randomBlue); + } + + // k-means + double min_rgb_euclidean {0}; + double old_rgb_euclidean {0}; + + while(1) + { + for(int k = 0; k < CLUSTER_COUNT; ++k) + { + clusters[k].count = 0; + clusters[k].color = clusters[k].newColor; + clusters[k].newColor.setRgb(ColorRgb::BLACK); + } + + const auto& imgData = image.memptr(); + for (const int pixelOffset : pixels) + { + const auto& pixel = imgData[pixelOffset]; + + min_rgb_euclidean = 255 * 255 * 255; + int clusterIndex = -1; + for(int k = 0; k < CLUSTER_COUNT; ++k) + { + double euclid = ColorSys::rgb_euclidean(ColorRgbScalar(pixel), clusters[k].color); + + if( euclid < min_rgb_euclidean ) { + min_rgb_euclidean = euclid; + clusterIndex = k; + } + } + + clusters[clusterIndex].count++; + clusters[clusterIndex].newColor += ColorRgbScalar(pixel); + } + + min_rgb_euclidean = 0; + for(int k = 0; k < CLUSTER_COUNT; ++k) + { + if (clusters[k].count > 0) + { + // new color + clusters[k].newColor /= clusters[k].count; + double ecli = ColorSys::rgb_euclidean(clusters[k].newColor, clusters[k].color); + if(ecli > min_rgb_euclidean) + { + min_rgb_euclidean = ecli; + } + } + } + + if( fabs(min_rgb_euclidean - old_rgb_euclidean) < 1) + { + break; + } + + old_rgb_euclidean = min_rgb_euclidean; + } + + int colorsFoundMax = 0; + int dominantClusterIdx {0}; + + for(int clusterIdx=0; clusterIdx < CLUSTER_COUNT; ++clusterIdx){ + int colorsFoundinCluster = clusters[clusterIdx].count; + if (colorsFoundinCluster > colorsFoundMax) { + colorsFoundMax = colorsFoundinCluster; + dominantClusterIdx = clusterIdx; + } + } + + dominantColor.red = static_cast(clusters[dominantClusterIdx].newColor.red); + dominantColor.green = static_cast(clusters[dominantClusterIdx].newColor.green); + dominantColor.blue = static_cast(clusters[dominantClusterIdx].newColor.blue); + } + + return dominantColor; + } + + /// + /// Calculates the 'dominant color' of an image area defined by a list of pixel indices + /// using a k-means algorithm (https://robocraft.ru/computervision/1063) + /// + /// @param[in] image The image for which a dominant color is to be computed + /// + /// @return The image's dominant color + /// + template + ColorRgb calculateDominantColorAdv(const Image & image) const + { + const unsigned pixelNum = image.width() * image.height(); + + std::vector pixels(pixelNum); + std::iota(pixels.begin(), pixels.end(), 0); + + return calculateDominantColorAdv(image, pixels); + } }; } // end namespace hyperion diff --git a/include/utils/ColorRgbScalar.h b/include/utils/ColorRgbScalar.h new file mode 100644 index 00000000..3b605a2f --- /dev/null +++ b/include/utils/ColorRgbScalar.h @@ -0,0 +1,203 @@ +#ifndef COLORRGBSCALAR_H +#define COLORRGBSCALAR_H + +// STL includes +#include +#include + +#include +#include +#include +#include + +/// +/// Plain-Old-Data structure containing the red-green-blue color specification. Size of the +/// structure is exactly 3 times int for easy writing to led-device +/// +struct ColorRgbScalar +{ + /// The red color channel + int red; + /// The green color channel + int green; + /// The blue color channel + int blue; + + /// 'Black' RgbColor (0, 0, 0) + static const ColorRgbScalar BLACK; + /// 'Red' RgbColor (255, 0, 0) + static const ColorRgbScalar RED; + /// 'Green' RgbColor (0, 255, 0) + static const ColorRgbScalar GREEN; + /// 'Blue' RgbColor (0, 0, 255) + static const ColorRgbScalar BLUE; + /// 'Yellow' RgbColor (255, 255, 0) + static const ColorRgbScalar YELLOW; + /// 'White' RgbColor (255, 255, 255) + static const ColorRgbScalar WHITE; + + ColorRgbScalar() = default; + + ColorRgbScalar(int _red, int _green,int _blue): + red(_red), + green(_green), + blue(_blue) + { + + } + + ColorRgbScalar(ColorRgb rgb): + red(rgb.red), + green(rgb.green), + blue(rgb.blue) + { + + } + + ColorRgbScalar operator-(const ColorRgbScalar& b) const + { + ColorRgbScalar a(*this); + a.red -= b.red; + a.green -= b.green; + a.blue -= b.blue; + return a; + } + + void setRgb(QRgb rgb) + { + red = qRed(rgb); + green = qGreen(rgb); + blue = qBlue(rgb); + } + + void setRgb(ColorRgb rgb) + { + red = rgb.red; + green = rgb.green; + blue = rgb.blue; + } + + QString toQString() const + { + return QString("(%1,%2,%3)").arg(red).arg(green).arg(blue); + } +}; +/// Assert to ensure that the size of the structure is 'only' 3 times int +static_assert(sizeof(ColorRgbScalar) == 3 * sizeof(int), "Incorrect size of ColorRgbInt"); + + +/// +/// Stream operator to write ColorRgbInt to an outputstream (format "'{'[red]','[green]','[blue]'}'") +/// +/// @param os The output stream +/// @param color The color to write +/// @return The output stream (with the color written to it) +/// +inline std::ostream& operator<<(std::ostream& os, const ColorRgbScalar& color) +{ + os << "{" + << static_cast(color.red) << "," + << static_cast(color.green) << "," + << static_cast(color.blue) + << "}"; + + return os; +} + +/// +/// Stream operator to write ColorRgbInt to a QTextStream (format "'{'[red]','[green]','[blue]'}'") +/// +/// @param os The output stream +/// @param color The color to write +/// @return The output stream (with the color written to it) +/// +inline QTextStream& operator<<(QTextStream &os, const ColorRgbScalar& color) +{ + os << "{" + << static_cast(color.red) << "," + << static_cast(color.green) << "," + << static_cast(color.blue) + << "}"; + + return os; +} + +/// Compare operator to check if a color is 'equal' to another color +inline bool operator==(const ColorRgbScalar & lhs, const ColorRgbScalar & rhs) +{ + return lhs.red == rhs.red && + lhs.green == rhs.green && + lhs.blue == rhs.blue; +} + +/// Compare operator to check if a color is 'smaller' than another color +inline bool operator<(const ColorRgbScalar & lhs, const ColorRgbScalar & rhs) +{ + return lhs.red < rhs.red && + lhs.green < rhs.green && + lhs.blue < rhs.blue; +} + +/// Compare operator to check if a color is 'not equal' to another color +inline bool operator!=(const ColorRgbScalar & lhs, const ColorRgbScalar & rhs) +{ + return !(lhs == rhs); +} + +/// Compare operator to check if a color is 'smaller' than or 'equal' to another color +inline bool operator<=(const ColorRgbScalar & lhs, const ColorRgbScalar & rhs) +{ + return lhs.red <= rhs.red && + lhs.green <= rhs.green && + lhs.blue <= rhs.blue; +} + +/// Compare operator to check if a color is 'greater' to another color +inline bool operator>(const ColorRgbScalar & lhs, const ColorRgbScalar & rhs) +{ + return lhs.red > rhs.red && + lhs.green > rhs.green && + lhs.blue > rhs.blue; +} + +/// Compare operator to check if a color is 'greater' than or 'equal' to another color +inline bool operator>=(const ColorRgbScalar & lhs, const ColorRgbScalar & rhs) +{ + return lhs.red >= rhs.red && + lhs.green >= rhs.green && + lhs.blue >= rhs.blue; +} + +inline ColorRgbScalar& operator+=(ColorRgbScalar& lhs, const ColorRgbScalar& rhs) +{ + lhs.red += rhs.red; + lhs.green += rhs.green; + lhs.blue += rhs.blue; + + return lhs; +} + +inline ColorRgbScalar operator+(ColorRgbScalar lhs, const ColorRgbScalar rhs) +{ + lhs += rhs; + return lhs; +} + +inline ColorRgbScalar& operator/=(ColorRgbScalar& lhs, int count) +{ + if (count > 0) + { + lhs.red /= count; + lhs.green /= count; + lhs.blue /= count; + } + return lhs; +} + +inline ColorRgbScalar operator/(ColorRgbScalar lhs, int count) +{ + lhs /= count; + return lhs; +} + +#endif // COLORRGBSCALAR_H diff --git a/include/utils/ColorRgba.h b/include/utils/ColorRgba.h index 63afdc5a..648cebb7 100644 --- a/include/utils/ColorRgba.h +++ b/include/utils/ColorRgba.h @@ -30,11 +30,11 @@ struct ColorRgba static const ColorRgba WHITE; }; -/// Assert to ensure that the size of the structure is 'only' 3 bytes +/// Assert to ensure that the size of the structure is 'only' 4 bytes static_assert(sizeof(ColorRgba) == 4, "Incorrect size of ColorARGB"); /// -/// Stream operator to write ColorRgb to an outputstream (format "'{'[alpha]', '[red]','[green]','[blue]'}'") +/// Stream operator to write ColorRgba to an outputstream (format "'{'[alpha]', '[red]','[green]','[blue]'}'") /// /// @param os The output stream /// @param color The color to write diff --git a/include/utils/ColorSys.h b/include/utils/ColorSys.h index 63fb74cb..8a83ef7a 100644 --- a/include/utils/ColorSys.h +++ b/include/utils/ColorSys.h @@ -105,6 +105,19 @@ public: /// @note See https://bottosson.github.io/posts/colorpicker/#okhsv /// static void okhsv2rgb(double hue, double saturation, double value, uint8_t & red, uint8_t & green, uint8_t & blue); + + template + static double rgb_euclidean(Pixel_T p1, Pixel_T p2) + { + double val = sqrt( + (p1.red - p2.red) * (p1.red - p2.red) + + (p1.green - p2.green) * (p1.green - p2.green) + + (p1.blue - p2.blue) * (p1.blue - p2.blue) + ); + + return val; + } + }; #endif // COLORSYS_H diff --git a/libsrc/hyperion/ImageProcessor.cpp b/libsrc/hyperion/ImageProcessor.cpp index ad0a902a..c9321d16 100644 --- a/libsrc/hyperion/ImageProcessor.cpp +++ b/libsrc/hyperion/ImageProcessor.cpp @@ -26,6 +26,10 @@ int ImageProcessor::mappingTypeToInt(const QString& mappingType) { return 3; } + else if (mappingType == "dominant_color_advanced" ) + { + return 4; + } return 0; } // global transform method @@ -42,6 +46,9 @@ QString ImageProcessor::mappingTypeToStr(int mappingType) case 3: typeText = "dominant_color"; break; + case 4: + typeText = "dominant_color_advanced"; + break; default: typeText = "multicolor_mean"; break; diff --git a/libsrc/hyperion/schema/schema-color.json b/libsrc/hyperion/schema/schema-color.json index 739a47ee..ed331d21 100644 --- a/libsrc/hyperion/schema/schema-color.json +++ b/libsrc/hyperion/schema/schema-color.json @@ -9,10 +9,10 @@ "type" : "string", "required" : true, "title" : "edt_conf_color_imageToLedMappingType_title", - "enum" : ["multicolor_mean", "unicolor_mean", "multicolor_mean_squared", "dominant_color"], + "enum" : ["multicolor_mean", "unicolor_mean", "multicolor_mean_squared", "dominant_color", "dominant_color_advanced"], "default" : "multicolor_mean", "options" : { - "enum_titles" : ["edt_conf_enum_multicolor_mean", "edt_conf_enum_unicolor_mean", "edt_conf_enum_multicolor_mean_squared", "edt_conf_enum_dominant_color"] + "enum_titles" : ["edt_conf_enum_multicolor_mean", "edt_conf_enum_unicolor_mean", "edt_conf_enum_multicolor_mean_squared", "edt_conf_enum_dominant_color", "edt_conf_enum_dominant_color_advanced"] }, "propertyOrder" : 1 },