MagicTool实现

MagicTool为仿照某产品的实现而提供的功能, 目的是尽量减少使用者切换工具的操作, 能够利用鼠标以不同的操作手势来实现不同的行为.

规格:

按下Magic按钮, 进入Magic模式.

  1. 染色体修型/切割: 在背景上按下鼠标, 画线经过一个染色体, 并在背景上释放鼠标.
    将经过的第一个染色体拆分, 并抛弃小于阈值的部分. 识别:
    • flyovers.size>=3.
    • 从染色体外起步
    • 从染色体外结束
    • 中间经过至少一个染色体
    • 要切割的就是遇到的第一个染色体
  2. 染色体增补: 在一个染色体内部按下鼠标, 拖动鼠标到这个染色体外部区域, 描绘区域再回到这个染色体放开鼠标.
    将鼠标围起来的染色体外部的区域叠加到染色体上面.
    • 从染色体里面进入
    • 进入其他的染色体或背景
    • 从同一个染色体结束
  3. 连接染色体: 在染色体内部按下鼠标, 移动鼠标到相邻的另一个染色体
    量两个染色体连接到一起.
    • 从一个染色体内部开始
    • 经过其他的染色体和空白
    • 在另一个染色体内结束
  4. 分割染色体: 在一个染色体内部按下鼠标, 在这个染色体内部移动鼠标并画一个封闭区域, 然后松开鼠标.
    • 从一个染色体内部开始,
    • 在同一个染色体内部结束
    • 有一个封闭曲线
  5. 新增染色体: 在背景上按下鼠标, 画一个封闭曲线, 然后松开鼠标. 中间不经过任何item.
    • 被围起来的区域就是新增的曲线.
    • 必须有且仅有一个封闭曲线—- 只识别一个, 如果多了, 结果不确保!

实现机理:

鼠标行为的跟踪

- 当鼠标被按下时, 检查是否在某个Item上面.
- 当鼠标移动时, 检测是否在Item上面, 并且这个Item和之前的Item是否一样
- 当鼠标松开时, 退出状态
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
void ExtractScene::_dealSplitMousePress(QGraphicsSceneMouseEvent *event)
{
if(_getMouseState() == EMouseState::Waiting && _impl->_splitToolType!=ScriptItem::Style::SelectMode )
{
if(event->button() == Qt::LeftButton)
{
_setMouseState(EMouseState::Drawing);
auto cur_pt = event->scenePos();
_impl->_magicFlyChroms.clear();
auto item = _firstChromItemAt(cur_pt);
_impl->_magicFlyChroms.push_back(item);
...
}
}
...
}

void ExtractScene::_dealSplitMouseMove(QGraphicsSceneMouseEvent *event)
{
if(_getMouseState() == EMouseState::Drawing)
{
if(event->buttons() & Qt::LeftButton)
{
auto cur_pt = event->scenePos();
auto item = _firstChromItemAt(cur_pt);
if(item != _impl->_magicFlyChroms.back())
{
_impl->_magicFlyChroms.push_back(item);
}
...
}
}
...
}

void ExtractScene::_dealSplitMouseRelease(QGraphicsSceneMouseEvent *event)
{
//TRACE() << TSHOW(_getMouseState());
if(_getMouseState() == EMouseState::Drawing)
{
if(event->button() == Qt::LeftButton)
{
auto cur_pt = event->scenePos();
auto item = _firstChromItemAt(cur_pt);
if(item != _impl->_magicFlyChroms.back())
{
_impl->_magicFlyChroms.push_back(item);
}
if(_impl->_curScript->valid())
{
_applySplit(_impl->_curScript, _impl->_magicFlyChroms);
}
}
...
}
}


其中, _impl->_magicFlyChroms是一个vector, 它记录鼠标当前经过的ChromosomeContourItem, 将Item的指针保存进去. 这里的一个小问题是, 我们每次都只检测topmost的那个item. 实际上, Qt的这个QGraphicsScene::itemsAt()函数的行为有点奇特, 当两个Item叠加的时候, 随着鼠标的移动, 究竟哪个在最上面都不能确定, 因此, 虽然这里已经做了去重, 但是最终的结果仍然是可能存在重复的. 我们还要在后面必要的时候再做检查.
不过, 就一般而言, 这种实现已经是足够了的.

另外一种做法, 可以不跟踪鼠标的移动, 而是在用户释放鼠标之后, 统一做识别. 这需要利用到Qt的Path的intersects(), 有时则需要用到OpenCV的相关函数. 这种做法的性能应该是更高的, 但是因为丢失了序列信息, 我们无法知道先进入的那个Item, 后进入的哪个Item, 后来证明并不能满足用户的诉求, 所以废弃了这种实现, 改为当前的设计.

手势操作的识别

当用户释放鼠标时, 识别用户的手势操作. 在_identifyMaginOp()中实现. 它的两个参数, 第一个是用户鼠标移动形成的一条曲线, 第二个参数则是所有经过的ChromosomeContourItem的指针. 后者经过了初步的去重操作, 但是并不一定保证.

1
2
3
std::pair<ExtractScene::SplitOpType, std::vector<ChromosomeContourItem *> > ExtractScene::_identifyMaginOp(
const ScriptItem *curve,
const std::vector<ChromosomeContourItem *> &flyovers)

我们要根据flyovers的首尾, 个数等信息来判断操作类型, 以及受影响的Item.

这里要注意的是连接操作. 连接操作因为包含了所有经过的item, 我们在mouseMove的事件处理中是无法实现完全的去重的, 所以要在这里做进一步的去重. 而其他的操作因为只做一个, 则不需要.

1
2
3
4
5
6
7
8
9
10
11
else if ( flyovers.size()>=2 && flyovers.front()!=nullptr && flyovers.back()!=nullptr && flyovers.front()!=flyovers.back())
{
//TRACE() << "Magic 连接染色体";
opType = SplitOpType::OpSticky;
opItems = ranges::views::all(flyovers)
| ranges::views::filter([](const auto p){return p!=nullptr;})
| ranges::to<std::vector>()
| ranges::actions::sort
| ranges::actions::unique
;
}

这里我们使用rangeV3来实现这个过滤操作. 注意的是unique()操作的微妙之处. 所谓的unique, 不管是rangev3还是标准的std::unique, 都是去掉相邻的重复值. 例如, 序列1,2,3,3,2, 处理后的结果是1,2,3,2, 而不是1,2,3. 要得到真正的唯一的值序列, 需要首先做排序处理. 我们这里就是简单地对指针做了个排列. 其顺序并不重要, 重要的是通过排序, 将相同的item放到一起.

应用处理

在识别了手势之后, 剩下的事情就没有太多的复杂了. 在_applySplit()函数中实现了这个处理. 简单来说, 就是要识别出新增的item, 要删除的item, 要修改的item, 分别发送给对应的undo command去处理.

例如, 下面的代码段是处理cut操作的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if(optype==SplitOpType::OpCut)
{
// 客户认为, 应该仅处理被切割第一个染色体. 如果涉及到其他的染色体, 要么是手滑了, 要么是其他的原因.
auto contours = _analyzer->splitContour(_getWorkingMat(), opitems.front()->contour(), {curve_line, }, configer()->getMinChromosomeArea(), configer()->getMaxChromosomeArea() );
if(contours.empty())
{
TRACE() << "错误: 切割不出染色体";
}
else if(contours.size()==1)
{
// 修改染色体
chg_contour = contours.front();
chg_item = opitems.front();
}
else
{
// 删除并新建
rmv_items.push_back(opitems.front());
new_contours.swap(contours);
}
}

在其中,

1
2
3
4
std::vector<std::vector<cv::Point>> new_contours;   // 新增的item
std::vector<ChromosomeContourItem *> rmv_items; // 要删除的item.
std::vector<cv::Point> chg_contour; // 要修改的item的轮廓(应该只有一个)
ChromosomeContourItem * chg_item=nullptr; // 要修改的item(应该只有一个)

最后, 我们统一处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 要修改的Item
if(chg_item!=nullptr && !chg_contour.empty())
{
auto new_chrom = _analyzer->extractChromoByContour(_getExtractSrcMat(), chg_contour, configer()->getFeatherSize());
...
_undoStack->push(new ReplaceChromCommand(this, chg_item->chromosome(), new_chrom));
}
// 要增删操作的Item
else if(!rmv_items.empty() || !new_contours.empty())
{
QList<Chromosome> rmvChromList;
std::transform(rmv_items.cbegin(), rmv_items.cend(), std::back_inserter(rmvChromList), [](const ChromosomeContourItem* a){return a->chromosome(); });
QList<Chromosome> newChromList;
...
_undoStack->push(new AddDelCommand(this, newChromList, rmvChromList, _getTaskStage(), _getMouseState(), _getTaskStage(), _getMouseState(), true));
}