集成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 ¶ms) { 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串,实际上现在的实现都已经很令人不爽了,可以采取哪些手段我们后面有机会再讨论。