使用Qt框架编写的一个支持课程提供的.raw格式16位灰度图操作的图像处理器,自定义Image类型,封装几何变换、灰度映射、图像增强等功能,可以多步撤回及多图切换,附加一个比较简单的伪彩色图生成。
感谢C提供的按行读取图像方法;感谢L提供的完美的拉普拉斯滤波。
一、交互界面设计
(……具体ui设计待编辑ing,左边是一个缩略图栏,listwidget+Qicon,切换式功能栏借鉴室友刘某,但是给自己挖了大坑orz,比较多逻辑没有考虑清楚的……
1.取消撤回没有实现……
2.右键icon删除没做……
3.双击放大没做,现在的label太小了,即使有scroll也很不友好)
2022.11.19更新:
增加了伪彩色图生成。
二、自定义图像类型Image
类成员模仿QImage。
#ifndef IMAGE_H
#define IMAGE_H
#include<QImage>
#include<QPixmap>
#include<QLabel>
#include<complex>
#include <QtMath>
#include<QtGlobal>
class Image:public QImage
{
public:
enum format{
GRAY8,
GRAY16,
RGB32
};
Image();
~Image();
Image(const Image &image);//拷贝构造
Image(QString);//Qimage转换
Image(unsigned char* data,int width,int height,enum format fmt);
Image(unsigned short* data,int width,int height,enum format fmt);
Image* OpenFile_Gray16(QString FileName);//自定义文件格式读取
void SaveFile(QString FileName);//自选文件格式保存
QImage* convert(Image*);
Image* Rotate();//旋转
Image* Scaling();//缩放
Image* Flip();//左右翻转
Image* Grayscale();//灰度映射
Image* GrayFlip();//灰度反转
Image* Enhance();//图像增强
QString getFileName();
int getWidth();
int getHeight();
double getScale();
void setFileName(QString fn);
void convertmatrix();
void convertpixel(uchar**);
void setRotationAngle(double);
void setRotationCenterX(int);
void setRotationCenterY(int);
void setScale(double);
void setWinCenter(int);
void setWinWidth(int);
uchar getmyPixel8(int x,int y);
ushort getmyPixel16(int x,int y);
format getmyformat();
protected:
/*基本参数*/
QString FileName;//文件名
int height=0;//图像高
int width=0;//图像宽
enum format myformat;//图像格式
uchar* pixels=nullptr;//8位图像数据指针
ushort* pixels16=nullptr;//16位图像数据指针
int ByteCount;// 一个像素所需的字节数
int BytesPerLine;//一行字节数
/*几何变换参数*/
double RotationAngle=0;//旋转角度,默认正向转0度
int RoCX=0;
int RoCY=0;//旋转中心
double Scale=1.0;//缩放比例,默认等比例
/*灰度映射*/
int WinCenter=0;//窗位
int WinWidth=255;//窗宽
};
#endif // IMAGE_H
反思:
1.没有使用模板,每个类自带一个8位uchar数组,一个16位ushort数组,代码比较冗余,涉及大量8位和16位转换的问题,功能基本上都是用的if else判断format……
2.图像灰度值对数据类型及范围非常敏感(因为int、ushort、uchar出了无数次乌龙……反思……)
3.浪费很多内存……嗯……Qrz,比如各种int和double
4.继承了QImage主要是为了在图像保存的时候偷懒……
三、具体功能
(一)文件读取
特化的文件读取,目前的逻辑是检测到文件名最后四个是‘.raw’就调用这种读取方式,不然就直接拿QImage的标准格式读取(…)再转换成Image
Image* Image::OpenFile_Gray16(QString fn)
{
fn=QDir::toNativeSeparators(fn);//绝对路径‘/’与‘\’转化
char* ch;
QByteArray ba = fn.toLatin1();
ch=ba.data();//文件名格式转化
ifstream is(ch, ifstream::binary);//文件流
ulong w,h,bytesperline;
enum format fmt=format::GRAY16;
is.seekg(0);
is.read((char*)&w, 4);//读4字节宽度
is.seekg(4);
is.read((char*)&h,4);//读4字节长度
is.seekg(8);
bytesperline= (w*8 +31)/32*4;
ushort* data=new ushort[h*bytesperline];
//按字节读取
/*int k=0;
for(int y=0;y<h;y++)
for(int x=0;x<w;x++)
{
is.seekg(8+k);
is.read((char*)&data[y*bytesperline+x],2);
k=k+2;
}*/ //一个数据两个字节
//改进:按行读取
//QT的QDataStream:适用于二进制流的文件读取/写入操作
for(int y=0;y<h;y++)
{
is.read(reinterpret_cast<char*>(data+y*bytesperline),sizeof(ushort)*w);
}
Image* Gray16Image=new Image(data,w,h,fmt);
Gray16Image->setFileName(fn);
is.close();
delete[] data;
return Gray16Image;
}
(二)几何变换
其实可以统一成仿射变换……16位的else if就不放了
Image* Image::Rotate()
{
//负数代表逆时针转,正数代表顺时针转
double theta = RotationAngle * M_PI/ 180;
double Cos=cos(theta);
double Sin=sin(theta);
double Cos2=qAbs(Cos);
double Sin2=qAbs(Sin);
int OpHeight = round(width * Sin2 + Cos2 * height);
int OpWidth = round(width * Cos2 + Sin2 * height);
int OpBytesPerLine=(OpWidth*ByteCount +31)/32*4;
double temp1, temp2;
temp1 = -0.5 * OpWidth * Cos - 0.5 * OpHeight * Sin + 0.5 * width;
temp2 = 0.5 * OpWidth * Sin - 0.5 * OpHeight * Cos + 0.5 * height;
if(myformat==format::GRAY8)
{
uchar* Oppixels=new uchar[OpHeight*OpBytesPerLine];
for (int y0 = 0; y0 < OpHeight; y0++)
{
for (int x0 = 0; x0 < OpWidth; x0++)
{
Oppixels[y0*OpBytesPerLine+x0] = 255;
}
}
Image* OpImage= new Image(Oppixels,OpWidth,OpHeight,myformat);
double SrcX, SrcY;//旋转后坐标位置
for (int y0 = 0; y0 < OpHeight; y0++)
{
for (int x0 = 0; x0 < OpWidth; x0++)
{
SrcX = RoCX+(x0-RoCX) * Cos + (y0-RoCY) * Sin + temp1;
SrcY = RoCY-(x0-RoCX) * Sin + (y0-RoCY) * Cos + temp2;
//SrcX = x0 * Cos + y0* Sin + temp1;
//SrcY = -x0* Sin + y0* Cos + temp2;
//双线性插值
if (SrcX >= 0 && SrcX <= width - 1 && SrcY >= 0 && SrcY <= height - 1)
//在原画布大小区域内
{
int X1 = round(SrcX-0.5);
int X2 =round(SrcX+0.5);
int Y2 =round(SrcY+0.5);
int Y1 =round(SrcY-0.5);
//相邻四个点以不同权值确定新图(x0,y0)出灰度值
OpImage->pixels[y0*OpBytesPerLine+x0] =
(Y2-SrcY)*((X2-SrcX)* pixels[Y1*BytesPerLine+X1]
+ (SrcX - X1)*pixels[Y1*BytesPerLine+X2])
+(SrcY-Y1)*((X2-SrcX)* pixels[Y2*BytesPerLine+X1]
+ (SrcX-X1)*pixels[Y2*BytesPerLine+X2]);
if (OpImage->pixels[y0*OpBytesPerLine+x0]> 255)
{
OpImage->pixels[y0*OpBytesPerLine+x0]= 255;
}
}
else
{
OpImage->pixels[y0*OpBytesPerLine+x0] = 255;//白色填充
}
}
}
delete[] Oppixels;
return OpImage;
}
}
Image* Image::Scaling()
{
int OpHeight =round(height * Scale);
int OpWidth = round(width *Scale);
int OpBytesPerLine=(OpWidth*ByteCount +31)/32*4;
if(myformat== format::GRAY8)
{
uchar* Oppixels=new uchar[OpHeight*OpBytesPerLine];
for (int y0 = 0; y0 < OpHeight; y0++)
{
for (int x0 = 0; x0 < OpWidth; x0++)
{
Oppixels[y0*OpBytesPerLine+x0] = 255;
}
}
Image* OpImage= new Image(Oppixels,OpWidth,OpHeight,myformat);
double SrcX, SrcY;//缩放后坐标位置
for (int y0 = 0; y0 < OpHeight; y0++)
{
for (int x0 = 0; x0 < OpWidth; x0++)
{
SrcX=(x0+0.5)/Scale -0.5;
SrcY=(y0+0.5)/Scale -0.5;
//正好落在采样点上,(SrcX、SrcY)为整数,否则双线性插值计算
if (SrcX >= 0 && SrcX <= width-1 && SrcY >= 0 && SrcY <= height - 1)
//在原画布大小区域内
{
int X1 =round(SrcX-0.5);
int X2 =round(SrcX+0.5);
int Y2 =round(SrcY+0.5);
int Y1 =round(SrcY-0.5);
//相邻四个点以不同权值确定新图(x0,y0)灰度值
OpImage->pixels[y0*OpBytesPerLine+x0] =
(Y2-SrcY)*((X2-SrcX)* pixels[Y1*BytesPerLine+X1]
+ (SrcX - X1)*pixels[Y1*BytesPerLine+X2])
+(SrcY-Y1)*((X2-SrcX)* pixels[Y2*BytesPerLine+X1]
+ (SrcX-X1)*pixels[Y2*BytesPerLine+X2]);
if (OpImage->pixels[y0*OpBytesPerLine+x0]> 255)
{
OpImage->pixels[y0*OpBytesPerLine+x0]= 255;
}
}
else
{
OpImage->pixels[y0*OpBytesPerLine+x0] = 255;//白色填充
}
}
}
delete[] Oppixels;
return OpImage;
}
(三)灰度映射(2022.11.19)
医学图像不适用直方图均衡算法,会丢失灰度,采用分段线性灰度映射。
Image* Image::Grayscale()
{
int L=4096;
int WinMax=WinCenter+(WinWidth/2);
int WinMin=WinCenter-(WinWidth/2);
int n=height*width;//像素数
int i,j,y0,x0=0;
uchar *S=new uchar[L];//[WinMin,WinMax]->[0,255]映射
int OpBytesPerLine= (width*8 +31)/32*4;
uchar* Oppixels=new uchar[height*OpBytesPerLine];
enum format opfmt=format::GRAY8;
/*线性灰度映射*/
double k=(double)255/WinWidth;
double b=(double)(-255)*WinMin/WinWidth;
for(i=0;i<L;i++)
{
if(i<WinMax&&i>WinMin){
S[i]=(uchar)(k*i+b);
}
else if(i>=0&&i<=WinMin)
{
S[i]=0;
}
else if(i>=WinMax&&i<L)
{
S[i]=255;
}
}
for(y0=0;y0<height;y0++)
{
for(x0=0;x0<width;x0++)
{
ushort r2=pixels16[y0*BytesPerLine+x0];
Oppixels[y0*OpBytesPerLine+x0]=S[r2];
}
}
Image* OpImage=new Image(Oppixels,width,height,opfmt);
delete[] S;
delete[] Oppixels;
return OpImage;
}
(四)拓展:伪彩色图生成(2022.11.19)
用了一个比较简单的分四级灰度计算LUT(色彩查找表),且偷懒用了QImage的RGB格式,没有直接更新Image类,于是把跟彩色图有关的操作全部扔在了MainWindow里。
void MainWindow::generateLUT()
{
int R,G,B;
int L=255;
for(int i=0;i<=L;i++)
{
if (i<=L/4)
{ R=0;
G=4*i;
B=L;}
else if (i>L/4&&i<=L/2)
{ R=0;
G=L;
B=-4*i+2*L; }
else if (i>L/2&&i<=3*L/4)
{ R=4*i-2*L;
G=L;
B=0;}
else
{ R=L;
G=-4*i+4*L;
B=0;}
R=qBound(0,R,255);
B=qBound(0,B,255);
G=qBound(0,G,255);
QRgb rgb=qRgba(R,G,B,1);
LUT[i]=rgb;
}
}
QImage应该是可以生成索引图,赶ddl交了作业没有仔细想,用了简单粗暴的循环赋值。
不支持16位灰度的查表,必须先进行灰度窗。
void MainWindow::on_pushButton_color_clicked()
{
OpImage=CurImage;
/*灰度映射*/
if(ui->checkBox_Gray1->isChecked()&&!ui->checkBox_Gray2->isChecked())
{
int WWidth=ui->textEdit_WinWidth->toPlainText().toInt();
int WCenter=ui->textEdit_WinCenter->toPlainText().toInt();
OpImage->setWinWidth(WWidth);
OpImage->setWinCenter(WCenter);
OpImage=OpImage->Grayscale();
LastOp=CurOp;
CurOp=Operation::GRAYSCALE;
}
if(ui->GrayFlipcheckBox->isChecked())
{
OpImage=OpImage->GrayFlip();
LastOp=CurOp;
CurOp=Operation::GRAYFLIP;
}
CoImage=QImage(OpImage->getWidth(),OpImage->getHeight(),QImage::Format_RGB32);
CoImage.fill(QColor(Qt::black));
if(OpImage->getmyformat()==Image::format::GRAY8)
{
Color=true;
for (int y0=0;y0<CoImage.height();y0++) {
for (int x0=0;x0<CoImage.width();x0++) {
CoImage.setPixel(x0,y0,LUT[OpImage->getmyPixel8(x0,y0)]);
}
}
}
ShowCoImage(CoImage);
}
(待更新)