按:测试时发现对于大个的图片文件,显示有明显延迟。跟踪后发现从硬盘读取数据时,需要消耗0.2秒左右的时间,如果要旋转图像,时间更长,这样使用起来体验很差。因此,提高显示速度也是成了一个需要解决的问题。
显然要用到多线程了。本人用过多线程,但是复杂的都是抄来的,因此对于其中的控制并不是很熟练,故而不想搞得太复杂。经过探索,发现采用std::thread 的最简单的多线程方法就可以实现预读功能。
程序中用一个成员变量记录要显示的图像,两个成员变量做预读文件的缓冲,一个用于向后浏览,一个用于向前浏览。为了进一步提高速度,启用了两个线程分别读取这两个文件。线程调用和线程代码如下:
// For Next button
void CseePictureDlg::OnBnClickedBtnNext()
{
// TODO: 在此添加控件通知处理程序代码
m_Pos++;
if (m_Pos >= m_ImgFilesVct.size()) {
m_Pos = 0;
}
//m_pBufferThread->join();
m_Img.copyImg(m_nextImg, m_Img);
std::thread thr1(&CseePictureDlg::readNextImageThread, this);
std::thread thr2(&CseePictureDlg::readPrevImageThread, this);
DrawPic(m_Img, true);
thr1.join();
thr2.join();
}
m_Img 保存当前显示的图片,是一个继承于CImage类的成员变量。该类中有各种图像操作的方法,copyImg是图像拷贝函数。(本开发日志系列会在最后给出核心代码,和大家共勉)
下面是读取线程代码(只列出其中一个)
// For Next thread
// the thread is used to buffer the image file
afx_msg UINT CseePictureDlg::readNextImageThread()
{
if (m_ImgFilesVct.size() == 0)
return -1;
int pos;
string file;
CString imgFile;
CImage bufImg;
//1#read the next one
pos = m_Pos +1;
if (pos >= m_ImgFilesVct.size()) {
pos = 0;
}
file = m_ImgPath + "\\" + m_ImgFilesVct[pos].getName();
imgFile = file.c_str();
if (!m_nextImg.IsNull()) {
m_nextImg.Destroy();
}
m_nextImg.Load(imgFile);
//如果需要自动向上显示图片....
if (m_autoUpWard) {
CImage tmpImg;
CExif exif;
exif.setImage(imgFile);
int degree;
degree = exif.getRotateAngle();
if (degree != 0) {
CMsApiRotate rtt;
rtt.filter(m_nextImg, degree,tmpImg );
m_Img.copyImg(tmpImg, m_nextImg);
}
}
图像旋转有多种方法,简单分为两类,一种是利用系统提供的API,一种是自己写代码实现。这两种方法,笔者都进行了测试,发现系统提供的方法在特殊角度旋转时速度并没有比自己的代码快,多数情况下稍慢一些,因此最终代码中用了自己的代码,大文件的耗时还是比较明显的。时间大约在0.5ms左右,文件大小11M,图像大小6000*6000 左右。
先介绍昨晚刚刚写完的利用API完成的旋转代码(这个代码也耗费了笔者几天的时间,主要是对API的理解不够准确,自己测试了很多情况,才搞清楚端倪。微软的msdn访问起来太费劲了,thanks to GFW!!!)
直接上代码
// For Next thread
#pragma once
#include
#include
#include
class CMsApiRotate
{
public:
CMsApiRotate()
{
}
~CMsApiRotate()
{
}
void filter(CImage &srcImg, int angle, CImage &dstImag) {
XFORM xform; // 保存旋转数据的结构体
double fangle = -(double)angle / 180. * 3.1415926; // 计算弧度
xform.eM11 = (float)cos(fangle);
xform.eM12 = (float)sin(fangle);
xform.eM21 = (float)-sin(fangle);
xform.eM22 = (float)cos(fangle);
int OW = srcImg.GetWidth();
int OH = srcImg.GetHeight();
int NW = abs(OW*cos(fangle))+ abs(OH*sin(fangle));
int NH = abs(OH*cos(fangle)) + abs(OW*sin(fangle));
if (!dstImag.IsNull()) {
dstImag.Destroy();
}
dstImag.Create(NW, NH, 24);
CPoint centerPt;
CRect rect;
rect.SetRect(0, 0, dstImag.GetWidth(), dstImag.GetHeight());
centerPt.x = (rect.left + rect.right) / 2;
centerPt.y = (rect.top + rect.bottom) / 2;
xform.eDx = (float)(centerPt.x - cos(fangle)*centerPt.x + sin(fangle)*centerPt.y);
xform.eDy = (float)(centerPt.y - cos(fangle)*centerPt.y - sin(fangle)*centerPt.x);
CDC* pSrcDC = CDC::FromHandle(srcImg.GetDC());
CDC* pDstDC = CDC::FromHandle(dstImag.GetDC());
int nGraphicsMode = SetGraphicsMode(pDstDC->m_hDC, GM_ADVANCED); //设置图形模式
SetWorldTransform(pDstDC->m_hDC, &xform);
int ModeOld = SetStretchBltMode(pDstDC->m_hDC, COLORONCOLOR);//设置指定设备环境中的位图拉伸模式, looks like STRETCH_HALFTONE mode, 0.16 ms or 0.00 ms
int nx, ny;
nx = NW / 2 - OW / 2;
ny = NH / 2 - OH / 2;
pDstDC->StretchBlt(nx, ny, srcImg.GetWidth(), srcImg.GetHeight(), pSrcDC, 0, 0, srcImg.GetWidth(), srcImg.GetHeight(), SRCCOPY);
SetStretchBltMode(pDstDC->m_hDC, ModeOld);
xform.eM11 = (float)1.0;
xform.eM12 = (float)0;
xform.eM21 = (float)0;
xform.eM22 = (float)1.0;
xform.eDx = (float)0;
xform.eDy = (float)0;
SetWorldTransform(pDstDC->m_hDC, &xform);
SetGraphicsMode(pDstDC->m_hDC, nGraphicsMode); // 恢复图形模式
dstImag.ReleaseDC();
srcImg.ReleaseDC();
}
};
简单说明:
上述代码的API是 SetWorldTransform
核心技巧是旋转后图像大小的计算以及StretchBlt 函数的前两个位置参数的计算。
第一个函数的使用情况大家自行搜索,下面简单介绍一下两个位置参数的计算方法。
StretchBlt 功能:将原图像中的部分图像打印到目标图像中涉及到原图像中子图像范围,以及目的图像中的打印位置(说明:这个位置可以负数)。
SetWorldTransform的功能是旋转画布,这样产生的效果就是图像旋转。因此,在旋转画布之前,要把要旋转的图像的位置和大小准备好,这样旋转之后,才能使图像正好出现在显示区域内。这需要计算两个关键参数,图像的左上角坐标和旋转中心坐标。因为是固定大小旋转,因此我们将中心设定在图像的显示中心。这样需要计算选中图像的高和宽。
如图
A1为旋转后的图像大小,令其左上角坐标为原点,那么旋转中心就是ta的几何中心。角度简单标注如图,其中d0是是对角线和一邻边的夹角,这样利用三角公式可以计算出新图像的长和宽分别为:
NW = owcos(d)+ohsin(d)
NH = ohcos(d)+owsin(d)
其中
旋转角度为 d
原图:Ow*OH;
新图:Nw,NH
则:新图像的
这样
旋转中心点坐标(相对于新图):
Cp(NW/2,NW/2)
原图在和新图几何中心一致时的
Left = NW/2-Ow/2;
Top = NH/2 – OH/2;
这样就获得了 StretchBlt 前两个定位参数。图像大小取和原图大小一致,这样得到如下代码:
pDstDC->StretchBlt(nx, ny, srcImg.GetWidth(), srcImg.GetHeight(), pSrcDC, 0, 0, srcImg.GetWidth(), srcImg.GetHeight(), SRCCOPY);
其余代码不在详述。
自己编写的旋转代码,将继续在后续的日志中更新。
谢谢大家阅读。
2020-03-27 于泛五道口地区
ps:今天发明了一个新词:仰光瞑寐