图片主题色在图片所占比例较大的页面中,能够配合图片起到很好视觉效果,给人一种和谐、一致的感觉。同时也可用在图像分类,搜索识别等方面。通常主题色的提取都是在后端完成的,前端将需要处理的图片以链接或id的形式提供给后端,后端通过运行相应的算法来提取出主题色后,再返回相应的结果。
一、主题色算法
目前比较常用的主题色提取算法有:最小差值法、中位切分法、八叉树算法、聚类、色彩建模法等。其中聚类和色彩建模法需要对提取函数和样本、特征变量等进行调参和回归计算,用到 python
的数值计算库 numpy
和机器学习库 scikit-learn
,用 python
来实现相对比较简单,而目前这两种都没有成熟的js库,并且js本身也不擅长回归计算这种比较复杂的计算。我也就没有深入的研究,而主要将目光放在了前面的几个颜色量化算法上。
而最小差值法是在给定给定调色板的情况下找到与色差最小的颜色,使用的场景比较小,所以我主要看了中位切分法和八叉树算法,并进行了实践。
中位切分法
中位切分法通常是在图像处理中降低图像位元深度的算法,可用来将高位的图转换位低位的图,如将24bit的图转换为8bit的图。我们也可以用来提取图片的主题色,其原理是是将图像每个像素颜色看作是以R、G、B为坐标轴的一个三维空间中的点,由于三个颜色的取值范围为0~255,所以图像中的颜色都分布在这个颜色立方体内,如下图所示。
之后将RGB中最长的一边从颜色统计的中位数一切为二,使得到的两个长方体所包含的像素数量相同,如下图所示
重复这个过程直到切出长方体数量等于主题色数量为止,最后取每个长方体的中点即可。
在实际使用中如果只是按照中点进行切割,会出现有些长方体的体积很大但是像素数量很少的情况。解决的办法是在切割前对长方体进行优先级排序,排序的系数为体积 * 像素数。这样就可以基本解决此类问题了。
八叉树算法
八叉树算法也是在颜色量化中比较常见的,主要思路是将R、G、B通道的数值做二进制转换后逐行放下,可得到八列数字。如 #FF7880
转换后为
R: 1111 1111
G: 0111 1000
B: 0000 0000
再将RGB通道逐列粘合,可以得到8个数字,即为该颜色在八叉树中的位置,如图。
在将所有颜色插入之后,再进行合并运算,直到得到所需要的颜色数量为止。
在实际操作中,由于需要对图像像素进行遍历后插入八叉树中,并且插入过程有较多的递归操作,所以比中位切分法要消耗更长的时间。
算法实现C++:
#include <iostream> // std::cout
#include <algorithm> // std::sort
#include <vector> // std::vector
#include <type_traits>
#include <list>
#include <deque>
#include <queue>
#include <tuple>
#include <map>
#include <utility>
#include <fstream>
#include <chrono>
#include <thread>
#include "opencv2/opencv.hpp"
using RGB_TYPE = std::tuple<int, int, int>;
using CBGR = std::tuple<int, int, int, int>;
//key--b,g,r value--count
using PIL = std::map<RGB_TYPE, int>;
//std::array<int, 4> = count b, g, r
using PIL_TYPE = std::vector<std::array<int, 4>>;
using PIL_COLOR_RANGE_TYPE = std::vector<std::array<int, 2>>;
class ColorBox
{
public:
ColorBox(PIL_COLOR_RANGE_TYPE cRange, const int &pxlSum, const PIL_TYPE &pxlSet, const int &vBalance);
friend bool operator <(const ColorBox &a, const ColorBox &b)
{
return a.rank < b.rank;
}
long long rank;
PIL_TYPE pixelSet;
PIL_COLOR_RANGE_TYPE colorRange;
int pixelSum;
int v;
};
using PIL_PRIORRY_TYPE = std::vector<std::pair<long long, ColorBox>>;
struct NODE_TYPE{
long long rank;
ColorBox p;
friend bool operator < (const NODE_TYPE &a, const NODE_TYPE &b)
{
return a.rank < b.rank; //结构体中,x小的优先级高
}
};
ColorBox::ColorBox(PIL_COLOR_RANGE_TYPE cRange,const int &pxlSum,const PIL_TYPE &pxlSet, const int &vBalance)
{
this->colorRange = cRange;
this->pixelSum = pxlSum;
this->pixelSet = std::move(pxlSet);
this->v = ((cRange.at(0).at(1) - cRange.at(0).at(0))
* ((cRange.at(1).at(1) - cRange.at(1).at(0)) *((cRange.at(2).at(1) - cRange.at(2).at(0))))) >> 16;
this->rank = (-1) * pxlSum * std::pow(this->v, vBalance);
}
PIL_TYPE getcolors(const cv::Mat &srcimg)
{
int width = srcimg.cols;
int height = srcimg.rows;
if (srcimg.isContinuous())
{
width *= height;
height = 1;
}
PIL imageColors;
for (int y = 0; y < height; ++y)
{
const uchar *p = srcimg.ptr<uchar>(y);
for (int x = 0; x < width; ++x)
{
RGB_TYPE temp = std::make_tuple(p[2], p[1], p[0]);
++imageColors[temp];
p += 3;
}
}
PIL_TYPE imgColors;
std::array<int, 4> a;
for (const auto &p : imageColors)
{
a.at(0) = p.second;
a.at(1) = std::get<0>(p.first);
a.at(2) = std::get<1>(p.first);
a.at(3) = std::get<2>(p.first);
imgColors.emplace_back(a);
}
//std::sort(imgColors.begin(), imgColors.end(),
// [](const std::array<int, 4> &x, const std::array<int, 4> &y) {return x.at(1) < y.at(1);});
return imgColors;
}
int getCutSide(PIL_COLOR_RANGE_TYPE colorRange)
{
std::array<int, 3> vSize = { 0, 0, 0 };
for (int i = 0; i < 3; ++i)
{
vSize.at(i) = colorRange.at(i).at(1) - colorRange.at(i).at(0);
}
return ((vSize.at(0) > vSize.at(1)) ? ((vSize.at(0) > vSize.at(2)) ? 0 : 2): ((vSize.at(1) > vSize.at(2)) ? 1 : 2));
}
std::vector<PIL_COLOR_RANGE_TYPE> cutRange(PIL_COLOR_RANGE_TYPE &cRange, int cutSize, int cutValue)
{
PIL_COLOR_RANGE_TYPE ret0 = cRange;
PIL_COLOR_RANGE_TYPE ret1 = cRange;
ret0.at(cutSize).at(1) = cutValue;
ret1.at(cutSize).at(0) = cutValue;
std::vector<PIL_COLOR_RANGE_TYPE> ret = { ret0, ret1 };
return ret;
}
std::vector<ColorBox> colorCut(ColorBox &colorBox, int vBalance)
{
int cutValue = 0;
std::vector<ColorBox> resBox;
PIL_COLOR_RANGE_TYPE colorRange = colorBox.colorRange;
int cutSide = getCutSide(colorRange);
long long pixelCount = 0;//当前像素累加数
PIL_TYPE sourceList = colorBox.pixelSet;
int pixelSum = colorBox.pixelSum;
int cutPoint = 0; // 切分点
switch (cutSide)
{
case 0:
std::sort(sourceList.begin(), sourceList.end(),
[](const std::array<int, 4> &x, const std::array<int, 4> &y) {return x.at(1) < y.at(1);});
break;
case 1:
std::sort(sourceList.begin(), sourceList.end(),
[](const std::array<int, 4> &x, const std::array<int, 4> &y) {return x.at(2) < y.at(2); });
break;
case 2:
std::sort(sourceList.begin(), sourceList.end(),
[](const std::array<int, 4> &x, const std::array<int, 4> &y) {return x.at(3) < y.at(3); });
break;
}
for (const auto & pixelPoint : sourceList)
{
pixelCount += pixelPoint.at(0);
cutPoint += 1;
if ( (pixelCount * std::pow((pixelPoint.at(cutSide + 1) - colorRange.at(cutSide).at(0)), vBalance))
> ((pixelSum - pixelCount) * std::pow((colorRange.at(cutSide).at(1) - pixelPoint.at(cutSide + 1)), vBalance)))
{
cutValue = pixelPoint.at(cutSide + 1);
break;
}
}
if (cutPoint == sourceList.size())
{
std::vector<PIL_COLOR_RANGE_TYPE> newRange = cutRange(colorRange, cutSide, sourceList.at(cutPoint - 2).at(cutSide + 1));
PIL_TYPE px;
px.assign(sourceList.begin(), sourceList.begin() + cutPoint - 1);
ColorBox box0 = ColorBox(newRange.at(0), pixelCount - sourceList.at(cutPoint -1).at(0), px, vBalance);
resBox.emplace_back(box0);
}
else
{
std::vector<PIL_COLOR_RANGE_TYPE> newRange = cutRange(colorRange, cutSide, cutValue);
PIL_TYPE px0, px1;
px0.assign(sourceList.begin(), sourceList.begin() + cutPoint);
px1.assign(sourceList.begin() + cutPoint, sourceList.end());
ColorBox box0 = ColorBox(newRange.at(0), pixelCount, px0, vBalance);
ColorBox box1 = ColorBox(newRange.at(1), colorBox.pixelSum - pixelCount, px1, vBalance);
resBox.emplace_back(box0);
resBox.emplace_back(box1);
}
return resBox;
}
std::priority_queue<NODE_TYPE> doCut(std::priority_queue<NODE_TYPE> queue, int colorNum)
{
if (queue.size() < colorNum)
{
ColorBox box = queue.top().p;
queue.pop();
std::vector<ColorBox> c = colorCut(box, colorNum);
for (const auto &vbox : c)
{
NODE_TYPE Node = { vbox.rank, vbox };
queue.push(Node);
}
return doCut(queue, colorNum);
}
else
{
return queue;
}
}
std::array<int, 4> sumColor(const PIL_TYPE &pxlSet)
{
std::array<int, 4> sumList = { 0 };
for (const auto & pixel : pxlSet)
{
sumList.at(0) += pixel.at(0);
sumList.at(1) += pixel.at(1) * pixel.at(0);
sumList.at(2) += pixel.at(2) * pixel.at(0);
sumList.at(3) += pixel.at(3) * pixel.at(0);
}
if (sumList.at(0) != 0)
{
sumList.at(1) /= sumList.at(0);
sumList.at(2) /= sumList.at(0);
sumList.at(3) /= sumList.at(0);
}
return sumList;
}
PIL_TYPE getMainColor(std::priority_queue<NODE_TYPE> queue, int colorNum)
{
PIL_TYPE colorList;
for (int i = 0; i < colorNum; ++i)
{
ColorBox box = queue.top().p;
queue.pop();
std::array<int, 4> t = sumColor(box.pixelSet);
colorList.emplace_back(t);
}
return colorList;
}
void colorExt(std::string filename, int colorNum, int vBalance, int padding)
{
cv::Mat image = cv::imread(filename);
cv::Size size = image.size();
int sizeX = size.width;
int sizeY = size.height;
int totalSize = size.area();
std::vector<cv::Mat> imgs;
cv::split(image, imgs);
int bMax, bMin;
int gMax, gMin;
int rMax, rMin;
double minValue, maxValue;
cv::minMaxIdx(imgs[0], &minValue, &maxValue);
bMax = static_cast<int>(maxValue);
bMin = static_cast<int>(minValue);
cv::minMaxIdx(imgs[1], &minValue, &maxValue);
gMax = static_cast<int>(maxValue);
gMin = static_cast<int>(minValue);
cv::minMaxIdx(imgs[2], &minValue, &maxValue);
rMax = static_cast<int>(maxValue);
rMin = static_cast<int>(minValue);
/*std::array<uchar, 6> initRange = { rMin, rMax, gMin, gMax,bMin, bMax};*/
std::cout << "rMin:" << rMin << " rMax:" << rMax << " gMin:" << gMin
<< " gMax:" << gMax << " bMin:" << bMin << " bMax:" << bMax << std::endl;
PIL_COLOR_RANGE_TYPE initRange = { {rMin, rMax},{gMin, gMax},{bMin, bMax}};
PIL_TYPE imageColors = getcolors(image);
ColorBox initBox = ColorBox(initRange, totalSize, imageColors, vBalance);
NODE_TYPE Node = {initBox .rank, initBox };
std::priority_queue<NODE_TYPE> initQueue;
initQueue.push(Node);
std::priority_queue<NODE_TYPE> resQueue = doCut(initQueue, colorNum);
PIL_TYPE mainColor = getMainColor(resQueue, colorNum);
std::sort(mainColor.begin(), mainColor.end(),
[](const std::array<int, 4> &x, const std::array<int, 4> &y) {return x.at(0) > y.at(0); });
for (const auto &c : mainColor)
{
std::cout << c.at(0) << " [R, G, B]-->" << c.at(1) << "," << c.at(2) << "," << c.at(3) << std::endl;
}
cv::Mat img = cv::Mat(cv::Size(sizeX * 3 / 2, sizeY), image.type(), cv::Scalar::all(255));
image.copyTo(img(cv::Rect(0, 0, sizeX, sizeY)));
for (int i = 0; i < colorNum; ++i)
{
cv::Mat pic = cv::Mat(cv::Size(sizeX / 2, sizeY / colorNum), image.type(),
cv::Scalar(mainColor.at(i).at(3),mainColor.at(i).at(2), mainColor.at(i).at(1)));
pic.copyTo(img(cv::Rect(sizeX, i * sizeY / colorNum, sizeX / 2, sizeY / colorNum)));
}
cv::imshow("dst", img);
cv::waitKey(0);
}
void colorExtDemo()
{
for (int i = 1; i < 24; ++i)
{
std::string filename = "G:\\" + std::to_string(i) + ".jpg";
int colorNum = 6;
int vBalance = 5;
int padding = 5;
colorExt(filename, colorNum, vBalance, padding);
}
cv::waitKey(0);
}
int main(int argc, char **argv)
{
colorExtDemo();
return 0;
}
效果展示: