0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

OpenCV.jsでKmeansを使って減色するときの高速化

Last updated at Posted at 2025-04-16

はじめに

Kmeans法で減色処理って遅いですよね。
何とかならないだろうかと思って考えてみました。
基本的な考え方は、画像を小さくしてKmeansし、量子化して画像を再構築する。であり、Kmeans法自体を速くするわけではありません。
本アルゴリズムは、「多少粗くてもいいから速い」がコンセプトなのでご注意ください。
image.png

実験結果

改良前と後で、Kmeansにかかる時間とKmeansの結果を表示するまでの時間を測定しました。
使用画像は lenna.bmp です。attempts = 2, size = 32, bitDepth = 4 です。
以下、kを変えて時間を測定してみます。

k = 5 Kmeans法にかかる時間 Kmeans後表示するまでの時間
改良前 - kmeans 300ms 54ms
改良後 - fastKmeans 2ms 71ms
k = 10 Kmeans法にかかる時間 Kmeans後表示するまでの時間
改良前 - kmeans 1200ms 52ms
改良後 - fastKmeans 6ms 72ms
k = 15 Kmeans法にかかる時間 Kmeans後表示するまでの時間
改良前 - kmeans 2100ms 52ms
改良後 - fastKmeans 10ms 72ms

kが増えても改良後はあまり遅くならないようです。
Kmeansする画像が小さい(要素の数が少ない)ので改良後の方が当然早くなります。
Kmeans後表示するまでの時間が思ったより遅くならないようです。

改良前のソース(普通のKmeansを使って減色処理)

    /**
     * Color reduction processing using K-means
     * @param {Mat} src source mat 
     * @param {Mat} dst destination mat
     * @param {number} k cluster count
     * @param {number} attempts number of attempts 
     * @param {number} maxCount the maximum number of iterations/elements
     * @param {number} epsilon the desired accuracy
     * @returns {Mat} destination mat
     */
    static kmeans(src, dst, k, attempts, maxCount = 1e4, epsilon = 1e-4) {
        // Initialize
        dst = dst || new cv.Mat(src.size(), src.type());
        const [labels, centers] = [new cv.Mat(), new cv.Mat()];
        const criteria = new cv.TermCriteria(cv.TermCriteria_EPS + cv.TermCriteria_MAX_ITER, maxCount, epsilon);
        const flags = cv.KMEANS_PP_CENTERS;

        // Create samples
        const samples = new cv.Mat(src.rows * src.cols, 3, cv.CV_32F);
        for(let y = 0; y < src.rows; y += 1) {
            for(let x = 0; x < src.cols; x += 1) {
                const idx = y * src.cols + x;
                for(let z = 0; z < 3; z += 1) {                    
                    samples.floatPtr(idx)[z] = src.data[idx * 4 + z];
                }
            }
        }

        // K-Means method
        let startDate = new Date();
        cv.kmeans(samples, k, labels, criteria, attempts, flags, centers);
        console.log('kmeans:', new Date() - startDate, 'ms');
        
        // Convert Float to UInt8
        const floatToUint8 = x => Math.min(Math.max(Math.round(x), 0), 255);
        const ui8Centers = new Uint8Array(k * 3);
        for(let c = 0; c < k; c += 1) {
            for(let z = 0; z < 3; z += 1) {
                ui8Centers[c * 3 + z] = floatToUint8(centers.floatAt(c, z));
            }
        }        
        centers.delete();
        
        // Create dst from labels and centers
        startDate = new Date();
        for(let y = 0; y < dst.rows; y += 1) {
            for(let x = 0; x < dst.cols; x += 1) {
                const p = (y * dst.cols + x) * 4;
                const c = labels.data[p];
                for(let z = 0; z < 3; z += 1) {                    
                    dst.data[p + z] = ui8Centers[c * 3 + z];
                }
                dst.data[p + 3] = src.data[p + 3];
            }
        }
        labels.delete();
        console.log('after kmeans:', new Date() - startDate, 'ms');
        return dst;
    }

改良後のソース

    /**
     * Fast color reduction processing using K-means
     * @param {Mat} src source mat 
     * @param {Mat} dst destination mat
     * @param {Size} dsize size in resizing
     * @param {number} interpolation interpolation
     * @param {number} bitDepth bit depth [1, 8]
     * @param {number} k cluster count
     * @param {number} attempts number of attempts 
     * @param {number} maxCount the maximum number of iterations/elements
     * @param {number} epsilon the desired accuracy
     * @returns {Mat} destination mat
     */
    static fastKmeans(src, dst, dsize, interpolation, bitDepth, k, attempts, maxCount = 1e4, epsilon = 1e-4) {
        // Resize
        const resized = new cv.Mat();
        cv.resize(src, resized, dsize, 0, 0, interpolation);

        // Initialize
        dst = dst || new cv.Mat(src.size(), src.type());
        const [labels, centers] = [new cv.Mat(), new cv.Mat()];
        const criteria = new cv.TermCriteria(cv.TermCriteria_EPS + cv.TermCriteria_MAX_ITER, maxCount, epsilon);
        const flags = cv.KMEANS_PP_CENTERS;

        // Create samples
        const samples = new cv.Mat(resized.rows * resized.cols, 3, cv.CV_32F);
        for(let y = 0; y < resized.rows; y += 1) {
            for(let x = 0; x < resized.cols; x += 1) {
                const idx = y * resized.cols + x;
                for(let z = 0; z < 3; z += 1) {                    
                    samples.floatPtr(idx)[z] = resized.data[idx * 4 + z];
                }
            }
        }

        // K-Means method
        let startDate = new Date();
        cv.kmeans(samples, k, labels, criteria, attempts, flags, centers);
        console.log('kmeans:', new Date() - startDate, 'ms');

        // Convert Float to UInt8
        const floatToUint8 = x => Math.min(Math.max(Math.round(x), 0), 255);
        const ui8Centers = new Uint8Array(k * 3);
        for(let c = 0; c < k; c += 1) {
            for(let z = 0; z < 3; z += 1) {
                ui8Centers[c * 3 + z] = floatToUint8(centers.floatAt(c, z));
            }
        }        

        // Create color map
        const mem = 2 ** bitDepth;
        const quant = (mem - 1) / (256 - 1);
        const digit = Math.ceil(Math.log10(mem - 1));
        const colorMap = {};
        for(let y = 0; y < resized.rows; y += 1) {
            for(let x = 0; x < resized.cols; x += 1) {
                const p = (y * resized.cols + x) * 4;
                const key = createColorKey(resized.data, p);
                if(typeof colorMap[key] === 'undefined') {
                    colorMap[key] = labels.data[p];
                }
            }
        }        
        resized.delete();
        labels.delete();
        centers.delete();

        // Create dst from ui8Centers
        startDate = new Date();
        for(let y = 0; y < dst.rows; y += 1) {
            for(let x = 0; x < dst.cols; x += 1) {
                const p = (y * dst.cols + x) * 4;
                const key = createColorKey(src.data, p);
                // Find nearest color
                if(typeof colorMap[key] === 'undefined') {
                    let [minDist2, minC] = [Number.MAX_VALUE, -1];
                    for(let c = 0; c < k; c += 1) {
                        // Compute distance squared in color space
                        let dist2 = 0;
                        for(let z = 0; z < 3; z += 1) {
                            dist2 += (src.data[p + z] - ui8Centers[c * 3 + z]) ** 2;
                        }
                        if(dist2 < minDist2) {
                            minDist2 = dist2;
                            minC = c;
                        }
                    }
                    colorMap[key] = minC;
                }          
                // Set color
                const c = colorMap[key];
                for(let z = 0; z < 3; z += 1) {
                    dst.data[p + z] = ui8Centers[c * 3 + z];
                }
                dst.data[p + 3] = src.data[p + 3];
            }
        }
        console.log('after kmeans:', new Date() - startDate, 'ms');
        return dst;

        /**
         * Create color key
         * @param {Uint8Array} data data 
         * @param {number} p pixel index 
         * @returns {string} color key
         */
        function createColorKey(data, p) {
            let key = '';
            for(let z = 0; z < 3; z += 1) {
                const c = Math.round(data[p + z] * quant); // Quantization
                key += c.toString().padStart(digit, '0');
            }
            return key;     
        }
    }

サンプルプログラム

fastKmeansとkmeansの呼び出し例を載せておきます。

// sample program
const [dsize, interpolation, bitDepth, k, attempts] = [new cv.Size(32, 32), cv.INTER_LINEAR, 4, 5, 2];
const src = cv.imread('src-canvas');
const dst = OpenCVWrapper.fastKmeans(src, dst, dsize, interpolation, bitDepth, k, attempts);

// use kmeans
//dst = OpenCVWrapper.kmeans(src, dst, k, attempts); 
        
// Show dst
cv.imshow('dst-canvas', dst);

// Remove src and dst
src.delete();
dst.delete();
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?