Extract picture color - front end scheme

preface

Last year, I came up with an idea to extract the theme color of the picture, cooperate with the picture to have an immersive visual effect, and produce a harmonious and consistent feeling. It dawdled until this year to start development~

It took a week to realize this function in two ways: client and server. The two methods have their own advantages and disadvantages. Usually, the extraction of theme color is completed in the server. The client provides the pictures to be processed to the server in the form of link or id. the server extracts the theme color by running the corresponding algorithm, and then returns the corresponding results. This can meet most display scenes, but for the pictures that need to be "customized" and "generated" according to the user, there is a way to upload pictures - > server calculation - > return result time, and the waiting time may be relatively long. In the client implementation, the compatibility of mobile browser is a painful thing.

The above is cumbersome. To put it simply, in most cases, it is recommended to choose the server for implementation. When users customize and generate pictures in real time, they can choose the client for implementation without considering the mobile terminal.

This article shares with you the implementation scheme of the client:

At present, the commonly used theme color extraction algorithms for extracting picture colors include: minimum difference method, median segmentation method, octree algorithm, clustering, color modeling method, etc. Here I choose median segmentation method for implementation.

thinking

Median segmentation is usually an algorithm to reduce the image bit depth in image processing. It can be used to convert the high-order graph into the low-order graph, such as converting the 24 bit graph into the 8-bit graph. Since the color distribution of each pixel in the image is the color of the three-dimensional axis (0 ~ 255) as shown in the figure below, the color distribution of each pixel in the image can also be regarded as the color of the three-dimensional axis (0 ~ 255)

Start with the whole image as a box initially, and then divide the longest side of RGB into two from the median of color statistics, so that the number of pixels contained in the two boxes is the same, as shown in the following figure:

Repeat the above steps until the number of boxes obtained by final segmentation is equal to the number of theme colors, and finally take the midpoint of each box.

In practice, if you only cut according to the midpoint, some cuboids will have a large volume but a small number of pixels. The solution is to prioritize the box before cutting, and the sorting coefficient is volume * pixels. In this way, such problems can be basically solved.

effect

code

  1. First create a canvas container
  2. Draw pictures into containers
  3. Use the getImageData method to obtain rgba and view getImageData
  4. The color is cut and extracted by median segmentation algorithm
  5. Filter out similar colors

color.vue (the following code is VUE3.0 syntax)

<template>
    <div>
      <canvas style="display: none" id="canvas"></canvas>
      <div
         id="extract-color-id"
         class="extract-color"
         style="display: flex;padding: 0 20px; justify-content:end;">
      </div>
    </div>
</template>
<script lang="ts">
import themeColor from '../../components/colorExtraction';
export default defineComponent({
 setup(props) {
    /**
     * Set color method
     */
    const SetColor = (colorArr: number[][]) => {
      // Initialize and delete multiple child nodes
      const extractColor = document.querySelector('#extract-color-id') as HTMLElement;
      while (extractColor.firstChild) {
        extractColor.removeChild(extractColor.firstChild);
      }
      // Create child node
      for (let index = 0; index < colorArr.length; index++) {
        const bgc = '(' + colorArr[index][0] + ',' + colorArr[index][1] + ',' + colorArr[index][2] + ')';
        const colorBlock = document.createElement('div') as HTMLElement;
        colorBlock.id = `color-block-id${index}`;
        colorBlock.style.cssText = 'height: 50px;width: 50px;margin-right: 10px;border-radius: 50%;';
        colorBlock.style.backgroundColor = `rgb${bgc}`;
        extractColor.appendChild(colorBlock);
      }
    };
    
    onMounted(()=> {
        const img = new Image();
        img.src = `Address of the picture`;
        img.crossOrigin = 'anonymous';
        img.onload = () => {
          themeColor(50, img, 20, SetColor);
        };
    })

colorExtraction.ts (the following code is TypeScript syntax, which can be converted to JavaScript and all type definitions can be deleted)

/**
 * Color box class
 *
 * @param {Array} colorRange    [[rMin, rMax],[gMin, gMax], [bMin, bMax]] Color range
 * @param {any} total   Total pixels, imageData / 4
 * @param {any} data    Pixel data set
 */
class ColorBox {
    colorRange: unknown[];
    total: number;
    data: Uint8ClampedArray;
    volume: number;
    rank: number;
    constructor(colorRange: any[], total: number, data: Uint8ClampedArray) {
        this.colorRange = colorRange;
        this.total = total;
        this.data = data;
        this.volume = (colorRange[0][1] - colorRange[0][0]) * (colorRange[1][1] - colorRange[1][0]) * (colorRange[2][1] - colorRange[2][0]);
        this.rank = total * this.volume;
    }
    getColor() {
        const total = this.total;
        const data = this.data;
        let redCount = 0,
            greenCount = 0,
            blueCount = 0;

        for (let i = 0; i < total; i++) {
            redCount += data[i * 4];
            greenCount += data[i * 4 + 1];
            blueCount += data[i * 4 + 2];
        }
        return [redCount / total, greenCount / total, blueCount / total];
    }
}

// Get cut edge
const getCutSide = (colorRange: number[][]) => {   // r:0,g:1,b:2
    const arr = [];
    for (let i = 0; i < 3; i++) {
        arr.push(colorRange[i][1] - colorRange[i][0]);
    }
    return arr.indexOf(Math.max(arr[0], arr[1], arr[2]));
}

// Cutting color range
const cutRange = (colorRange: number[][], colorSide: number, cutValue: any) => {
    const arr1: number[][] = [];
    const arr2: number[][] = [];
    colorRange.forEach(function (item) {
        arr1.push(item.slice());
        arr2.push(item.slice());
    })
    arr1[colorSide][1] = cutValue;
    arr2[colorSide][0] = cutValue;

    return [arr1, arr2];
}

// Find a color with a median number of occurrences
const __quickSort = (arr: any[]): any => {
    if (arr.length <= 1) {
        return arr;
    }
    const pivotIndex = Math.floor(arr.length / 2);
    const pivot = arr.splice(pivotIndex, 1)[0];
    const left = [];
    const right = [];
    for (let i = 0; i < arr.length; i++) {
        if (arr[i].count <= pivot.count) {
            left.push(arr[i]);
        }
        else {
            right.push(arr[i]);
        }
    }
    return __quickSort(left).concat([pivot], __quickSort(right));
}

const getMedianColor = (colorCountMap: Record<string, number>, total: number) => {

    const arr = [];
    for (const key in colorCountMap) {
        arr.push({
            color: parseInt(key),
            count: colorCountMap[key]
        })
    }

    const sortArr = __quickSort(arr);
    let medianCount = 0;
    const medianIndex = Math.floor(sortArr.length / 2)

    for (let i = 0; i <= medianIndex; i++) {
        medianCount += sortArr[i].count;
    }

    return {
        color: parseInt(sortArr[medianIndex].color),
        count: medianCount
    }
}

// Cut color box
const cutBox = (colorBox: { colorRange: number[][]; total: number; data: Uint8ClampedArray }) => {

    const colorRange = colorBox.colorRange;
    const cutSide = getCutSide(colorRange);
    const colorCountMap: Record<string, number> = {};
    const total = colorBox.total;
    const data = colorBox.data;

    // Count the number of each value
    for (let i = 0; i < total; i++) {
        const color = data[i * 4 + cutSide];

        if (colorCountMap[color]) {
            colorCountMap[color] += 1;
        }
        else {
            colorCountMap[color] = 1;
        }
    }

    const medianColor = getMedianColor(colorCountMap, total);
    const cutValue = medianColor.color;
    const cutCount = medianColor.count;
    const newRange = cutRange(colorRange, cutSide, cutValue);
    const box1 = new ColorBox(newRange[0], cutCount, data.slice(0, cutCount * 4));
    const box2 = new ColorBox(newRange[1], total - cutCount, data.slice(cutCount * 4));
    return [box1, box2];
}

// Queue cutting
const queueCut = (queue: any[], num: number) => {
    while (queue.length < num) {
        queue.sort((a: { rank: number }, b: { rank: number }) => {
            return a.rank - b.rank
        });
        const colorBox = queue.pop();
        const result = cutBox(colorBox);
        queue = queue.concat(result);
    }
    return queue.slice(0, num)
}

// Color de duplication
const colorFilter = (colorArr: number[][], difference: number) => {
    for (let i = 0; i < colorArr.length; i++) {
        for (let j = i + 1; j < colorArr.length; j++) {
            if (Math.abs(colorArr[i][0] - colorArr[j][0]) < difference && Math.abs(colorArr[i][1] - colorArr[j][1]) < difference && Math.abs(colorArr[i][2] - colorArr[j][2]) < difference) {
                colorArr.splice(j, 1)
                j--
            }
        }
    }
    return colorArr
}

/**
 * Extract color
 * @param colorNumber Extract maximum number of colors
 * @param img Pictures to be extracted
 * @param difference Image color filtering accuracy
 * @param callback Callback function
 */
const themeColor = (colorNumber: number, img: CanvasImageSource, difference: number, callback: (arg0: number[][]) => void) => {
    const canvas = document.createElement('canvas') as HTMLCanvasElement;
    const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
    let width = 0
    let height = 0
    let imageData = null

    canvas.width = img.width as number;
    width = canvas.width as number
    canvas.height = img.height as number
    height = canvas.height

    ctx.drawImage(img, 0, 0, width, height);

    imageData = ctx.getImageData(0, 0, width, height).data;

    const total = imageData.length / 4;

    let rMin = 255,
        rMax = 0,
        gMin = 255,
        gMax = 0,
        bMin = 255,
        bMax = 0;

    // Get range
    for (let i = 0; i < total; i++) {
        const red = imageData[i * 4];
        const green = imageData[i * 4 + 1];
        const blue = imageData[i * 4 + 2];

        if (red < rMin) {
            rMin = red;
        }

        if (red > rMax) {
            rMax = red;
        }

        if (green < gMin) {
            gMin = green;
        }

        if (green > gMax) {
            gMax = green;
        }

        if (blue < bMin) {
            bMin = blue;
        }

        if (blue > bMax) {
            bMax = blue;
        }
    }

    const colorRange = [[rMin, rMax], [gMin, gMax], [bMin, bMax]];
    const colorBox = new ColorBox(colorRange, total, imageData);
    const colorBoxArr = queueCut([colorBox], colorNumber);
    let colorArr = [];

    for (let j = 0; j < colorBoxArr.length; j++) {
        colorBoxArr[j].total && colorArr.push(colorBoxArr[j].getColor())
    }

    colorArr = colorFilter(colorArr, difference)

    callback(colorArr);
}

export default themeColor

reference resources:
https://github.com/lokesh/col...
http://blog.rainy.im/2015/11/...
https://www.yuque.com/along-n...
https://cloud.tencent.com/dev...
https://xcoder.in/2014/09/17/...

Keywords: Javascript Front-end TypeScript Vue.js

Added by josephicon on Mon, 21 Feb 2022 14:52:35 +0200