我们先重构低倍扫描,为它生成金字塔图层数据。然后同样改造高倍图。

低倍扫描重构思路

接下来我们要分析一下如何生成金字塔图层。我们先看高倍扫描的实现,在类HighImageJointer中,我们已经留出了实现的地方。我们看下面的makeTile()函数:

1
2
3
4
5
6
7
8
9
void HighImageJointer::makeTile(int col, int row, cv::Mat image, bool is_last_col, bool is_last_row, bool make_pyrmid)
{
Q_UNUSED(make_pyrmid) // 暂时不实现金字塔
Q_UNUSED(is_last_row)
Q_UNUSED(is_last_col)
//TRACE() << "构造tile: " << TSHOW(col) << TSHOW(row);
dumpSlice(col, row, image);
emit sigDumpSlice(col, row, image);
}

在之前的实现中,它仅仅是调用了dumpSlice(),将层0的图像保存到slice目录里面。我们可以在这里添加业务逻辑,将金字塔生成逻辑放在这里。

我们使用Reduce的做法,逐层生成金字塔图层。简单地说,比如,我们层0有64x32个视野,即在层0中,有$32\times32=1024$个视野,即现有运行了1024个makeTile()的工作线程。每4个线程会拼凑成层1的一个视野,例如,线程{0,0},{0,1},{1,0},{1,1}四个线程将图像缩小一半后,又凑成一个视野,层1的{0,0}视野。这四个线程中,有三个会终止,剩下一个继续运行,继续拼凑他图层2的{0,0}视野。
$$
32\times32\
16\times16\
8\times8\
4\times4\
2\times2\
1\times1
$$
如此,总共生成了6$log_2(32)+1=6$​个图层。

我们对比一下ScanTaskdoLowScan()doHighScan(),可以看到它们的差别:

低倍扫描的处理部分:

1
2
3
4
5
6
7
8
9
10
11
// 低倍扫描
...
while(index<picList.size())
{
...
// 发送给界面
emit sigImageCaptured({column, row}, imgMat);
emit sigDumpImage(imgMat, column, row, _impl->_lowImageFormat,
_impl->_lowImageFormat, dumpPath);
...
}

高倍连续扫描的图像处理部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
...
while(index<picList.size() && index<total)
{
...
jointer->add({column, row}, imgMat.clone());
emit sigImageCaptured({column, row}, imgMat);
emit sigDumpImage(imgMat, column, row, _impl->_highImageFormat,
_impl->_highImageQuality, viewDumpPath);
...
}
if(method==EScanMethod::eMatrixScan)
{
TRACE() << "保存连续扫描的区域尺寸";
...
emit sigPackViewInfo(EViewType::eHighView, cell_type, round, 0, _impl->_imageSize, rects.at(0), method, path );
emit sigZipDir(sliceDumpPath, basePath());
}
else
{
...
}

实现LowImageJointer

在开始工作之前,我们再回顾一下HighImageJointer的实现,我们发现它和我们将要实现的低倍图的拼接器十分相似,唯一的差别在于匹配的计算方式不同——对于高倍图,我们需要对相邻的图像做匹配处理,而对低倍图,我们根本不需要计算,简单地让他们相邻就可以了,也就是说,视野和视野之间的偏移量是固定的,可知的。

这样,我们就有两个方案备选:

一个方案是将LowImageJointerHighImageJointer做共性提取,要么再提取出一个基类,并各自实现自己的doMatch()函数,其他的使用完全相同的处理逻辑。或者我们做得再“现代C++”一点,把doMathch()实现为一个模板参数,或者,稍微传统一点,使用模板方法模式。都是可以的。

我个人十分厌恶滥用template,尤其是将函数作为模板的时候。虽然看上去很酷,但是一旦不小心,编译器报错太痛苦了。所以我总是克制自己的这种欲望,在本书中基本上不会使用这种方法,只会使用传统的面向对象的设计思想。

另一个方案是针对低倍图这种情况做专门处理,因为我们完全不需要matchView(),自然也不需要splitViewProc(),实在是没有必要启动一堆的工作线程再立即返回。我们只要从makeSliceProc()这个地方出发就可以了。

第一个方案相对来说,虽然看上去修改更少,但是对于不匹配的情况,还是显得过于笨重了,而且,我们所有的一切工作都是为了匹配连接所作的优化,突然要它还能支持不需要匹配的拼接,总是让人不够放心,担心哪里出一点纰漏,导致破坏了整个类的结构。所以,我们最终决定还是为这种情况单独实现一个版本的好。

不过,在此之前,我们先调整一下这几个类的名字:

  • HighImageJointer改名为ImageMatchJointer,表示需要计算图像匹配的拼接器。
  • DummyImageJointer改名为ImageDummyJointer,表示不拼接
  • 新增ImageDirectJointer,代替我们原先想创建的LowImageJointer,表示不需要图像匹配的简单拼接

这样的几个类名称修改,一方面它们的含义更清晰,另一方面不管是在文件系统还是在QtCreater的项目中,都是排列在一起的,我们找起来更方便一些。