图像主题色提取

   图片主题色在图片所占比例较大的页面中,能够配合图片起到很好视觉效果,给人一种和谐、一致的感觉。同时也可用在图像分类,搜索识别等方面。通常主题色的提取都是在后端完成的,前端将需要处理的图片以链接或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;
}

效果展示:

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

血_影

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值