集成ImageJointer

完成HighImagerJointer单元测试后,我们要将其集成到ScanTask中。

首先创建一个工厂函数,它根据样本类型来决定创建的IImageJointer子类的类别:

1
2
3
4
5
6
7
8
9
10
11
std::unique_ptr<IImageJointer> AlgorithmFactory::makeImageJointer(int scan_method)
{
if(scan_method == EScanMethod::eSingleScan)
{
return std::make_unique<DummyImageJointer>();
}
else
{
return std::make_unique<HighImageJointer>();
}
}

ScanTask::doHighScan()中增加Jointer的创建和启动,并在得到每张图片后将其发送给Jointer处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
bool ScanTask::doHighScan(int cell_type)
{
...
for(int round=0; round<regions.size(); ++round)
{
int total = scanInfo.column * scanInfo.row;
int index = 0;
auto jointer = AlgorithmFactory::makeImageJointer(method);
QDir().mkpath(sliceDumpPath(round));
jointer->start(scanInfo.column, scanInfo.row, QRect(), slicePath, false, false);
...
while(true)
{
...
else
{
_impl->_device->GetRealtimePics(&picList);
while(index<picList.size() && index<total)
{
...
auto imgMat = cvtPicToMat(pic);
jointer->add({column, row}, imgMat.clone());
...`
}
}
}
jointer->waitForFinished();
if(method==EScanMethod::eMatrixScan)
{
QSize size{jointer->sliceColumns()*jointer->sliceWidth(), jointer->sliceRows()*jointer->sliceHeight()};
auto path = viewZipDirPath(EViewType::eHighView, cell_type, round);
emit sigPackViewInfo(EViewType::eHighView, cell_type, round, 0, _impl->_imageSize, rects.at(0), method, path );
emit sigZipDir(sliceDumpPath, basePath());
}
else
{
emit sigZipDir(viewDumpPath, basePath());
}
...
}

然后我们添加测试用例进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void ScanTaskTester::test_scan()
{
...
#if 1 // 非连续扫描
const int HIGH_COLUMNS = 12;
const int HIGH_ROWS = 1;
task.setScanMethod(ECellType::eWhiteCell, EScanMethod::eSingleScan);
#else // 连续扫描
const int HIGH_COLUMNS = 5;
const int HIGH_ROWS = 5;
task.setScanMethod(ECellType::eWhiteCell, EScanMethod::eMatrixScan);
#endif
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
A crash occurred in D:\CodeLib\QtInPractice\source\ScanerSuit\build\Desktop_Qt_6_7_0_MSVC2019_64bit-Debug\UnitTest\UnitTest_Frameworks_ScanTask\debug\UnitTest_Frameworks_ScanTask.exe.
While testing test_doHighScan_Matrix
Function time: 1603ms Total time: 1605ms

Exception address: 0x00007FFA268D5F6F
Exception code : 0xc0000005
Nearby symbol : cv::MatExpr::type

Stack:
# 1: QTestLog::enterTestFunction() - 0x00007FFAA35B5CE0
# 2: UnhandledExceptionFilter() - 0x00007FFABEECDB40
# 3: memset() - 0x00007FFAC196AA80
# 4: _C_specific_handler() - 0x00007FFAC1953C30
# 5: _chkstk() - 0x00007FFAC1968BB0
# 6: RtlRestoreContext() - 0x00007FFAC18F5390
# 7: KiUserExceptionDispatcher() - 0x00007FFAC1967CA0
# 8: cv::MatExpr::type() - 0x00007FFA24967408
# 9: cv::MatExpr::type() - 0x00007FFA24967408
# 10: cv::MatExpr::type() - 0x00007FFA24967408
# 11: cv::MatExpr::type() - 0x00007FFA24967408
# 12: cv::MatExpr::type() - 0x00007FFA24967408
# 13: HighImageJointer::doMatch() - 0x00007FFAA2CFAEF0
# 14: HighImageJointer::matchView() - 0x00007FFAA2CFA750
# 15: HighImageJointer::add() - 0x00007FFAA2CFA220
# 16: ScanTask::doHighScan() - 0x00007FFAA2D36880
# 17: ScanTaskTester::test_doHighScan_Matrix() - 0x00007FF67813B320
# 18: ScanTaskTester::qt_static_metacall() - 0x00007FF6781352D0
# 19: QFileInfo::filePath() - 0x00007FFA2E0977F9
# 20: QFileInfo::filePath() - 0x00007FFA2E0977F9
# 21: QTestLog::enterTestFunction() - 0x00007FFAA35B5CE0
# 22: QTestLog::enterTestFunction() - 0x00007FFAA35B5CE0
# 23: QTestLog::enterTestFunction() - 0x00007FFAA35B5CE0
# 24: QTestLog::enterTestFunction() - 0x00007FFAA35B5CE0
# 25: QTestLog::enterTestFunction() - 0x00007FFAA35B5CE0
# 26: QTestLog::enterTestFunction() - 0x00007FFAA35B5CE0
# 27: QTestLog::enterTestFunction() - 0x00007FFAA35B5CE0
# 28: QTestLog::enterTestFunction() - 0x00007FFAA35B5CE0
# 29: main() - 0x00007FF67813EB80
# 30: invoke_main() - 0x00007FF67815C4C0
# 31: __scrt_common_main_seh() - 0x00007FF67815C2B0
# 32: __scrt_common_main() - 0x00007FF67815C290
# 33: mainCRTStartup() - 0x00007FF67815C580
# 34: BaseThreadInitThunk() - 0x00007FFAC0B753D0
# 35: RtlUserThreadStart() - 0x00007FFAC18C4830

这个错误很奇怪,因为我们在单元测试中完全没有出现问题。我们只能分析doHighScan()test_doHighScan_Matrix()两个函数的区别。最后发现了问题在这里:

1
2
3
4
5
6
cv::Mat cvtPicToMat(const PicInfor &pic)
{
auto channel = pic.pictureType==PictureType::DCM_PICTURETYPE_COLOR ? 3:1;
cv::Mat dst(pic.height, pic.width, CV_8UC(channel), pic.matPic);
return dst;
}

我们生成的Mat对象直接使用了PicInfor结构中的图像数据,而紧接着我们就把PicInfor结构中的内存给删除了。这个问题在doLowScan()中其实也存在,只是因为LowAnalyzer类并没有真的使用Mat数据,所以我们没有注意到。那么这个问题的修改也就很简单了,我们先这么修改一下:

1
auto imgMat = cvtPicToMat(pic).clone();

这样,得到的Mat就和从设备那边拿来的内存块解开了。现在我们再测试,就不会崩溃了。

对低倍扫描这边就安全了,但是对高倍这边还是有风险的。我们看到,即使这样做了,ImageJointer还是和PackingWorker共用imgMat的数据,但是它在释放的时候可没有考虑这个问题。其实,如果我们不是为了自己释放PicInfor的内存,这个问题其实不会出现;如果不是为了在ImageJointer里面及早释放内存,而是让Mat自己管理自己的生命周期,也不会有这个问题,如果我们不是又想保留ViewElement数据,又想释放它的Mat数据,也不会有这个问题。但是,怎么说呢,你想有所得就要有所失吧。多线程下最好是使用自己的数据,最好不要修改,我们再修改一下add的调用,让ImageJointer使用自己的一份数据副本,自己去释放它。而原先那个副本就让ScanTask自己去使用和自动释放。

1
jointer->add({column, row}, imgMat.clone());

收尾工作

最后,扫描完成后我们要做收尾工作。包括扫描仪退片,通知主控不再使用显微镜,关闭数据库,将数据库文件压入zip文件中,检查文件完整性等。 为了便于观察,我们添加了一行sleep操作,来模拟后面的耗时工作。

在这里我们会依次发出几个信号:

  • 先执行退片
  • 退片完成后,发送信号sigDeviceFree()给控制端,通知控制端设备已经空闲,可以重新启动扫描了
  • 同时发送信号sigCompletePacking()PackingWorker做后面的打包收尾工作(我们目前为了便于测试,是一边扫描一边压缩的,为了提高设备利用率,以后应该是放到最后才压缩文件)。
  • 最后,给控制端发送sigTaskComplete(),表示这个扫描任务全部结束了。
1
2
3
4
5
6
7
8
9
bool ScanTask::complete()
{
_impl->_device->ReturnSlide();
emit sigDeviceFree();
TRACE() << "扫描仪使用完毕. 执行后台任务...";
emit sigCompletePacking();
emit sigTaskCompleted();
return true;
}

汇总实现doScan()

最后,我们将前面实现的步骤合并到外部接口doScan()中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bool ScanTask::doScan(const QVariantHash &params)
{
RETVAL_LOG_IF(!doPrepare(params), false, "准备失败!");

RETVAL_LOG_IF(!doPreview(), false, "预览图拍摄失败!");

RETVAL_LOG_IF(!doLowScan(), false, "低倍扫描失败!");

if(cellCount(ECellType::eWhiteCell)>0)
{
RETVAL_LOG_IF(!doHighScan(ECellType::eWhiteCell), false, "白细胞扫描失败");
}
if(cellCount(ECellType::eRedCell)>0)
{
RETVAL_LOG_IF(!doHighScan(ECellType::eRedCell), false, "红细胞扫描失败");
}
if(cellCount(ECellType::eMegaCell)>0)
{
RETVAL_LOG_IF(!doHighScan(ECellType::eMegaCell), false, "巨核细胞扫描失败");
}
return complete();
}

这个实现是一个急就的实现,还有很多问题没有解决。比如,如果某个阶段失败了就直接退出了,完全没有做任何清理工作。比如,扫描完成后要有退片工作,将载物台移出镜下,以便更换玻片做下一次扫描。这个工作是否是属于ScanTask的职责?如果不是,又应该赋予谁?

再比如,我们在扫描时打开了zip文件和db数据库,正常情况下时在complete()中关闭,如果直接退出了,怎么办?

当然,我们可以写一个函数,在每次提前退出时调用做清理工作,这就变成了一串的臃肿的if串,实际上现在的实现都已经很令人不爽了,可以采取哪些手段我们后面有机会再讨论。