Day 12 实现简单界面
实现ControlPanelFormControlPanelForm用于显示界面左侧的信息栏,它有两部分组成:上面主要是一个QLabel,用于显示样本的预览图,下方是一个QTableWidget,用于显示扫描进度。是一个很粗糙的界面设计,但是用于演示也够了。
它包含四个slot函数,我们会依次简单看一下:
显示预览图信息函数onSigPreviewCaptured()用于在收到sigPreviewed后显示样本信息。它将收到的cv::Mat转换成QPixmap,并按照这个QLabel的实际大小做缩放,并设置到QLabel上面。
12345678910void ControlPanelForm::onSigPreviewCaptured(cv::Mat prev_mat, cv::Mat label_mat, const QString &qrcode){ _impl->_prevMat = prev_mat.clone(); _impl->_qrcode = qrcode; auto pixmap = ImageTool::MatToQP ...
Day 11 集成到界面
集成ScanTask到界面中我们前面花了好几天来实现后台的处理,却一直看不到程序的界面,接下来我们尝试实现界面相关的功能。首先我们实现单张扫描功能。
我们增加一个slot函数onActSingleScan(),并在ScannerMainWindow的构造函数中将其与QAction actSingleScan关联起来。然后我们给出第一个实现版本:
12345void ScannerMainWindow::onActSingleScan(){ ScanTask task(_impl->_configer, _impl->_scanner); task.doScan({});}
运行程序,我们能够从调试窗口看到了程序运行的调试输出信息,说明程序在运行,但是界面一动不动,被阻塞住了。我们需要把它挪到独立的线程中运行。最简单的做法还是和在连接设备中做的一样,使用QtConcurrent::run()启动线程运行。
一种新手很容易犯的错误写法是:
123456void ScannerMainWindow::onActSingleSc ...
Day 10 高倍扫描 II
集成ImageJointer完成HighImagerJointer单元测试后,我们要将其集成到ScanTask中。
首先创建一个工厂函数,它根据样本类型来决定创建的IImageJointer子类的类别:
1234567891011std::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处理:
123456 ...
Day 09 高倍扫描
高倍扫描高倍扫描从细胞类别上分为白细胞,红细胞,巨核细胞三类,在细节上有所差别,但是本质上是相同的。我们只讨论白细胞的高倍扫描。
高倍扫描有两种扫描方式,一种是外周血常用的离散方式,即根据提供的坐标,拍摄一张张独立的照片,照片和照片之间没有关联。另一种是连续扫描,和低倍扫描类似,提供的是一个矩形的范围,拍摄连续的照片。和低倍扫描不同的是高倍连续扫描下,相邻的图片之间是有重叠区域的,我们需要将这些图片做拼接和切割,重新生成无重叠的固定大小的图片。
为什么外周血要使用离散扫描方式,而骨髓要使用连续扫描方式?这是一种时间和空间的权衡。在外周血中,白细胞的密度是比较低的,一般需要几个百倍视野的范围内才能有一个白细胞,如果使用连续扫描,会浪费大量的存储空间。相反,一般骨髓样本中的白细胞密度相当高,从几个到几十个,这样我们就不可能拍摄单独的照片了。对于一些特殊的病例,比如急性白血病,白细胞大量增生,我们就应当改用连续扫描,而另一方面,比如对化疗晚期的病人,其体内的白细胞极其稀少,即使是骨髓,一张玻片可能都找不到一百个,此时我们自然需要使用离散扫描的方式。
同样的道理,我们拍摄红细胞的时候必然是连 ...
overload模式详解
前几天跟一个解释代码,他怎么都理解不了我写的使用overload转换QOverload的代码。最后我只能跟他说你就记住我是这么写的,你在上面改就是了。今天有时间,把这个东西详细地写一下。
C++17中的overloadC++17中提供了overload模式,在std::visit的示例中。不注意还真的注意不到。
12345678template <typename... Ts>struct overload : Ts...{ using Ts::operator()...;};template <typename ... Ts>overload(Ts...) -> overload<Ts...>;
按照上面的观点,它应该是作为一个visitor和std::visit配合使用的。
123456789101112131415161718192021222324252627282930void overload_test(){ auto twice = overload{ [](std:: ...
C++20新增的数字比较函数
C++20新增的数字比较函数浏览C++refreence时注意到,C++20开始,增加了一批数字比较的函数
1234567891011121314template <class T, class U>constexpr bool cmp_equal (T t , U u) noexcepttemplate <class T, class U>constexpr bool cmp_not_equal (T t , U u) noexcepttemplate <class T, class U>constexpr bool cmp_less (T t , U u) noexcepttemplate <class T, class U>constexpr bool cmp_greater (T t , U u) noexcepttemplate <class T, class U>constexpr bool cmp_less_equal (T t , U u) noexcepttemplate <class T, class ...
Day 08 打包和低倍扫描
实现PackingWorker设计PackingWorker类接下来是数据打包功能。数据打包功能类PackingWorker,我们封装了对玻片索引数据库的操作和对zip归档文件的操作。这个类的生命周期应该是贯穿玻片扫描的全过程的,它应该是在doPrepare()的时候被创建,在每个扫描活动进行中被操作,用于记录数据库,将图像压缩到数据库等。下面是我们定义的它的初步的接口。可以看到,它的接口是和ScanTask中的signal对应的:
123456789101112131415161718192021222324class FRAMEWORKS_EXPORT PackingWorker : public QObject{ Q_OBJECTpublic: friend class PackingWorkerTester; friend class ScanTaskTester; explicit PackingWorker(const QString& path, const QString& slide_id, IConfiger* co ...
Day 07 预览图拍摄和数据归档
Day 07 拍摄预览图基本流程接下来实现拍摄预览图的函数doPreview()。
预览图拍摄阶段要做的活动包括:
调用GetSlidePreview()拍摄样本的预览图和标签图,并解析出标签上的条码/二维码内容
保存预览图和标签图到本地硬盘中和扫描档案文件中
解析标签图,识别出样本编号和样本类型
(可选)弹出对话框,让用户确认和修改样本编号和扫描参数等信息
保存扫描参数到索引文件
利用预览图找出要进行低倍扫描的范围
(可选)弹出对话框,让用户确认和修改低倍扫描的范围
从这里我们发现,要实现拍摄预览图,还有很多基础工作要做。我们先不考虑这些,尽量写一个初稿出来:
1234567891011121314151617181920212223242526bool ScanTask::doPreview(){ int retval; std::vector<PicInfor> picList; ScanInfo scanInfo; auto cleanup = qScopeGuard([&picList](){ ...
Day 6 实现扫描业务类
Day 6 实现扫描业务类ScanTask接下来要实现扫描业务。这将是软件比较复杂的一部分,我们将先抛弃界面部分,集中于业务管理。扫描活动是一个耗时操作,为了避免界面阻塞,我们需要在单独的线程中运行它。另外,它本身也会有很多需要并发执行的任务,也将会包含诸多的工作线程。本章开始我们的工作重点将是各种线程模式的使用。在多线程中运行ScanTask将放在界面部分讨论,我们重点关注ScanTask内部的多线程机制。
ScanTask概述当设备可用后,我们就可以进行扫描活动了。我们先看一下扫描的过程:
简单的来说,一张玻片样本的扫描步骤由下面几步组成:
12flowchart LR 进片 --> 拍摄预览图 --> 计算低倍扫描区域 --> 低倍扫描 --> 计算高倍扫描区域 --> 高倍扫描 --> 高倍扫描 --> 退片
进片:扫描仪移动载物台,将载物台移动到预览相机下面,拍摄样本全局图和标签图。这一步是由驱动的GetSlidePreview()实现的。
计算低倍扫描区域:软件对拍摄到的预览图做分析,根据玻片类型和扫描要求寻找出需要进行 ...
Day 5 扫描驱动桩开发
Day 5 扫描驱动桩开发关于扫描仪显微扫描设备是稀缺且昂贵的设备,并且扫描过程繁复(厂商提供的驱动,必须按照低倍-高倍的顺序进行,且百倍油镜一定会滴油,滴油之后如果再要做十倍扫描就需要再将油擦去),因此,前期的软件开发中,使用真实设备是很低效的工作。我们需要通过打桩的方式来实现开发前期对设备的替代。
首先定义接口IScanDevice。它是对提供的显微镜的接口的封装。我们在Adaptors项目中增加一个类IScanDevice,它为抽象接口类:
123456789101112class ADAPTORS_EXPORT IScanDevice{public: IScanDevice() = default; virtual int Initialize() = 0; virtual int GetSlidePreview(std::vector<PicInfor>* pic_list, ScanInfo* scan_info, std::string& qrcode) = 0; virtual int LowScanMoving(S ...