0 项目需求:
解析PCB电路板的gerber文件,将PCB电路板图像显示并绘制出PCB电路板BMP图像用于喷墨打印,针对打印需求,需要输出1bit图像、2bit图像、1bit&2bit反色图、8bit墨量直观图、预览图和xml文件,同时,为控制喷头喷印的墨量,需要对图像进行处理,比如2bit灰度变化、抽点、削减线宽,为防止喷墨后有墨流出,需要对色块的边缘进行一圈筑坝。解析和显示部分由同事完成,本人主要完成PCB电路板BMP图像处理部分,即上述黑体字所描述的功能。
1 绘制8bit图像
业务上并没有输出8bit图像的需求,但由于直接对1bit图像进行绘制看上去可行,但是1bit图像只有黑白两种颜色,0表示黑色,1表示白色,由于2bit图像是由1bit图像转换而来,1bit图像无法进行灰度变化,所以我们无法对2bit进行灰度变化,所以这种方法不可取。8bit图像用1字节表示一个像素,具有256种灰度值,如果2bit需要进行灰度变化,那就先对8bit图像进行灰度变化,然后转化后的2bit也就进行了相应的灰度变化,因此这里先对8bit图像进行绘制,将8bit转化为2bit和1bit,同时也可以用于后面的2bit图像灰度变化。
绘制直线、圆弧、闪绘、自定义区域:
内外削:
核心思想:通过腐蚀和膨胀达到内外削的效果
- 主要使用的halcon算子:
erosion_rectangle1 dilation_rectangle1
抽点:
抽点也是一种控制墨量的方式,通过对白油块的部分白色素转化为黑色素,从而减少喷墨量,由于需要避免喷墨不均匀的问题,因此需要均匀减少各个区域的喷墨量,即均匀地将白色素转化为黑色素。算法步骤如下:
- 通过Halcon读取图像,并进行阈值分割,将白色前景和黑色背景分割开来。
- 利用Halcon查找连通区域,并按面积筛选连通区域(太小的白油块不进行抽点),然后统计符合需求的,需要进行抽点的连通区域数量。
- 多线程处理连通区域,每个线程处理一部分连通区域
- 接下来就是针对每个连通区域进行处理,由于我们需要均匀地将白色素转化为黑色素,所以需要获取该连通区域的最小外接矩形,然后从左上角到右下角以对角线方向改变像素颜色,改变像素颜色之前要注意判断该像素是否在联通区域内。
- 由于需要实现25%、50%、75%不同程度的抽点,因此需要设置步长,对于25%程度的抽点,每4条对角线,沿对角线方向进行一次抽点,对于50%程度的抽点,每2条对角线,沿对角线方向进行一次抽点,对于75%程度的抽点,4条对角线,对3条对角线进行抽点。
筑坝: 本质上是一种边缘提取,用较浅色的灰度值在区域周围画一圈轮廓,从而减少周围区域墨量,防止周围墨量溢出。算法主要步骤:
- 使用halcon算子读取图像,并进行阈值分割,提取白色区域
- 对于阈值分割后的区域进行连通区域分析
- 对分析得到的连通区域以绘制边缘的方式进行绘制,同时指定较浅色的灰度值,实现边缘提取,即筑坝功能。
2 获取8位深图像底层2进制数据
逐行读取有效数据。首先计算8比特图像初始偏移量,即文件头+信息头+调色板的大小,即14+50+256*4 = 1078字节,定位到初始偏移量处,读取一行的有效字节数,存入事先申请的空间中,并将指向该行数据的指针存入vector容器中,然后初始偏移量+第一行实际字节数(有效字节数+填充字节),得到新的偏移量,并定位到新的偏移量处,然后读取第二行的有效字节数,存入事先申请的空间中,并将指向行数据的指针存入vector容器中,如此循环,不断读取每一行的有效字节数,并按顺序将指向行数据的指针存入vector容器中,直到每一行均读取并存储完毕。
3 图像位深度转换
3.1 8bit图像转化为2bit/1bit
8bit图像转化为2bit和1bit思路一模一样,接下来以8bit转化为2bit为例,主要算法步骤如下:
- 创建并设置2位深图像文件头和信息头的基本信息,并定义调色板
//创建并设置BMP文件头和信息头
BITMAPFILEHEADER fileHeader;
BITMAPINFOHEADER infoHeader;
fileHeader.bfType = 0x4D42;
fileHeader.bfSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + (pixelWidth * 2 + 31) / 32 * 4 * pixelHeight;
fileHeader.bfReserved1 = 0;
fileHeader.bfReserved2 = 0;
fileHeader.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + sizeof(colors2);
infoHeader.biSize = sizeof(BITMAPINFOHEADER);
infoHeader.biWidth = pixelWidth;
infoHeader.biHeight = pixelHeight;
infoHeader.biPlanes = 1;
infoHeader.biBitCount = 2;
infoHeader.biCompression = BI_RGB;
infoHeader.biSizeImage = 0;
infoHeader.biXPelsPerMeter = 0;
infoHeader.biYPelsPerMeter = 0;
infoHeader.biClrUsed = 0;
infoHeader.biClrImportant = 0;
//定义2位深图像调色板
RGBQUAD colors2[4];
colors2[0].rgbBlue = 0; // 黑色
colors2[0].rgbGreen = 0;
colors2[0].rgbRed = 0;
colors2[0].rgbReserved = 0;
colors2[1].rgbBlue = 96; // 暗灰色
colors2[1].rgbGreen = 96;
colors2[1].rgbRed = 96;
colors2[1].rgbReserved = 0;
colors2[2].rgbBlue = 48; // 淡灰色
colors2[2].rgbGreen = 48;
colors2[2].rgbRed = 48;
colors2[2].rgbReserved = 0;
colors2[3].rgbBlue = 255; // 白色
colors2[3].rgbGreen = 255;
colors2[3].rgbRed = 255;
colors2[3].rgbReserved = 0;
- 将文件头、信息头、调色板写入文件
ofstream image2bit = ofstream(finalOutputPath2.toLocal8Bit(), ios::binary);
if (!image2bit.is_open())
{
qDebug() << "generate2bit: file of 2bit open failed";
return false;
}
image2bit.write(reinterpret_cast<const char*>(&fileHeader), sizeof(BITMAPFILEHEADER));
image2bit.write(reinterpret_cast<const char*>(&infoHeader), sizeof(BITMAPINFOHEADER));
image2bit.write(reinterpret_cast<const char*>(&colors2), sizeof(colors2));
- 创建线程,并行处理多行数据,将8位深图像数据转化为2位深图像数据
- 具体转化思路为,将8位深图像的有效字节数(去除内存对齐的部分)转化为2bit,比如00000000转化为00,11111111转化为11。如果2位深图像存满一字节,则算出其所对应的实际值写入一片内存中。8位深图像一行的有效数据部分转化完后,开始对2位深图像进行内存对齐处理,先对未填满字节进行比特填充,最后再填充字节。
- 每个线程处理一部分行数据,每个线程对应一个vector容器,用于存储该线程所处理的所有行数据,每处理好一行数据,就将指向该行数据的指针存入vector中,即vetocr中每一个元素对应2位深图像完整一行的数据,该线程处理完毕后,将该线程的索引号和vector容器存到map集合中。
- 所有线程处理完毕后,遍历map,依次获取每一个线程的vector容器,依次将每个vector中的每一行数据写入2位深图像文件中。
- bmp图像文件格式超详解
3.2 2bit图像转化为8bit
- 前面几步和8转2一样,都是创建并设置文件头、信息头基本信息,定义调色板,将文件头、信息头和调色板写入图像文件中。
- 多线程转化,每个线程分别处理相应的行。
- 具体转化思路为,将2位深图像的有效字节数(去除内存对齐的部分)和最后一字节的有效比特数转化为8bit,比如00转化为00000000,11转化为11111111。通常情况下每一字节实际要转化的比特数就是8位,我们依次计算每两位的像素值,根据值的大小转化为8bit图像的像素值,但是当达到一行最后一个有效字节的时候需要注意,2bit图像最后一个有效字节可能只有部分比特是表示像素,其余比特可能是填充的部分,所以对于最后一个有效字节要注意判断有效比特数,不要将填充的部分进行转化了。当最后一个有效字节的有效比特部分全部转化完,这个时候就要开始考虑内存对齐,对于8比特图像而言,不存在需要填充字节的情况,只需要将这一行的总字节数减去有效字节数,就是我们要填充的字节。内存对齐完成后,将该行数据所在空间的指针存储在该线程所对应的vector容器中。当该线程处理完毕其所对应的所有行后,将该线程索引号和相应的vector容器存入map集合。
- 所有线程处理完毕后,遍历map,依次获取每一个线程的vector容器,依次将每个vector中的每一行数据写入8位深图像文件中。
4 输出8bit墨量直观图
将2bit图像通过位深度转化算法转化为8位深度彩色图,具体算法步骤如下:
- 创建并设置文件头、信息头基本信息,定义调色板 ,并将文件头、信息头、调色板写入图像文件
- 创建线程,并行处理多行数据,将2位深图像数据转化为8位深图像数据。
- 具体转化步骤为:将2位深图像的每一行有效字节数每两位转化为相应的1字节,当处理完最后一个有效字节后,需要进行内存对齐处理,8位深图像直接填充字节即可。
- 每个线程处理一部分行数据,每个线程对应一个vector容器,vetocr中每一个元素对应8位深图像完整一行的数据,将处理好的8位深行数据存入到该vector容器中,该线程处理完毕后,将该线程的索引号和vector容器存到map集合中。
- 所有线程处理完毕后,依次将每个vector中的每一行数据写入8位深图像文件中。
5 输出预览图
核心思想: 对1bit图像进行缩放
-
主要使用的halcon算子:
GetImageSize ZoomImageSize