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
目录-基于图像遍历的颜色压缩
遍历图像(方法3,不常被用来进行遍历,常被用于随机访问某个像素点)
-
颜色压缩算法(color space reduction)
算法描述:
算法解释:
即新的色彩值 = 压缩率 * (旧的色彩值 / 压缩率)
颜色压缩基于一个很简单的想法,对于一个8个位表示的一种单一的颜色,三通道RGB/BGR能够组合出
即接近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提供了两个跨平台的标准的函数来进行计算
获取从开机起,到目前一共经过了多少个tick
获得当前系统环境下的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
#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;
}