miruos的文章

  • 计算机视觉现代优化方法

    1. 概述本次实验中,我基于OpenCV,实现了一个二维图像配准工具,全部代码均为自行实现,OpenCV用于计算图像变换与相似度。
    该工具能够将一幅图像进行变换,并与另一幅图像相匹配。支持包括平移、旋转(含平移、缩放)、仿射与透视共四种变换,使用L1、L2、无穷范数作为优化的目标函数,实现了暴力算法、梯度下降法、模拟退火算法来求解该优化问题。
    2. 应用问题如果两幅图像,它们是在同一场景、不同角度下拍摄的,那么,存在一种图像变换,使得其中一幅图像经过变换后,能与另一图像大部分重合。
    上述图像变换被称为配准(registration)T,被变换的图像被称为参考图像I_M,另一图像被称为目标图像I_F。优化的目标是使变换后的参考图像T(I_M)与目标图像I_F的差异尽可能低。
    最简单的图像变换是平移变换,需要确定两个参数: x和 y; 旋转变换通常与缩放、平移共同进行,需要确定四个参数: x、y、theta、scale; 仿射变换将矩形图像线性映射至一个平行四边形,需要确定三个坐标点,共六个参数,三个坐标点分别表示原图左上、右上、左下角变换至新图像的坐标位置; 透视变换与仿射变换相似,不同的是原图像的四个顶点可变换至任意的四边形,所以需要确定四个坐标点,共八个参数。此外,也有更为精细的图像变换方法,但相比于上述简单变换,其参数较多,难以优化,故本次实验不予考虑。
    对于图像相似度,需针对使用场景选择合适的度量方法。本实验中,实现的方法有L1(1范数)、L2(2范数)、无穷范数三种。
    总的来说,问题可以总结为如下步骤:

    输入参考图像、目标图像;
    选择合适的变换,确定参数范围;
    设置初始参数,在这个参数下变换参考图像,并计算与目标图像的差异;
    调整参数,使上述差异达到最小值;
    输出最优参数作为配准变换。

    3. 数据来源本实验使用在实验室拍摄的四张照片作为测试数据。














    4. 实现算法4.1 软件界面本实验搭建了一个二维图像配准工具。”主界面”可进行配准的各项设置,并展示参考图像与目标图像:

    “结果界面”会在运行配准之后弹出,分左中右三栏,分别为变换后的参考图像、变换后的参考图像与目标图像的对比(会交替显示两图像)、目标函数优化曲线:

    4.2 优化算法在上述基础上实现了三种优化算法,分别是暴力算法、梯度下降法、模拟退火算法。执行算法前,首先会在Registration::initialize()函数中针对不同变换方式,初始化变换的各项参数,例如旋转变换(四个参数):
    params.resize(4); // cx cy angle scalelimits.resize(4);steps.resize(4);limits[0] = std::make_pair(-c / 2, c / 2);limits[1] = std::make_pair(-r / 2, r / 2);limits[2] = std::make_pair(-180, 180);limits[3] = std::make_pair(0.5, 2);for (int i = 0; i < params.size(); i++) { params[i] = limits[i].first;}steps[0] = 1;steps[1] = 1;steps[2] = 1;steps[3] = 0.01;
    其中params表示参数数组,limits表示各个参数的取值范围,steps表示各个参数邻域的步长。
    暴力算法使用深度优先搜索遍历整个可行解空间,可求解出全局的最优解:
    void Registration::optimizeNaive(int pos) { if (pos >= params.size()) { completeIteration(); return; } for (float i = limits[pos].first; i <= limits[pos].second; i += steps[pos]) { params[pos] = i; optimizeNaive(pos + 1); }}
    梯度下降法在每轮迭代中,寻找当前最优解的邻域中最优的可行解,并以此代替最优解,直到邻域中没有比当前解更好的解为止。我实现的函数中,某个可行解的邻域定义为将第i个参数增加或减少steps[i]得到的可行解的集合(i为某个随机参数的序号)。因梯度下降法与初始值选取密切相关,选择不当会导致陷入局部最优解,我在函数中增加了一个外循环,每轮循环随机选择一组初始值,并开始一轮新的梯度下降:
    void Registration::optimizeGD() { for (int i = 0; i < 10000; i++) { double tmp_loss = 1e30; double cur_loss = 1e30; for (int j = 0; j < params.size(); j++) { // give random start values params[j] = random(limits[j].first, limits[j].second); } while (true) { std::vector<float> old = params; std::vector<float> best = params; for (int pos = 0; pos < params.size(); pos++) { params[pos] = old[pos] + steps[pos]; optimizeGDHelper(best, cur_loss); params[pos] = old[pos] - steps[pos]; optimizeGDHelper(best, cur_loss); params[pos] = old[pos]; } if (tmp_loss == cur_loss) { break; } tmp_loss = cur_loss; params = best; completeIteration(); } completeIteration(); }}void Registration::optimizeGDHelper(std::vector<float>& best, double& cur_loss) { applyTransform(); double s = getSimilarity(trans_img, tar_img, similarity_type); if (s < cur_loss) { cur_loss = s; best = params; }}
    模拟退火算法与梯度下降法相比,具有跳出局部最优解的能力。当温度较低时,算法不容易跳出局部最优解,从而退化为梯度下降法。所以,如果在温度下降到很低时没能跳入最优解附近,算法同样会收敛于局部最优解。因此,与上述梯度下降法的代码类似,我也使用了一层外循环,用于生成多个随机初始参数:
    void Registration::optimizeSA() { for (int i = 0; i < 10000; i++) { for (int j = 0; j < params.size(); j++) { // give random start values params[j] = random(limits[j].first, limits[j].second); } applyTransform(); double fi = getSimilarity(trans_img, tar_img, similarity_type); double temp = 10; double r = 0.99; int k = 1000; while (k > 0) { // select a random neighbor int len = params.size(); int r1 = rand() % len; double r2 = random(1, 10) * ((rand() % 2) * 2 - 1); params[r1] += r2 * steps[r1]; applyTransform(); double fj = getSimilarity(trans_img, tar_img, similarity_type); double delta = fj - fi; if (random(0, 1) < exp(-delta / temp)) { // jump to this neighbor completeIteration(iter % 10 == 0); fi = fj; } else { params[r1] -= r2 * steps[r1]; } temp *= r; k--; } completeIteration(); }}
    4.3 实现细节
    计算图像变换、图像相似度需要遍历图像每一像素,这一过程是较为耗时的。对此,代码中将图像先缩小至原有尺寸的1/10,即,像素数变为原来的1/100,再进行变换、相似度计算。最终再恢复至原有图像尺寸。
    图像变换后,其周围可能出现空白区域(下图),在计算图像相似度时,这一区域也是需要计算的(否则会倾向于生成全是边界的图像),所以需要填补这一区域。我使用的方法是计算全图像的平均颜色,并以平均颜色填补。


    5. 实验结果与分析5.1 目标函数值可视化下图展示了4号图片到1号图片的平移变换的优化目标函数值。平移变换有两个参数,所以可以映射到二维空间。使用上述暴力算法计算出参考图像平移至所有位置,计算其目标函数值,便得到如下的可视化结果。
    从图中可以看出目标函数最优值(最小值)位于图像中间附近,有局部最优值,但该函数较为光滑,因此可认为性质较好。

    5.2 结果5.2.1 暴力算法暴力算法效率较低,所以只进行了平移变换、旋转变换的实验。其中,平移变换约耗时3秒,旋转变换耗时数分钟。
    该算法可以确保找到最优解,可作为其他算法的验证手段。
    下图是枚举平移变换参数时,相似度函数的变化曲线:

    5.2.2 梯度下降法下图是使用梯度下降法优化仿射变换配准问题的结果,在迭代至5000轮左右时,已经找到较好的结果。这里,每一轮迭代指的是找到一个新的最优值,或开启一次新的梯度下降。
    右侧的蓝色曲线表示梯度下降过程的目标函数值变化,可以看出,在每轮梯度下降过程内,目标函数值能够快速下降。

    5.2.3 模拟退火算法下图使用了模拟退火算法来计算5.2.2中相同的优化问题以便对比。该方法约在4000轮左右取得一个较好的解,更好的解出现在50000轮左右。
    相比于梯度下降法,模拟退火在每轮迭代中不需要计算所有邻域解,而梯度下降计算数量为参数个数*2,所以模拟退火的迭代速度高于梯度下降。
    为了使两次模拟退火之间可以区分,我在两次模拟退火的能量函数之间插入了一个峰值。观察蓝色曲线可以发现,每次运行模拟退火时,前期能量函数波动较大(因为跳到更差的解的几率很大),在后期逐渐趋于稳定,类似梯度下降的结果。

    5.3 参数调整三种算法中,只有模拟退火算法有需要调整的参数。包括迭代次数k、初始温度temp与温度衰减系数r。
    k控制何时停止迭代,根据曲线取值即可。
    起初不知道如何设置初始温度与衰减系数,将温度设置得很高,衰减也很快,然后发现优化效果不如梯度下降法。分析各个参数的作用与模拟退火算法的优势之后,我得到结论:

    初始温度应与目标函数值在同一量级;
    衰减系数应调整到使得温度在进行了一半的迭代轮数之后下降到足够低,进而使得优化只向更优的可行解跳跃。

    最终的曲线和期望一致,一开始有较大的波动,随后逐渐稳定直至收敛。
    5.4 分析与结论本次实验我得到了如下结论:

    经过对比试验,对于二维照片配准问题,使用2范数作为相似性度量函数的效果较好,其次是1范数。
    参数空间的邻域结构非常重要。例如仿射变换矩阵尺寸为2*3,有六个参数,若直接优化这六个参数,则优化结果较差,很难收敛到最优解。原因可能是这六个参数的现实意义不够明确(例如,增大第一个参数会使图像怎样变化),且参数之间存在较大关联性(例如,同时增大第一个、第五个参数,作用是放大图像,而只改变一个则图像会变得不自然)。仿射变换的另一种表示方式是,原图像的左上、右上、左下三个顶点分别变换到了新图像的哪些位置,同样需要六个参数描述,但这些参数的可解释性更强,目标函数也更容易优化至最优。实验发现,选择合适的参数能够让目标函数的性质更好,更容易收敛到全局最优解。
    模拟退火算法在图像配准问题上的优势不明显。对于特别不光滑的目标函数,梯度下降算法是无能无力的,因为选择一个随机初始值下降至全局最优值的概率非常低。而模拟退火算法有助于跳出这些波动,相比梯度下降法更容易得到最优解。但是对于图像配准问题,其目标函数性质较好,局部最优解较少。于是,多次使用梯度下降算法也能够找到全局最优解,模拟退火算法的优势难以体现。
    0  留言 2019-06-16 23:22:40
  • 数据压缩WIN32 API

    背景对于Windows上的数据压缩和解压缩的实现,最方便的就是直接调用Win32 API函数。Widnows系统的ntdll.dll专门提供了RtlCompressBuffer函数和RtlDecompressBuffer函数来负责对数据压缩和解压缩操作,这两个函数并未公开,需要通过在ntdll.dll中动态调用。
    正是由于这两个函数简单易用,所以常被病毒木马用来对数据进行压缩和解压缩。接下来,本文将介绍利用RtlCompressBuffer函数和RtlDecompressBuffer函数实现数据压缩和解压缩操作。
    函数介绍RtlGetCompressionWorkSpaceSize函数
    用于确定RtlCompressBuffer和RtlDecompressFragment函数的WorkSpace缓冲区的正确大小。
    NTSTATUS RtlGetCompressionWorkSpaceSize( _In_ USHORT CompressionFormatAndEngine, _Out_ PULONG CompressBufferWorkSpaceSize, _Out_ PULONG CompressFragmentWorkSpaceSize);
    参数
    CompressionFormatAndEngine[in]位掩码指定压缩格式和引擎类型。 该参数必须设置为以下按位或组合之一:COMPRESSION_FORMAT_LZNT1 |COMPRESSION_ENGINE_STANDARDCOMPRESSION_FORMAT_LZNT1 |COMPRESSION_ENGINE_MAXIMUM
    CompressBufferWorkSpaceSize[out]指向调用方分配缓冲区的指针,用于接收压缩缓冲区所需的大小(以字节为单位)。此值用于确定RtlCompressBuffer的WorkSpace缓冲区的正确大小。
    CompressFragmentWorkSpaceSize[out]一个指向调用者分配缓冲区的指针,用于接收将压缩缓冲区解压缩为片段所需的大小(以字节为单位)。此值用于确定RtlDecompressFragment的WorkSpace缓冲区的正确大小。请注意,RtlCompressFragment函数当前不存在。
    返回值
    返回STATUS_SUCCESS,则表示成功;否则,失败。

    RtlCompressBuffer函数
    压缩一个缓冲区,并且可以被文件系统驱动程序使用,以促进文件压缩的实现。
    NTSTATUS RtlCompressBuffer( _In_ USHORT CompressionFormatAndEngine, _In_ PUCHAR UncompressedBuffer, _In_ ULONG UncompressedBufferSize, _Out_ PUCHAR CompressedBuffer, _In_ ULONG CompressedBufferSize, _In_ ULONG UncompressedChunkSize, _Out_ PULONG FinalCompressedSize, _In_ PVOID WorkSpace);
    参数
    CompressionFormatAndEngine[in]指定压缩格式和引擎类型的位掩码。此参数必须设置为一种格式类型和一种引擎类型的有效按位或组合,相关值的含义如下。



    VALUE
    MEANING




    COMPRESSION_FORMAT_LZNT1
    LZ压缩算法


    COMPRESSION_FORMAT_XPRESS
    Xpress压缩算法


    COMPRESSION_FORMAT_XPRESS_HUFF
    Huffman压缩算法


    COMPRESSION_ENGINE_STANDARD
    标准压缩算法


    COMPRESSION_ENGINE_MAXIMUM
    最大程度压缩



    UncompressedBuffer[in]指向要压缩的数据缓冲区的指针。该参数是必需的,不能为NULL。UncompressedBufferSize[in]UncompressedBuffer缓冲区的大小(以字节为单位)。CompressedBuffer[out]指向压缩之后的数据缓存的缓冲区的指针,用于接收压缩数据。该参数是必需的,不能为NULL。CompressedBufferSize[in]CompressedBuffer缓冲区的大小(以字节为单位)。UncompressedChunkSize[in]压缩UncompressedBuffer缓冲区时使用的块大小。该参数必须是以下值之一:512、1024、2048或4096。操作系统使用4096,此参数的推荐值也是4096。FinalCompressedSize[out]指向调用方分配变量的指针,该变量接收存储在CompressedBuffer中的压缩数据的大小(以字节为单位)。该参数是必需的,不能为NULL。WorkSpace[in]指向压缩期间RtlCompressBuffer函数使用的调用方分配的工作空间缓冲区的指针。使用RtlGetCompressionWorkSpaceSize函数来确定正确的工作空间缓冲区大小。
    返回值
    返回STATUS_SUCCESS,则表示成功;否则,失败。

    RtlDecompressBuffer函数
    解压缩整个压缩缓冲区。
    NTSTATUS RtlDecompressBuffer( _In_ USHORT CompressionFormat, _Out_ PUCHAR UncompressedBuffer, _In_ ULONG UncompressedBufferSize, _In_ PUCHAR CompressedBuffer, _In_ ULONG CompressedBufferSize, _Out_ PULONG FinalUncompressedSize);
    CompressionFormat[in]指定压缩缓冲区压缩格式的位掩码。该参数必须设置为COMPRESSION_FORMAT_LZNT1。这个和其他相关压缩格式值的含义如下。



    VALUE
    MEANING




    COMPRESSION_FORMAT_LZNT1
    LZ解压缩算法


    COMPRESSION_FORMAT_XPRESS
    Xpress解压缩算法



    UncompressedBuffer[out]指向存储解压缩数据的缓冲区的指针,该缓冲区从CompressedBuffer接收解压缩的数据。该参数是必需的,不能为NULL。UncompressedBufferSize[in]UncompressedBuffer缓冲区的大小(以字节为单位)。CompressedBuffer[in]指向包含要解压缩的数据的缓冲区的指针。该参数是必需的,不能为NULL。CompressedBufferSize[in]CompressedBuffer缓冲区的大小(以字节为单位)。FinalUncompressedSize[out]指向解压之后得到的数据大小的指针,该变量接收UncompressedBuffer中存储的解压缩数据的大小(以字节为单位)。该参数是必需的,不能为NULL。
    返回值
    返回STATUS_SUCCESS,则表示成功;否则,失败。

    实现过程数据压缩主要是通过调用RtlCompressBuffer函数来实现的,那么,具体的数据压缩实现流程如下所示。
    首先,先调用LoadLibrary函数加载ntdll.dll,并获取ntdll.dll加载模块的句柄。再调用GetProcAddress函数来获取RtlGetCompressionWorkSpaceSize函数以及RtlCompressBuffer函数。
    然后,直接调用RtlGetCompressionWorkSpaceSize函数来获取RtlCompressBuffer函数工作空间缓冲区的大小。其中,压缩格式和引擎类型设置为COMPRESSION_FORMAT_LZNT1和COMPRESSION_ENGINE_STANDARD。然后,根据工作空间缓冲区大小申请一个工作空间缓冲区给压缩数据使用。
    最后,调用RtlCompressBuffer函数来压缩数据。数据压缩缓冲区的大小为4096字节,在成功压缩数据之后,便获取实际的压缩数据大小。此时需要将实际压缩数据大小和数据压缩缓冲区大小进行比较,如果数据压缩缓冲区太小,则需要释放原来的缓冲区,重新按照实际压缩数据的大小来申请一个新的数据压缩缓冲区,并且重新压缩数据。这样,才能获取所有的压缩数据。
    那么,使用RtlCompressBuffer函数压缩数据的具体实现代码如下所示。
    // 数据压缩BOOL CompressData(BYTE *pUncompressData, DWORD dwUncompressDataLength, BYTE **ppCompressData, DWORD *pdwCompressDataLength){ BOOL bRet = FALSE; NTSTATUS status = 0; HMODULE hModule = NULL; typedef_RtlGetCompressionWorkSpaceSize RtlGetCompressionWorkSpaceSize = NULL; typedef_RtlCompressBuffer RtlCompressBuffer = NULL; DWORD dwWorkSpaceSize = 0, dwFragmentWorkSpaceSize = 0; BYTE *pWorkSpace = NULL; BYTE *pCompressData = NULL; DWORD dwCompressDataLength = 4096; DWORD dwFinalCompressSize = 0; do { // 加载 ntdll.dll hModule = ::LoadLibrary("ntdll.dll"); if (NULL == hModule) { ShowError("LoadLibrary"); break; } // 获取 RtlGetCompressionWorkSpaceSize 函数地址 RtlGetCompressionWorkSpaceSize = (typedef_RtlGetCompressionWorkSpaceSize)::GetProcAddress(hModule, "RtlGetCompressionWorkSpaceSize"); if (NULL == RtlGetCompressionWorkSpaceSize) { ShowError("GetProcAddress"); break; } // 获取 RtlCompressBuffer 函数地址 RtlCompressBuffer = (typedef_RtlCompressBuffer)::GetProcAddress(hModule, "RtlCompressBuffer"); if (NULL == RtlCompressBuffer) { ShowError("GetProcAddress"); break; } // 获取WorkSpqce大小 status = RtlGetCompressionWorkSpaceSize(COMPRESSION_FORMAT_LZNT1 | COMPRESSION_ENGINE_STANDARD, &dwWorkSpaceSize, &dwFragmentWorkSpaceSize); if (0 != status) { ShowError("RtlGetCompressionWorkSpaceSize"); break; } // 申请动态内存 pWorkSpace = new BYTE[dwWorkSpaceSize]; if (NULL == pWorkSpace) { ShowError("new"); break; } ::RtlZeroMemory(pWorkSpace, dwWorkSpaceSize); while (TRUE) { // 申请动态内存 pCompressData = new BYTE[dwCompressDataLength]; if (NULL == pCompressData) { ShowError("new"); break; } ::RtlZeroMemory(pCompressData, dwCompressDataLength); // 调用RtlCompressBuffer压缩数据 RtlCompressBuffer(COMPRESSION_FORMAT_LZNT1, pUncompressData, dwUncompressDataLength, pCompressData, dwCompressDataLength, 4096, &dwFinalCompressSize, (PVOID)pWorkSpace); if (dwCompressDataLength < dwFinalCompressSize) { // 释放内存 if (pCompressData) { delete[]pCompressData; pCompressData = NULL; } dwCompressDataLength = dwFinalCompressSize; } else { break; } } // 返回 *ppCompressData = pCompressData; *pdwCompressDataLength = dwFinalCompressSize; bRet = TRUE; } while(FALSE); // 释放 if (pWorkSpace) { delete[]pWorkSpace; pWorkSpace = NULL; } if (hModule) { ::FreeLibrary(hModule); } return bRet;}
    数据解压缩主要是通过调用RtlDecompressBuffer函数来实现的,相比于数据压缩,数据解压缩实现起来更为简单。那么,具体的数据解压缩实现流程如下所示。
    首先,同样是先调用LoadLibrary函数加载ntdll.dll,并获取ntdll.dll加载模块的句柄。再调用GetProcAddress函数RtlDecompressBuffer函数。不需要获取RtlGetCompressionWorkSpaceSize函数的地址,因为数据解压缩操作不需要确定压缩工作空间缓冲区大小
    然后,开始调用RtlDecompressBuffer函数来解压缩数据。其中,压缩格式和引擎类型必须设置为COMPRESSION_FORMAT_LZNT1。数据解压缩缓冲区的初始大小为4096字节,在成功解压数据之后,便获取实际的解压数据大小。此时需要将实际解压数据大小和数据解压缓冲区大小进行比较,如果数据解压缓冲区太小,则需要释放原来的缓冲区,重新按照实际解压数据的大小来申请一个新的数据解压缓冲区,并且重新解压缩数据。这样,才能获取所有的解压数据。
    那么,使用RtlDecompressBuffer函数压缩数据的具体实现代码如下所示。
    // 数据解压缩BOOL UncompressData(BYTE *pCompressData, DWORD dwCompressDataLength, BYTE **ppUncompressData, DWORD *pdwUncompressDataLength){ BOOL bRet = FALSE; HMODULE hModule = NULL; typedef_RtlDecompressBuffer RtlDecompressBuffer = NULL; BYTE *pUncompressData = NULL; DWORD dwUncompressDataLength = 4096; DWORD dwFinalUncompressSize = 0; do { // 加载 ntdll.dll hModule = ::LoadLibrary("ntdll.dll"); if (NULL == hModule) { ShowError("LoadLibrary"); break; } // 获取 RtlDecompressBuffer 函数地址 RtlDecompressBuffer = (typedef_RtlDecompressBuffer)::GetProcAddress(hModule, "RtlDecompressBuffer"); if (NULL == RtlDecompressBuffer) { ShowError("GetProcAddress"); break; } while (TRUE) { // 申请动态内存 pUncompressData = new BYTE[dwUncompressDataLength]; if (NULL == pUncompressData) { ShowError("new"); break; } ::RtlZeroMemory(pUncompressData, dwUncompressDataLength); // 调用RtlCompressBuffer压缩数据 RtlDecompressBuffer(COMPRESSION_FORMAT_LZNT1, pUncompressData, dwUncompressDataLength, pCompressData, dwCompressDataLength, &dwFinalUncompressSize); if (dwUncompressDataLength < dwFinalUncompressSize) { // 释放内存 if (pUncompressData) { delete[]pUncompressData; pUncompressData = NULL; } dwUncompressDataLength = dwFinalUncompressSize; } else { break; } } // 返回 *ppUncompressData = pUncompressData; *pdwUncompressDataLength = dwFinalUncompressSize; bRet = TRUE; } while (FALSE); // 释放 if (hModule) { ::FreeLibrary(hModule); } return bRet;}
    测试直接运行上述程序,对“DDDDDDDDDDGGGGGGGGGGGG”字符串进行压缩得到压缩数据,再对压缩数据进行解压缩得到解压数据。得到的压缩数据和解压数据如下图8-1所示。

    小结不能直接调用数据压缩函数RtlCompressBuffer和数据解压缩函数RtlDecompressBuffer,而是需要提前从ntdll.dll中通过GetProcAddress函数来动态获取。
    在调用RtlCompressBuffer函数进行压缩数据之前,一定要先调用RtlGetCompressionWorkSpaceSize函数来获取压缩数据的工作空间大小,以此来申请工作空间缓冲区。压缩数据的时候,可以先设置一个初始的压缩数据缓冲区来存储压缩数据;在压缩完毕之后,一定要判断该初始缓冲区的大小是否满足实际的压缩数据,若不满足,则重新申请缓冲区,重新压缩数据。
    在调用RtlDecompressBuffer函数解压缩数据的时候,压缩格式,必须为COMPRESSION_FORMAT_LZNT1;而且先设置一个初始的解压数据缓冲区来存储解压数据,在解压完毕后,再判断该初始缓冲区大小是否满足实际的解压数据,若不满足,则重新申请缓冲区,重新解压缩。
    参考参考自《Windows黑客编程技术详解》一书
    1  留言 2019-01-16 10:47:41
  • 使用VS2013实现创建快捷方式

    背景我们经常会遇到,程序安装完成后,会在桌面生成快捷方式图标。或者,当我们想在桌面就可以打开某个程序时,只要给程序创建相应的快捷方式图标发送到桌面就好。所以,我们对创建快捷方式这个功能并不陌生,反而还很熟悉。
    那么,这篇文档就讲解下使用VS2013去实现为指定程序创建快捷方式的功能。主要是使用Shell拓展编程的方式进行开发,把开发思路和实现过程写成文档的形式,分享给大家。
    函数介绍CoInitialize 函数
    用来告诉 Windows以单线程的方式创建com对象。应用程序调用com库函数(除CoGetMalloc和内存分配函数)之前必须初始化com库。
    函数声明
    HRESULT CoInitialize( _In_opt_ LPVOID pvReserved);
    参数

    参数被保留,且必须为NULL。
    返回值

    S_OK : 该线程中COM库初始化成功S_FALSE: 该线程中COM库已经被初始化 CoInitialize () 标明以单线程方式创建。
    注意

    CoInitialize和CoUninitialize必须成对使用。

    CoCreateInstance 函数
    用指定的类标识符创建一个Com对象,用指定的类标识符创建一个未初始化的对象。
    函数声明
    STDAPI CoCreateInstance( REFCLSID rclsid, //创建的Com对象的类标识符(CLSID) LPUNKNOWN pUnkOuter, //指向接口IUnknown的指针 DWORD dwClsContext, //运行可执行代码的上下文 REFIID riid, //创建的Com对象的接口标识符 LPVOID * ppv //用来接收指向Com对象接口地址的指针变量);
    参数

    rclsid[in] 用来唯一标识一个对象的CLSID(128位),需要用它来创建指定对象。pUnkOuter[in] 如果为NULL, 表明此对象不是聚合式对象一部分。如果不是NULL, 则指针指向一个聚合式对象的IUnknown接口。dwClsContext[in] 组件类别. 可使用CLSCTX枚举器中预定义的值.riid[in] 引用接口标识符,用来与对象通信。ppv[out] 用来接收指向接口地址的指针变量。如果函数调用成功,*ppv包括请求的接口指针。
    返回值

    S_OK指定的Com对象实例被成功创建。REGDB_E_CLASSNOTREG指定的类没有在注册表中注册. 也可能是指定的dwClsContext没有注册或注册表中的服务器类型损坏。CLASS_E_NOAGGREGATION这个类不能创建为聚合型。E_NOINTERFACE指定的类没有实现请求的接口, 或者是IUnknown接口没有暴露请求的接口。

    IShellLink类主要成员

    GetArguments:获得参数信息GetDescription:获得描述信息(备注行)GetHotkey:获得快捷键GetIconLocation:获得图标 GetIDList:获得快捷方式的目标对象的item identifier list (Windows外壳中的每个对象如文件,目录和打印机等都有唯一的item identifiler list)GetPath: 获得快捷方式的目标文件或目录的全路径GetShowCmd:获得快捷方式的运行方式,比如常规窗口,最大化GetWorkingDirectory:获得工作目录Resolve:按照一定的搜索规则试图获得目标对象,即使目标对象已经被删除或移动,重命名
    下面是对应信息的设置方法
    SetArguments
    SetDescriptionSetHotkeySetIconLocationSetIDListSetPathSetRelativePatSetShowCmdSetWorkingDirectory

    IPersistFile类主要成员

    Save:保存内容到文件中去Load:读取
    Load的函数原型

    HRESULT Load( LPCOLSTR pszFileName, //快捷方式的文件名,应该是ANSI字符 DWORD dwMode //读取方式 );
    参数

    dwMode可取如下值:
    STGM_READ:只读
    STGM_WRITE:只写
    STGM_READWRITE:读写


    实现原理本文介绍的创建快捷功能的实现,是使用Windows Shell编程,会涉及到COM组件调用的内容。如果你没有接触过COM,那也没有关系。你只需要了解清楚程序的逻辑就好,然后,知道每个操作大概是干什么就可以了。
    那么,创建快捷方式的过程原理就是:

    首先,先初始化COM接口环境,因为接下来会调用到COM组件。
    然后,创建一个IShellLink对象。
    接着,设置IShellLink对象的目标程序路径以及目标程序所在的目录。
    从IShellLink对象中获取IPersistFile接口,用于保存快捷方式的数据文件(*.lnk)。
    将保存的快捷方式路径窄字符串转换为宽字符串,然后调用IPersistFile接口保存生成快捷方式文件。
    释放对象接口、释放对象以及释放COM接口环境。

    这样,创建快捷方式的流程就走完了,那么,指定程序的快捷方式.lnk文件也已经生成。
    编程实现BOOL CreateLink(char *lpszExeFileName, char *lpszLnkFileName){ HRESULT hRet = 0; IShellLink *lpShellLink = NULL; IPersistFile *lpPersistFile = NULL; char szDirectory[MAX_PATH] = { 0 }; WCHAR szwLink[MAX_PATH] = { 0 }; // 初始化COM库 ::CoInitialize(NULL); // 创建一个IShellLink实例 hRet = ::CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID *)&lpShellLink); if (FAILED(hRet)) { ShowError("CoCreateInstance"); return FALSE; } // 设置目标应用程序 lpShellLink->SetPath(lpszExeFileName); // 目标文件的工作目录 ::lstrcpy(szDirectory, lpszExeFileName); char *p = ::strrchr(szDirectory, '\\'); p[0] = '\0'; lpShellLink->SetWorkingDirectory(szDirectory); // 从ISHellLink获取IPersistFile接口,用于保存快捷方式的数据文件(*.lnk) hRet = lpShellLink->QueryInterface(IID_IPersistFile, (LPVOID *)&lpPersistFile); if (FAILED(hRet)) { ShowError("lpShellLink->QueryInterface"); return FALSE; } // 保存快捷方式的数据文件(*.lnk) C2W(lpszLnkFileName, szwLink, 2*MAX_PATH); hRet = lpPersistFile->Save(szwLink, STGM_READWRITE); if (FAILED(hRet)) { ShowError("lpPersistFile->Save"); return FALSE; } // 释放接口 lpPersistFile->Release(); lpShellLink->Release(); ::CoUninitialize(); return TRUE;}
    程序测试在 main 函数中上述封装好的函数,进行测试。
    main 函数为:
    int _tmain(int argc, _TCHAR* argv[]){ if (FALSE == CreateLink("C:\\Users\\DemonGan\\Desktop\\520.exe", "C:\\Users\\DemonGan\\Desktop\\520.lnk")) { printf("Create Link Error!\n"); } printf("Create Link OK!\n"); system("pause"); return 0;}
    测试结果:
    运行程序,提示执行成功。

    查看目录,“520.lnk”快捷方式文件生成。

    查看“520.lnk”文件的属性。

    并双击快捷方式,成功运行程序。所以,测试成功。
    总结这个程序,使用到COM组件相关的知识,理解起来有些复杂。但是,具体原理可以不进行深究,只需熟练使用提供的函数接口就可以了。
    参考参考自《Windows黑客编程技术详解》一书
    1  留言 2018-11-12 12:33:37
eject