看图工具ImageView的设计与实现

1 概述

干镜扫描出来的是一个包含数千张甚至数万张高分辨率图片组成的图像数据. 在查看时, 我们不可能一次性的加载所有的图像去显示, 只能使用滑动窗口的方式, 仅加载当前显示的部分, 并且释放掉已经不再需要查看的图像数据, 以节省内存的占用.

目前的版本是从本地获取图像数据,以后要考虑数据存储在网络上的情况。

我们使用金字塔方式的文件结构, 逐级构造出金字塔方式的档案文件.

我们后来才查找到TIFF也支持类似的存储方式, 但是找不到详细的资料, 而且也没有能够直接使用的图像显示操作控件, 所以最终还是自己从头实现.

2. 基本架构

ImageView核心由及部分组成:

  • ImageView, 是QGraphicsView的一个派生类, 用于显示图片
  • TiledPixmapItem, 是QGraphicsItem的子类, 用于显示一张Tile的Item
  • ImageProvider, 用于封装瓦片文件的按需读取

3. 基本实现思路

实现的基本思路是利用了Qt的QGraphicsScene/QGraphicsView的机制. 在Scene中按照坐标创建布置一个个Item, 然后利用Qt本身提供的机制, 按需加载和释放.

3.1 TiledPixmapItem::paint

只有在paint()函数被调用时才会延迟加载图像数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void TiledPixmapItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
Q_UNUSED(widget)
// 计算显示比例.
const qreal lod = option->levelOfDetailFromTransform(painter->worldTransform());
// 计算需要的级别:
int level = qBound(0, int(qLn(lod)/qLn(0.5)), _provider->maxLevel());
// 减少读取图像的次数: 缓存pixmap. 只要level不发生变化, 就不需要修改.
if( level != _level)
{
_level = level;
_pixmap = _provider->requestImage(_pos, level);
}
painter->drawPixmap(option->rect, _pixmap);
}

3.2 ImageProvider::requestImage

实现加载操作. 根据要求加载指定级别和位置的图像数据.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
QPixmap ImageProvider::requestImage(QPoint pos, int level)
{
// 计算当前级别下的缩小比例. 即(1/2)^level.
double ratio = pow(0.5, level);
// 计算出当前需要的图像在level级别中的图像的行列号
int x = qFloor(pos.x() * ratio);
int y = qFloor(pos.y() * ratio);
// 计算要获取的图片在所求的瓦片中的偏移位置
qreal dx = (pos.x() * ratio - x) * _tileWidth;
qreal dy = (pos.y() * ratio - y) * _tileHeight;
TileIndexKey key{level, x, y};
if( !_cached.contains(key)){
auto pixmap = new QPixmap();
pixmap->loadFromData(_reader->loadTile(key));
_cached.insert(key, pixmap);
}
auto pixmap = _cached.value(key);
int width = qMin(pixmap->width(), qCeil(_tileWidth * ratio));
int height= qMin(pixmap->height(), qCeil(_tileHeight * ratio));
return pixmap->copy(dx, dy, width, height);
}

3.3 Cache清理

我们在ImageProvider中使用一个QMap来保存加载的图像数据: QMap<TileIndexKey, QPixmap*> _cached;
随着用户的操作, 这个缓存会越来越大, 直到最极端的情况下, 将整个金字塔都加载进来. 因此, 必须进行清理操作.

一种是Qt提供的QCache,它实现了LRU。并且恰好也是一个Map之类的东西。
另一种方法是自己写一个,满足自己一些特殊的要求。我们选用后者:

我们这样进行:

  1. 当更改显示级别的时候, 清理掉前后隔一级以上的所有图片资源. 例如, 当前切换到Level3, 那么清理Level0-1, Level 5及以上的所有图像.
  2. 当在同一级中滑动时, 清理以当前视野为中心, 3个视野距离以外的所有视野的图像资源.
  3. 不需要做预加载. 实际上, 读取金字塔文件并不是一个很耗时得操作, 因此并不需要过度关心cache得因素. 从实测看, 即使按需加载立刻释放也不会对图像流畅度有太大得影响.

这样可以把消耗内存减小到最小的地步,同时不过于影响性能。

如果是从网络上获取数据,则考虑直接用QCache,同时将数据缓存到本地上。同时再实现一个对本地硬盘做清理的定时任务。