[opencv4]03-基于图像遍历的颜色压缩

https://docs.opencv.org/4.1.0/db/da5/tutorial_how_to_scan_images.html

完整Demo:

https://github.com/youhengchan/learn_opencv4/tree/master/03-color_compressing

目录-基于图像遍历的颜色压缩

颜色压缩算法(color space reduction)

Look Up Table

     Tick

遍历图像(方法1,最快的方法,指针直接访问)

遍历图像(方法2,使用迭代器,安全)

遍历图像(方法3,不常被用来进行遍历,常被用于随机访问某个像素点)


  • 颜色压缩算法(color space reduction

算法描述:

hue_{new} = compression\,rate * \frac{hue{_{old}}}{compression\, rate}

算法解释:

即新的色彩值 = 压缩率 * (旧的色彩值 / 压缩率)

颜色压缩基于一个很简单的想法,对于一个8个位表示的一种单一的颜色,三通道RGB/BGR能够组合出

2^{^{8*3}} = 2^{^{24}} = 16777216

即接近2千万种不同的颜色

一段测试代码说明压缩的核心思想:(这里压缩率取10,对一个单通道的颜色数组进行处理)

产生10个0-255的随机数,然后将其对应的uchar型, int型,以及经过压缩处理的int型的值表示出来

#include <iostream>
#include <cstdlib>
#include <ctime>

using namespace std;

unsigned char random_char(int min, int max) {
	if (max > 255 || min < 0) {
		return 0;  // Avoid exceeding
	}
	return ((double)rand() / RAND_MAX) * (max - min) + min; 
}

void print_uchar_array(unsigned char* ch_arr, int len) {
	cout << "print array" << endl;
 	for (int i = 0 ; i < len ; i++) { 
		cout << ch_arr[i] << " ";
	} 
	cout << endl << endl;
}

void print_uchar_array_cv_int(unsigned char* ch_arr, int len) {
	cout << "cv print array" << endl;
	for (int i = 0 ; i < len ; i++) {
    	cout << int(ch_arr[i])  << " ";
    }
	cout << endl << endl;
}

void test_compressing(unsigned char* ch_arr, int len) {
	cout << "cv compressing print array" << endl;
	for (int i = 0 ; i < len ; i++) {
    	cout << 10 * (int(ch_arr[i]) / 10)  << " ";
    }
	cout << endl << endl;
}

int main(int argc, char** argv) { 
	unsigned char ch_arr[10];
	srand(time(unsigned(NULL)));
	for (int i = 0 ; i < 10 ; i++) { 
		ch_arr[i] = random_char(0, 255);
	} 		
	print_uchar_array(ch_arr, 10);
	print_uchar_array_cv_int(ch_arr, 10);
	test_compressing(ch_arr, 10);
	return 0;
}

测试结果:(因为使用了随机数,所以测试结果不一定相同)


可以看到,输出的结果就是将一个8位的unsigned char的对应int值压缩为一个10的倍数的整数

对于0-9 压缩为0, 10-19压缩为10,依次类推

但是乘法和除法的代价是很高的,所以有在图形处理的时候,借助hash table的思想,设计一个look up table

预先将256种对应的值先算出来,然后在对一张图片中的各个像素点进行处理的时候,只需要进行快速的

查表就可以了 (look up table在数字电路中是一种很重要的结构)

 

  • Look Up Table

在处理的时候,可以使用C++的stringstream 类进行传入参数的数据类型的快速转换

传入的参数显然是一个常量字符串char* argv[]

因为需要获得压缩率,在设计图片读入的时候,会将压缩率以一个参数的形式传入

对于字符串到数字,当然可以使用c自带的atoi和atof,但是这里使用stringstream是最方便有效的

在进行图像颜色压缩之前,先进行处理

#include <iostream>
#include <cstdlib>
#include <sstream>

using namespace std;

unsigned char lookup_table[256];

void check_table(unsigned char* arr) {
	for (int i = 0 ; i <= 255 ; ++i) {
		cout << (int)(arr[i]) << " ";
		if (i % 16 == 0 && i != 0) { 
			cout << endl;
		}
	}
	cout << endl;
}


void init_lookup_table(char* divide_factor) {
	for (int i = 0 ; i < 256 ; ++i) {
		lookup_table[i] = i;
	}
	cout << "Before divided by compression_rate : " << endl; 
	check_table(lookup_table);
	stringstream ss;
	int compression_rate = 0;
	ss << divide_factor;
	ss >> compression_rate;
	cout << endl << "Caught compression_rate = " << compression_rate << endl;
	if (!divide_factor || !compression_rate) {
		cerr << "Error: No compreesion rate given" << endl;
		exit(1);
	}
	for (int i = 0 ; i < 256 ; ++i) {
		lookup_table[i] = (unsigned char)((compression_rate) * (lookup_table[i] / compression_rate));
	}
	cout << "After divided by compression_rate :" << endl;
	check_table(lookup_table);
}

int main(int argc, char** argv) {
	if (argc < 3) {
		cerr << "Usage: " << argv[0] << " pic_path compression_rate [G]" << endl;
		cerr << "Hint : [] means optional" << endl; 
		exit(1);
	}
	init_lookup_table(argv[2]);	
	return 0;
}

上面的代码展示了如何制作一个通用的look up table

运行结果:

之后进行颜色的压缩的时候,就可以直接查这个表,而不用每次都做乘法和除法

  • Tick

为了比较算法的性能,需要得到算法执行的时间,这可以通过tick来进行计算

tick是一个很小的时间单位,例如windows平台大多一个tick为1 / 10000 millisecond

opencv提供了两个跨平台的标准的函数来进行计算

1.cv::getTickCount()

获取从开机起,到目前一共经过了多少个tick

2.cv::getTickFrequency()

获得当前系统环境下的tick的频率(频率就是1秒多少次,用次数/频率 = 时间(s))

double t = (double)getTickCount();
// do something ...
t = ((double)getTickCount() - t)/getTickFrequency();
cout << "Times passed in seconds: " << t << endl;
  • 遍历图像(方法1,最快的方法,指针直接访问)

Mat& ScanImageAndReduceC(Mat& I, const uchar* const table)
{
    // accept only char type matrices
    CV_Assert(I.depth() == CV_8U);
    int channels = I.channels();
    int nRows = I.rows;
    int nCols = I.cols * channels;
    if (I.isContinuous())
    {
        nCols *= nRows;
        nRows = 1;
    }
    int i,j;
    uchar* p;
    for( i = 0; i < nRows; ++i)
    {
        p = I.ptr<uchar>(i);
        for ( j = 0; j < nCols; ++j)
        {
            p[j] = table[p[j]];
        }
    }
    return I;
}

解释:

Mat& ScanImageAndReduceC(Mat& I, const uchar* const table)

函数ScanImageAndReduceC见名知意,扫描图像并减少颜色

两个参数:

Mat& I (一个Mat对象的引用,Mat是opencv中图片的容器)

const unsigned char* const table 这里是表示指针本身的指向和指向的内容(这里是look_up_table中的值)都不能修改

关于C/C++中const完整用法,见我这两篇博客:

const 基础用法:https://blog.csdn.net/chenhanxuan1999/article/details/82973165

const 在类设计中应用:https://blog.csdn.net/chenhanxuan1999/article/details/78934915

这样传入了look_up_table 和 图片对象 I

然后可以通过这个传入的指针访问

 

这里有一个宏定义:

// accept only char type matrices

CV_Assert(I.depth() == CV_8U);

#define CV_8U   0

当I.depth() == CV_8U不成立时,报错

关于cv :: Mat :: depth()的官方文档:https://docs.opencv.org/4.1.0/d3/d63/classcv_1_1Mat.html#a8da9f853b6f3a29d738572fd1ffc44c0

查到:CV_8U - 8-bit unsigned integers ( 0..255 )

以及:The method returns the identifier of the matrix element depth (the type of each individual channel). For example, for a 16-bit signed element array, the method returns CV_16S

即这个函数,仅支持处理单通道使用8位无符号整型编码的图片

对于这三行:

    int channels = I.channels();
    int nRows = I.rows;
    int nCols = I.cols * channels;

这个可以参考图片的编码结构(基于CV_8U编码):

对于一个灰度图像:(单通道,only one channel)

每一个像素PIXI只有一个值(0-255)

对于一个三通道(不含 alpha透明度,仅BGR)

每一个像素由三个值共同组成

对于一个图片,其所对应的矩阵的大小就是Row * Col

在OpenCV中,可以使用Mat :: row() 和 Mat :: col() 分别获得像素的行个数和列个数

对于一个灰度的图像,其对应矩阵的Col即是mat.col(),行Row即是mat.row()

但对于彩色的图像其对应的行确实是mat.row(),但是对应的列还需要乘以通道数量

即:

int nCols = I.cols * channels;

 channel数据也可以通过OpenCV内置函数cv :: mat :: channels() 拿到:

int channels = I.channels()

接着进行了一个判断图像是否连续的操作:

使用了函数: bool cv::Mat::isContinuous

对应的官方文档:https://docs.opencv.org/4.1.0/d3/d63/classcv_1_1Mat.html#aa90cea495029c7d1ee0a41361ccecdf3

    if (I.isContinuous())
    {
        nCols *= nRows;
        nRows = 1;
    }

 如果图像是连续的,就可以看成是1 × (nCol * nRow) 的一维数组进行处理

    int i,j;
    uchar* p;
    for( i = 0; i < nRows; ++i)
    {
        p = I.ptr<uchar>(i);
        for ( j = 0; j < nCols; ++j)
        {
            p[j] = table[p[j]];
        }
    }

然后遍历这个矩阵

p = I.ptr<uchar>(i);

cv :: Mat :: ptr <uchar> (int num)

https://docs.opencv.org/4.1.0/d3/d63/classcv_1_1Mat.html#a13acd320291229615ef15f96ff1ff738

返回row = num对应的行的首地址

如果图像本身是连续的,那么有更加简单的写法:

uchar* p = I.data;
for( unsigned int i =0; i < ncol*nrows; ++i)
    *p++ = table[*p];

这里使用了cv :: Mat :: data属性,这个值为图像的左上角的像素点的起始地址

这样就避免了对于每一行单独使用 p = I.ptr<uchar>(i); 来获得地址

Demo: main.cpp

#include <iostream>
#include <cstdlib>
#include <sstream>
#include <opencv2/core.hpp>
#include <opencv2/opencv.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/highgui.hpp>

using namespace std;
using namespace cv;

unsigned char lookup_table[256];

void init_lookup_table(char* divide_factor) {
    for (int i = 0 ; i < 256 ; ++i) {
        lookup_table[i] = i;
    }
    stringstream ss;
    int compression_rate = 10;

    if (divide_factor != NULL) {
        ss << divide_factor;
        ss >> compression_rate;
    }

    if (!compression_rate) {
        cerr << "Error: No compreesion rate given" << endl;
        exit(1);
    }
    for (int i = 0 ; i < 256 ; ++i) {
        lookup_table[i] = (unsigned char)((compression_rate) * (lookup_table[i] / compression_rate));
    }
}

Mat& color_compress(Mat& I, const uchar* const table) {
    CV_Assert(I.depth() == CV_8U);
    uchar *p_row = NULL;  //  Start position of the row
    int nRow = I.rows;
    int nCol = I.cols * I.channels();
    if (I.isContinuous()) {
        nCol *= nRow;
        nRow = 1;
    }
    for (int i = 0 ; i < nRow ; ++i) {
        p_row = I.ptr<uchar>(i);
        for (int j = 0 ; j < nCol ; ++j) {
            p_row[j] = table[p_row[j]];
        }
    }
    return I;
}

int main(int argc, char** argv) {
    // Default:
    String img_name = "../media/cat.jpeg";
    char* p_com = NULL;
    if (argc < 3) {
        cerr << "Usage: " << argv[0] << " pic_path compression_rate [G]" << endl;
        cerr << "Hint : [] means optional" << endl;
        cerr << "Now use default img at '../media/cat.jpeg' " << endl;
        cerr << "Now use default compressing rate = 10" << endl;
        // exit(1);
    }
    else {
        img_name = argv[1];
        p_com = argv[2];
    }
    init_lookup_table(p_com);
    double time_start = (double)getTickCount();

    Mat source_img = imread(img_name, IMREAD_COLOR);
    namedWindow("Original", WINDOW_AUTOSIZE);
    imshow("Original", source_img);
    color_compress(source_img, lookup_table);
    namedWindow("Reduced", WINDOW_AUTOSIZE);
    imshow("Reduced", source_img);
    imwrite("../media/reduced.jpeg", source_img);
    double time_end = (double)getTickCount();
    cout << "Time consumption : " << (time_end - time_start) / getTickFrequency() << endl;
    waitKey();
    return 0;
}

 运行结果:

使用默认的压缩率10:

使用压缩率30:

使用压缩率50:

  • 遍历图像(方法2,使用迭代器,安全)

Mat& iterator_compress(Mat& I, const uchar* const table) {
    cout << endl << "********Use Iterator Scanner***********" << endl;
    CV_Assert(I.depth() == CV_8U);
    const int channels = I.channels();
    switch (channels) {
        case 1: {
            MatIterator_<uchar> it, end;
            for (it = I.begin<uchar>(), end = I.end<uchar>() ; it != end ; ++it) {
                *it = table[*it];
            }
            break;
        }
        case 3 :  {
            MatIterator_<Vec3b> it, end;
            for (it = I.begin<Vec3b>(), end = I.end<Vec3b>() ; it != end ; ++it) {
                (*it)[0] = table[(*it)[0]];
                (*it)[1] = table[(*it)[1]];
                (*it)[2] = table[(*it)[2]];
            }
            break;
        }
    }
    return I;
}

速度也比较快

  • 遍历图像(方法3,不常被用来进行遍历,常被用于随机访问某个像素点)

Mat& random_access_compress(Mat& I, uchar* const table) {
    cout << endl << "**********Use random access***********" << endl;
    CV_Assert(I.depth() == CV_8U);

    const int channels = I.channels();
    switch (channels) {
        case 1 : {
            for (int i = 0 ; i < I.rows ; ++i) {
                for (int j = 0 ; j < I.cols ; ++j) {
                    I.at<uchar>(i, j) = table[I.at<uchar>(i, j)];
                }
            }
            break;
    }
        case 3 : {
            Mat_<Vec3b> _I = I;
            for (int i = 0 ; i < I.rows ; ++i) {
                for (int j = 0 ; j < I.cols ; ++j) {
                    _I(i, j)[0] = table[_I(i, j)[0]];
                    _I(i, j)[1] = table[_I(i, j)[1]];
                    _I(i, j)[2] = table[_I(i, j)[2]];
                }
            }
            I = _I;
            break;
        }
    }
    return I;
}

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值