ImageMatcher设计与实现
1. 概述
干镜的扫描软件需要做连续扫描, 并将结果拼接成一张完整的大图形式. 涉及到几个难点:
- 图像数据量很大. 最低的20倍镜头, 扫描玻片全片大约是2200张图片. 40倍则是接近10000张, 100倍则是6万张上下. 每张图片是2448x2048的分辨率, 24bit彩色图像, 原始数据是15M上下.
- 扫描仪扫描图片的速度是极快的, 以20倍扫描为例, 2200张图片大约90秒就能够完成扫描. 总的数据量超过了30G, 已经超过了电脑的内存大小. 如果再加上其他过程需要的内存, 完全会将系统撑死. 因此, 图片的处理必须能够尽量跟的上扫描的速度.
- 图像的拼接. 扫描仪的硬件精度不是很好, 在运行扫描过程中, 帧与帧之间大约又±50个像素的误差, 因此无法直接利用硬件(步进电机)的位置作为拼接, 只能作为一个拼接的基准值, 使用图像匹配的方式来做精确的匹配. 而图像匹配的速度是相对比较慢的. 比如想尽一切办法来进行优化. 甚至, 我们都要想办法来尽量拖慢扫描仪的扫描进度(因为它不归我们控制, 我们不好对第三方提出减慢扫描速度的要求).
- 扫描的图片中可能存在大片的空白区域, 是真正的空白区域, 图像匹配一定是结果异常的. 但是必须要能够适应, 不能说匹配失败了就终止拼接, 既然是空白, 或者邻域空白, 那么总要找一个合适的地方来放置它.
- 要定义一种文件格式, 能够保存并且快速查看这么巨大的图像, 并且不能有太高的资源占用率.
在过程中, 我们考虑过利用GPU来加速图像匹配的速度, 但是实际测试下来, 价值并不大. 因为我们对图像处理很单一, 仅仅是加载图片, 做一次图像匹配检测, 然后就释放. 大量的时间实际上消耗在从CPU到GPU之间的数据总线传输上了, 和CPU多核处理比较起来, 价值很有限.
2. 基本设计
ImageMatcher的核心设计有几点:
- 尽量挖掘多核CPU的潜力, 实现拼接的线程化.
- 随扫随拼随释放, 图像尽量不在内存中积压
- 尽量不使用文件系统做缓存, 一张图片的整个生命周期中都在内存中存在. 因为一旦涉及到硬盘读写, 都会极大的拖累整个系统的性能.
- 尽量实现系统的可靠性和可调试性. 为此, 专门实现了一个可以用于多线程环境的日志系统.
3. 类结构
包括下面几个类:
- ImageProcessor: 实现图片的采集和拼接功能
- TileArchiverWriterEx和TileArchiverReaderEx: 实现基于Tile的文件存储和解析.
- ImageView: 一个实现查看TileArchiver格式的图像的控件.
- ILogger: 日志类的封装, 用于支持多线程下的调试日志输出到文件.
4. ImageProcessor类实现机理
类的主要接口如下所示:
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| class ImageProcessor : public QObject, public ILogger { explicit ImageProcessor(const QString& task_name , const QString& work_path , DeepLogger::Logger::Ptr logger , QObject* parent=nullptr);
void prepare(int cols, int rows); void addImageAsync(Offset pos, cv::Mat data, RawImageFuncType raw_func=RawImageFuncType()); void waitForFinished(); int waitingFutures(); void startMatchTasks();
void startSplitTask();
void startTileSaveServer();
void startTileTask(TileArchiverEx::TileIndexKey pos, cv::Mat image, bool is_last_col, bool is_last_row);
void matchProcByCol(int thread_id, int start_col, int end_col, int step); void splitViewProc(); void setViewDataInputed(ImageElement& view); void waitingViewDataInputed(ImageElement& view); void setViewDataReady(ImageElement& view); void waitingViewDataReady(ImageElement& view); void setViewMatched(ImageElement& view); void waitViewMatched(ImageElement& view); void setViewLocated(ImageElement& view); void waitViewLocated(ImageElement& view);
void matchSurround(ImageElement& view); void initSliceCache();
void splitViewLines(int row); void makeSlice(int col, int row, bool is_last_col, bool is_last_row); void makeSliceProc(int col, int row, bool is_last_col, bool is_last_row); void cacheView(Offset pos, cv::Mat image);
void cacheSlice(int col, int row, cv::Mat image);
void dumpMat(int col, int row, cv::Mat mat, const QString& base_path, ImageFormat format, int quality);
void archiverServer(); }
|
4.1 线程规划
本质上ImageProcessor是数据流的处理模式:
图像数据 -> 匹配计算 -> 切割 -> Tile -> 归档
每个处理步骤由一个或几个工作线程完成. 为了实现简单, 这些全部在线程池中进行. 我们使用QtConcurrent::run()
来创建工作线程, 而不是实现更复杂的线程对象.
事实上我们也不需要对启动的工作线程做什么额外的控制. 扫描过程是一旦启动就自动进行的, 出错了唯一的可以做的工作就是抛弃数据, 退出程序, 重新扫下一张数据. 主要把shadule线程杀掉就可以了.
线程规划如下:
一个Matcher线程池, 用于匹配计算相邻视野的偏移量位置. 支持两种策略: 每个视野一个工作线程, 和每列视野一个工作线程. 前者为matchProcByView
, 后者为matchProcByCol
. 对于perView策略, 每个工作线程是临时性的, 完成自己视野的计算就结束了, 对于perColumn来说, 每个线程的生命周期是长期的, 要一直等到整个扫描处理结束后才会逐步结束. 相对来说, 前者更容易管理. 后者因为是持久性线程, 实际上线程数量远远超过了CPU的理论规格数量. 必须对线程池的大小做特别的设置.
一个用于做切分线程splitViewProc
, 它处理计算好匹配位置的视野, 并计算出切分的位置
一个切分实施线程池, makeSliceProc
, 每个要被切分的视野由一个工作线程所处理, 它会根据splitViewProc
计算出来的偏移量, 从各个原始图片中获取图像部分的数据, 并拼接成一张切分图片.
生成Tile的线程池, 每个splitViewProc()
生成切分图片后会创建一个makeSliceProc()
来做瓦片化处理. 每四个相邻的切分子图会缩小合并成上一级的金字塔. 这四个makeSliceProc
线程会保留一个继续做上一级的合并, 其他三个工作线程就结束.
一个Tile格式归档工作线程archiverServer
, 它负责生成最终的扫描结果文件. 这个文件由一级级的图像金字塔组成, 并且提供了索引机制, 以便快速定位到具体某一个金字塔图像.
4.2 线程生命周期
4.2.1 启动线程
ImageProcessor::prepare()
负责启动长生命周期线程. 在扫描开始时进行.
- 启动切分线程
splitViewProc()
- 启动图像档案文件保存线程
archiverServer()
- 在preColumn模式下, 启动所有的匹配线程
matchProcByCol
. 如果是perView模式, 则不需要实现启动.
splitViewProc()
线程中, 负责启动每个子图切割线程makeSliceProc()
. 目前我们的做法是按行处理, 当识别到新得到的扫描图片能够拼接出新的一行后, 就启动一行子线程makeSliceProc()
, 分别计算. 这些工作线程会一直向上生成金字塔, 最终只剩下一个线程完成最高一张金字塔图像.
4.3 主要数据结构
4.3.1 视野信息
定义结构ImageElement
来表示一个视野的处理状态和信息, 使用一个QMap<Offset, ImageElement>
来保存所有视野的数据. 其中, Offset是一个自定义结构, 其实就是一个QPoint
, 表示这个视野的行列号.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| struct ImageElement { QSharedPointer<QSemaphore> _view_inputed; QSharedPointer<QSemaphore> _view_available; QSharedPointer<QSemaphore> _view_matched; QSharedPointer<QSemaphore> _view_located; QAtomicInteger<quint32> _used_flag; Offset cord; cv::Mat data; double degree; bool credible; int anchor; QPoint offset; QRect grect; ImageElement() ... ImageElement(Offset pos, QSize size) ... ... };
|
上面的结构是针对perColumn的场景提出来的. 几个信号量用于标记这个线程的工作状态, 每个match线程会等待检测它的相邻线程的就绪标记, 当检测到邻居数据就绪了, 就可以开始匹配计算. 使用完邻居数据后, 会设置其使用标记_used_flag
. 每个线程都有义务在自己或邻居被使用完毕后释放自己的图像数据.
后面的一些属性是匹配要使用的. 计算完匹配后, 会记录下和邻域的相对位置. 这是通过一个anchor
和offset
标记的. anchro
表示它基于它的哪个邻居视野–上面的视野, 或者左/右视野–因为扫描是蛇形扫描, 上一个扫描视野可能在它的左边, 也可能在它的右边.
所有的视野的ImageElement
构成一个QMap
结构:
ImageMap m_imageCache; //< 视野的缓存
这个Map在扫描开始前就准备好, 在整个扫描匹配过程中不会发生element的增删操作, 涉及到多线程互斥操作的都是信号量或原子量, 就不再需要在访问时特别做互斥保护了.
4.3.2 Tile信息
TileElement
用于表示每个Tile的状态和数据:
1 2 3 4 5 6 7 8 9 10 11
| struct TileElement { TileIndexKey _pos; bool _is_col_end; bool _is_row_end; QAtomicInteger<FlagType> _flags; QVector<cv::Mat> _sub_data; cv::Mat _data; QSharedPointer<QMutex> _mutex; ... };
|
其中, TileIndexkey
表示这个Tile的位置, 它的定义如下. 其三个成员分别表示这个Tile的层级, 以及在这一层中的行列号. 我们使用它来标记定位每一个Tile.
1 2 3 4 5
| struct TileIndexKey{ qint32 _level; qint32 _col; qint32 _row; };
|
由于我们是数据随完成随归档保存, 因此数据文件中的tile是无序的, 并且也不能保证相同的两次扫描数据存储的一致性. 同时, 在Tile中保存的是压缩后的图像数据, 长度也是不确定的. 为此, 我们在数据文件中使用一个索引来标记每个Tile的起始位置和数据长度.
1 2 3 4 5 6 7
| struct TileIndexReal{ FlagType _tag; qint32 _level; qint32 _col; qint32 _row; qint64 _offset; };
|
5. ImageProcessor主要处理算法
5.1 添加图像 addImageAsync()
当扫描仪检测线程从扫描仪读取到一张图片后, 调用此函数, 将图片添加到对应的图像缓冲数据中m_imageCache
中去. 并设置图像就绪标志.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| void ImageProcessor::addImageAsync(Offset pos, cv::Mat data, RawImageFuncType raw_func) { ... auto& view = getImageElement(pos); cv::Mat image = raw_func ? raw_func(pos, data) : data; image.copyTo(view.data); setViewDataInputed(view); }
void ImageProcessor::setViewDataInputed(ImageElement &view) { view._view_inputed->release(255); }
|
这样, 这个视野自己的匹配线程就可以进行匹配操作, 而它的相邻的视野的匹配线程也可以使用它来做匹配数据了.
如果使用得是perViewperThread得模式, 则在这个函数中启动对应视野得matchProcByView()
线程. 其他得处理也是一样得.
5.2 视野匹配线程 matchProcByCol()
这个函数是按列做匹配的长生命周期工作线程. 我们的列分布, 准确的说, 是一个线程处理等间隔的几列的视野匹配. 当初设计的初衷是让线程能够尽可能彼此错开计算, 因此数据是1,2,3,4,5这样的顺序, 而如果是两个线程, 则一个线程处理1,3,5, 一个线程处理2,4,6列, 这样尽量避免一个线程处理大量数据而另一个线程闲着.
工作线程根据蛇形扫描顺序, 依次等待它应该接收到的下一个视野的图像数据, 等待相邻视野数据就绪后执行匹配计算, 并将计算出的结果写入到ImageElement
中.
这里要注意的一点是, 如果是使用OpenCV的GPU, 一定要创建一个matcher对象, 并且这个matcher对象不能在线程之间共用. 实际测试下来, 发现GPU价值并不大.
匹配处理线程只做匹配, 并记录匹配结果, 并不做其他的事情. 后续的处理由分割线程splitViewProc()
来做.
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 45 46 47 48 49
| void ImageProcessor::matchProcByCol(int thread_id, int start_col, int end_col, int step) { #ifdef HAS_GPU auto matcher = cv::cuda::createTemplateMatching(CV_8UC3, cv::TM_CCORR_NORMED); cv::cuda::Stream stream; #endif for(int row=0; row<_scan_rows; ++row) { int col_begin = isEven(row) ? start_col : end_col; int col_end = isEven(row) ? (end_col + step) : (start_col-step); int col_step = isEven(row) ? step : -step;
for(int col=col_begin; col!=col_end; col+=col_step) { Offset pos(col, row); auto& view = getImageElement(pos); waitingViewDataInputed(view); cacheView(pos, view.data); if(!_is_do_match) { inspect2(__func__, "do not match!"); setViewDataReady(view); continue; }
if( _pre_match_func){ _pre_match_func(pos); } calibViewVignet(view); setViewDataReady(view); #ifdef HAS_GPU matchSurround(view, matcher, stream); #else matchSurround(view); #endif this->matchTicker().stop(); setViewMatched(view); } } log2(__func__, "matchProc() Finished: ", DSHOW(thread_id)); }
|
5.3 视野匹配
收到一个视野, 要将它和它正上方的视野, 以及它的扫描前序视野做匹配. 如果是最上面一行, 则不需要和上面的匹配, 如果是扫描过程中一行的第一个视野, 也没有前序视野可以匹配. 这些都是在matchSurround()
里面需要处理的.
因为我们是按列处理, 因此, 一个视野正上方的视野数据一定是就绪的, 它的前序视野则需要等待那个处理线程的就绪标志.
匹配使用opencv的matchTemplate()
函数进行, 本身没有太多的需要特别说明的.
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| void ImageProcessor::matchSurround(ImageElement &view) { auto pos = view.cord; MatchResult result1{MatchFlag::UnMatch, 0.0, QPoint(), 0.0 }, result2{MatchFlag::UnMatch,0,QPoint(), 0.0}; int direction1 = UP; int direction2 = pos.y() % 2==0 ? LEFT : RIGHT; cv::Rect srcRect1, tmplRect1, srcRect2, tmplRect2; if(!isTopView(pos)) { auto peer_pos = getUpPos(pos); auto& peer = getImageElement(peer_pos); TRACE_IF(peer.data.empty(), QString("视野{%1,%2}上面的视野{%3,%4}图像非法").arg(pos.x()).arg(pos.y()).arg(peer_pos.x()).arg(peer_pos.y())); srcRect1 = getSrcRect(direction1); tmplRect1 = getTmplRect(direction1); try { auto srcImg = peer.data(srcRect1); auto tmplImg = view.data(tmplRect1); auto t1 = cv::getCPUTickCount(); result1 = matchHost(srcImg, tmplImg); auto t2 = cv::getCPUTickCount(); view._click += t2-t1; if(result1._credit==Matched) { view.offset = QPoint( srcRect1.x + result1._offset.x() - tmplRect1.x, srcRect1.y + result1._offset.y() - tmplRect1.y ); } else { view.offset = QPoint(this->_vx0, this->_vy0); } view.degree = result1._degree; view.credible = (result1._flag == Matched); view.anchor = direction1; inspect2(__func__, "View", DSHOW(pos), "match top view",peer_pos," finished. result is: " , DSHOW(result1._flag), DSHOW(result1._degree), DSHOW(result1._credit) , DSHOW(result1._offset), "\n", DSHOW(view.offset) ); } catch(cv::Exception& e) { error2(__func__, "match upside exception! current: ", pos, ", up view: ", peer_pos, "exception: ", e.what()); } } else { inspect2(__func__, "current view need not match to up", DSHOW(view.offset)); } if(!isLineFirst(pos)) { ... } ... trace2(__func__, view.cord, "has finished matched. ", DSHOW(view.offset), DSHOW(view.anchor)); }
|
5.4 分割线程 splitViewProc()
分割线程是一个独立的线程, 它会按扫描的顺序依次等待每个扫描视野匹配就绪. 然后计算这个视野在全局坐标系中的位置, 也更新ImageElement
集合m_imageCache
.
当检测到一行扫描视野都完成匹配之后, 就执行这一行数据的分割工作, 分割出一个个相邻的不重叠的子图像.
这个线程中, 会释放掉m_imageCache
中不再使用的视野的图像数据.
前面讨论的所有的匹配工作线程的结果都会汇集到这个线程来处理, 然后再分发出tile处理线程分别处理, 因此这个线程是一个数据流程中的中间汇集点, 也是吞吐量性能的瓶颈. 因此这个线程中不做大规模耗时的计算处理, 也不做IO操作. 仅仅做计算和分发.
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 45 46 47 48
| void ImageProcessor::splitViewProc() { int rows = this->_scan_rows; int cols = this->_scan_cols; struct { int begin; int end; int delta; } controls[2] = { {0, cols, 1} ,{cols-1, -1, -1} };
for(int row=0; row<rows; ++row) { int idx = row % 2; for(int col=controls[idx].begin; col!=controls[idx].end; col+=controls[idx].delta) { Offset pos = Offset(col, row); ImageElement& view = getImageElement(pos); waitViewMatched(view); if( col==0 && row==0){ view.grect = view.grect.translated(-_protect_horz, -_protect_vert); setViewLocated(view); } else { auto anchor_pos = (view.anchor==LEFT || view.anchor==RIGHT) ? getPrevPos(pos) : getUpPos(pos); auto& anchor_view = getImageElement(anchor_pos); waitViewLocated(anchor_view); view.grect = anchor_view.grect.translated(view.offset); setViewLocated(view); } } trace2(__func__, "scan ", DSHOW(row) ); if(row==0){ initSliceCache(); } splitViewLines(row); } }
|
真正的切割工作是在函数splitViewLines()
中进行的:
5.5 行切割函数splitViewLines()
行切割函数在splitViewProc()
检测到扫描了一行数据后进行. 为了简化实现, 我们有一定的限制:
- 函数成功的前提是扫描视野图像的高度要大于要切割出来的子图的高度. 这个不是什么特别大的问题. 我们扫描仪的扫描视野是2048x2448的高度, 而切割出的视图大小要求不能超过1024x1024. 最高也不超过2048.
- 要求扫描的一行视野在垂直方向上不会有太大的差别. 这个也不是问题. 经过实测, 在扫描仪各个倍率对应允许的扫描宽度范围内, 扫描仪的精度完全足以满足这个限制: 扫描仪只是每次换行时有较大的机械误差, 在同一行扫描的过程中, 其水平准确度还是能够控制在200个像素以内的.
- 上述要求其实就是要求我们扫描最后一行后, 一定可以切割出新的一行出来. 而不是扫描玩最后一行, 发现由于重叠或参差, 导致是无效数据.
- 还假设扫描完前两行之后一定可以切割出一行完成的slice. 这个要求收紧一点, 就是slice的高度不能大于视野的高度.
上述限制都是为了让代码实现更简单易懂而加的限制. 完全可以满足.
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
| void ImageProcessor::splitViewLines(int view_row) { auto left_view = getImageElement(0, view_row); int top_up = left_view.grect.top(); int top_down = top_up; int bottom_up = left_view.grect.bottom(); int bottom_down = bottom_up; static int pre_finished_slice_rows = 0; static int last_used_view_row = -1; if(view_row==0){ pre_finished_slice_rows = 0; last_used_view_row = -1; } for(int col=0; col<this->_scan_cols; ++col) { auto rect = getImageElement(col, view_row).grect; top_up = std::min(top_up, rect.top()); top_down = std::max(top_down, rect.top()); bottom_up = std::min(bottom_up, rect.bottom()); bottom_down = std::max(bottom_down, rect.bottom()); }
int slice_row_begin = top_up / _slice_height; int slice_row_end = bottom_down / _slice_height; int slice_row_finished = bottom_up / _slice_height;
_slice_rows = isBottomView(view_row) ? slice_row_finished : slice_row_end+1;
try{ for(int slice_row=slice_row_begin; slice_row<_slice_rows; ++slice_row) { for(int slice_col=0; slice_col<this->_slice_cols; ++slice_col) { SliceElement& slice = getSliceElement(Offset(slice_col, slice_row));
for(int view_col=0; view_col<this->_scan_cols; ++view_col) { auto& view = getImageElement(view_col, view_row); QRect cp = slice.rect.intersected(view.grect); if(cp.isValid()) { QRect s = cp.translated(-view.grect.topLeft()); QRect d = cp.translated(-slice.rect.topLeft()); ViewPart part{ view.cord, s, d}; auto rect = cv::Rect(s.left(), s.top(), s.width(), s.height()); TRACE_IF(!validPart(view.data, rect), QString("拼接异常: rect={x=%1,y=%2, w=%3, h=%4}, data size: {cols=%5, rows=%6}") .arg(rect.x).arg(rect.y).arg(rect.width).arg(rect.height).arg(view.data.cols).arg(view.data.rows)); view.data(cv::Rect(s.left(), s.top(), s.width(), s.height())).copyTo(part.data); slice.parts.insert(view.cord, part); } else { continue; } } } } } catch(cv::Exception& e) { error2(__func__, "opencv exception occured slice making :", e.what()); }
for(int row=pre_finished_slice_rows; row<slice_row_finished; ++row) { auto is_last_row = isBottomView(view_row) && row==_slice_rows-1; for(int col=0; col<_slice_cols; ++col) { auto is_last_col = col==_slice_cols-1; if(_is_make_slice) { makeSlice(col, row, is_last_col, is_last_row); } } } int begin, end; if( isTopView(view_row) && isBottomView(view_row) ) { begin = end = view_row; } else if (isTopView(view_row)) { begin = 0; end = -1; } else if (isBottomView(view_row)){ begin = view_row -1 ; end = view_row; } else { begin = end = view_row-1; } for(auto row=begin; row<=end; ++row) { for(int col=0; col<_scan_cols; ++col) { getImageElement(col, row).data.release(); } } pre_finished_slice_rows = slice_row_finished; }
|
在上面, 会调用makeSlice()
来处理识别出的一个子图. 它会启动一个工作线程makeSliceProc()
来进行处理.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| void ImageProcessor::makeSlice(int col, int row, bool is_last_col, bool is_last_row) { if(_slice_task_parallel) { _sync_mutex.lock(); _sync.addFuture(QtConcurrent::run(&this->_slice_thread_pool, this, &ImageProcessor::makeSliceProc, col, row, is_last_col, is_last_row)); log2(__func__, "current waiting tasks: ", DSHOW(_sync.futures().size())); _sync_mutex.unlock(); } else { trace2(__func__, "start makeSliceProc parallel"); makeSliceProc(col, row, is_last_col,is_last_row); } }
|
5.6 数据拼接和瓦片处理 makeSliceProc
在上一级中, 识别出每个Slice, 并将信息保存在SliceElement
结构中. SliceElement
中的成员parts
中保存了构成这个Slice的视野的索引
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| struct ViewPart { Offset view_id; QRect view_rect; QRect slice_rect; cv::Mat data; };
struct SliceElement { Offset cord; bool is_col_end; bool is_row_end; QRect rect; QMap<Offset, ViewPart> parts; ... };
|
首先需要完成视野的拼接. 如果不做平滑处理, 则只需要简单地拼接拷贝就可以. 生成Slice后, 则执行打包处理. 打包处理是要根据这张原始Slice向上逐级生成金字塔层次. 每四张第一级的图片合并成一张高一级的, 这样逐级向上, 直到结束.
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
| void ImageProcessor::makeSliceProc(int col, int row, bool is_last_col, bool is_last_row) { auto& slice = getSliceElement(col, row); cv::Mat slice_data(_slice_height, _slice_width, CV_8UC3); for(auto iter=slice.parts.begin(); iter!=slice.parts.end(); ++iter) { auto part_img = iter.value().data; auto part_rect = cvRectFromQRect(iter.value().slice_rect); part_img.copyTo(slice_data(part_rect)); iter.value().data.release(); } ... if( _is_export_slice){ cacheSlice(col, row, slice_data); } if( _is_archive) { this->startTileTask({0, col, row}, slice_data, is_last_col, is_last_row); } ... }
|
这个工作我们直接在这个线程中进行. 每个线程会检测自己的邻居是否就绪了, 如果这四个邻居都就绪了, 则其中一个线程负责生成上一级图片, 其他三个线程就退出结束. 这里的startTileTask()
函数最终是调用了TileArchiverWriterEx::tileTask()
来做这件事.
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
| void TileArchiverWriterEx::tileTask(TileArchiverEx::TileIndexKey start_pos, cv::Mat image,bool is_last_col,bool is_last_row, bool is_save) { ... auto pos = start_pos; auto& start_tile = getOrNewElement(pos, is_last_col, is_last_row, 0); image.copyTo(start_tile._data);
if(is_last_col && pos._row==0){ _imageInfo._cols_0 = pos._col+1; _imageInfo._imgwidth = _imageInfo._cols_0 * _imageInfo._tilewidth; } if( is_last_row && pos._col==0){ _imageInfo._rows_0 = pos._row+1; _imageInfo._imgheight = _imageInfo._rows_0 * _imageInfo._tileheight; } _start: TileElement& tile = getTileElement(pos); if( this->_is_debug_open){ dbgDumpTile(tile._pos, tile._data); } if(is_save) { this->archiveTile(pos, this->transfer(tile._data)); } if( tile._pos._col==0 && tile._is_col_end && tile._pos._row==0 && tile._is_row_end) { _imageInfo._levels = tile._pos._level + 1; if(is_save) { this->archiveTile(NullIndex, QByteArray()); } return; } else { qint32 flags = (tile._is_col_end && pos._col % 2==0) ? 0x0A : 0; flags |= ( tile._is_row_end && pos._row %2==0) ? 0x0C:0;
auto np = makeNextCoord(pos); auto sub_col = np.second.x(); auto sub_row = np.second.y();
auto& sub_tile = getOrNewElement(np.first, tile._is_col_end, tile._is_row_end, flags); cv::Mat sub_data; cv::pyrDown(tile._data, sub_data); setTileSubData(sub_tile, sub_col, sub_row, sub_data); tile._data.release(); if( isTileDataReady(sub_tile)) { mergeTileSubData(sub_tile); pos = sub_tile._pos; goto _start; } } ... return; }
bool TileArchiverWriterEx::isTileDataReady(TileArchiverWriterEx::TileElement &tile) { return tile._flags.testAndSetOrdered(getFullFlag(), getFullFlag()+1); }
void TileArchiverWriterEx::mergeTileSubData(TileArchiverWriterEx::TileElement &tile) { auto size = getTileSize(); auto sub_width = size.width()/2; auto sub_height=size.height()/2; cv::Mat result(size.height(), size.width(), CV_8UC3); cv::Rect dst_rect[4] { cv::Rect(0, 0, sub_width, sub_height) , cv::Rect(sub_width, 0, sub_width, sub_height) , cv::Rect(0, sub_height, sub_width, sub_height) , cv::Rect(sub_width, sub_height, sub_width, sub_height) }; for(int i=0; i<4; ++i) { if( !tile._sub_data.value(i).empty()){ tile._sub_data[i].copyTo(result(dst_rect[i])); tile._sub_data[i].release(); } } setTileData(tile, result); }
|