失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > Qt 地震剖面图(或者叫地震摆动图 波形变面积图)

Qt 地震剖面图(或者叫地震摆动图 波形变面积图)

时间:2023-03-23 12:49:08

相关推荐

Qt 地震剖面图(或者叫地震摆动图 波形变面积图)

0: 项目需求

近期项目有了新的需求, 需要根据地震数据绘制出对应图表, 关于这种图的资料比较少, 翻了不少网站, 也没找到太多有用的数据, 而关于Qt的, 更是只有一篇论文. 不过搜这么多资料也不是一无所获, 最起码知道了这种图的名字. 如标题所示, 下文统一称其为地震剖面图.

1: 图形分析:

上图是我查资料时找到的一张地震剖面图的图片, 可以看出,横轴代表通道, 纵轴代表时间, 图表中的折线按照则代表了震动的强度和方向(这一点说的可能不准确), 震动起来的部分,超出某个值的, 则将波峰染成黑色, 波谷则不做处理

2: Qt效果展示

在继续分析之前, 先看下用Qt实现的效果

3: 图形分解

如第一张图所示, 图中的折线, 坐标轴等,可以用QCustomplot来实现. 关于波峰染色, QCustomPlot在使用时, 如果设置了Brush笔刷, 那么其实就自带染色, 只不过效果可能与我们的需求有差异.

下面是一个简单的折线图, 然后加上笔刷 截图, 可以看到, 箭头指向的部分, 就是我们需要的染色效果. 但是这和我们的需求其实还是有些差距

主要有2点

1) 这里的染色是以0为基础, 颜色值从0到波形的折线图中进行填充, 并不能控制 "振幅超出某个值以后, 进行填充的效果, 并且, 地震剖面图是有很多条曲线的, 不可能都以0为起点,必然要加上偏移量显示"

2) 图形是横着的, 地震剖面图一般都是竖着, 所以这一点也需要做点调整

4: 首先尝试现有方案

QCustomPlot中, QGraph类有一个setChannelFillGraph函数, 可以将2个图层之间的部分进行填充, 下边进行测试

1) 先创建2个图层, 并设置数据看看

QVector<double> x, y, y2;for(int i=0; i<11; i++){x.push_back(i);if(i%3 == 0 || i%4 == 0 || i%8 == 0){y.push_back(25);}else{y.push_back(10);}y2.push_back(20);}//设置交互属性, 可拖动,可缩放customPlot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom);//添加图层, 并设置画笔和笔刷颜色customPlot->addGraph();customPlot->graph(0)->setPen(QPen(Qt::black));customPlot->graph(0)->setBrush(QColor(255, 0, 0, 50));//设置数据进去customPlot->graph(0)->setData(x, y);//添加图层2, 并设置画笔和笔刷颜色customPlot->addGraph();customPlot->graph(1)->setPen(QPen(Qt::red));customPlot->graph(1)->setBrush(QColor(0, 0, 255, 50));//设置数据进去customPlot->graph(1)->setData(x, y2);//图层间填充customPlot->graph(0)->setChannelFillGraph(customPlot->graph(1));//设置坐标轴范围customPlot->xAxis->setRange(0, 10);customPlot->yAxis->setRange(-10, 30);//重绘customPlot->replot();

运行起来, 结果是这样的

如图所示, 比起直接设置默认笔刷, 图层间填充确实可以实现以某条线为分界, 然后以该线为中心, 进行填充( 或许这样看和直接设置笔刷没啥区别, 但是入如果把红线的笔刷透明, 或者直接把设置图层1笔刷的代码注释掉的话, 就能看出来区别了)

// customPlot->graph(1)->setBrush(QColor(0, 0, 255, 50));

那么, 问题来了, 是不是按照这个思路下去, 把红线下边的填充去掉, 只把红线上方的部分进行填充. 就完成任务了?

我认为是不行的, 使用这种方式, 显示一条曲线, 需要2个图层, 并且数据量也是要翻倍的. 如果只显示一条线还好说, 显示的线条数量很多的话, 必然会导致卡顿.

5: 源码分析

现有代码无法满足需求, 那么就只能对QCustomPlot进行二次开发了. 想要解决这个问题的第一步, 就是要找到, 笔刷填充部分的绘制逻辑

进入到QCustomplot.cpp源码中, 查找QCPGraph的 draw函数

void QCPGraph::draw(QCPPainter *painter){if (!mKeyAxis || !mValueAxis) { qDebug() << Q_FUNC_INFO << "invalid key or value axis"; return; }if (mKeyAxis.data()->range().size() <= 0 || mDataContainer->isEmpty()) return;if (mLineStyle == lsNone && mScatterStyle.isNone()) return;QVector<QPointF> lines, scatters; // line and (if necessary) scatter pixel coordinates will be stored here while iterating over segments// loop over and draw segments of unselected/selected data:QList<QCPDataRange> selectedSegments, unselectedSegments, allSegments;getDataSegments(selectedSegments, unselectedSegments);allSegments << unselectedSegments << selectedSegments;for (int i=0; i<allSegments.size(); ++i){bool isSelectedSegment = i >= unselectedSegments.size();// get line pixel points appropriate to line style:QCPDataRange lineDataRange = isSelectedSegment ? allSegments.at(i) : allSegments.at(i).adjusted(-1, 1); // unselected segments extend lines to bordering selected data point (safe to exceed total data bounds in first/last segment, getLines takes care)getLines(&lines, lineDataRange);// check data validity if flag set:#ifdef QCUSTOMPLOT_CHECK_DATAQCPGraphDataContainer::const_iterator it;for (it = mDataContainer->constBegin(); it != mDataContainer->constEnd(); ++it){if (QCP::isInvalidData(it->key, it->value))qDebug() << Q_FUNC_INFO << "Data point at" << it->key << "invalid." << "Plottable name:" << name();}#endif//注释的还是比较清除的, 在这里进行了图形填充的绘制// draw fill of graph:if (isSelectedSegment && mSelectionDecorator)mSelectionDecorator->applyBrush(painter);elsepainter->setBrush(mBrush);painter->setPen(Qt::NoPen);drawFill(painter, &lines);// draw line:if (mLineStyle != lsNone){if (isSelectedSegment && mSelectionDecorator)mSelectionDecorator->applyPen(painter);elsepainter->setPen(mPen);painter->setBrush(Qt::NoBrush);if (mLineStyle == lsImpulse)drawImpulsePlot(painter, lines);elsedrawLinePlot(painter, lines); // also step plots can be drawn as a line plot}// draw scatters:QCPScatterStyle finalScatterStyle = mScatterStyle;if (isSelectedSegment && mSelectionDecorator)finalScatterStyle = mSelectionDecorator->getFinalScatterStyle(mScatterStyle);if (!finalScatterStyle.isNone()){getScatters(&scatters, allSegments.at(i));drawScatterPlot(painter, scatters, finalScatterStyle);}}// draw other selection decoration that isn't just line/scatter pens and brushes:if (mSelectionDecorator)mSelectionDecorator->drawDecoration(painter, selection());}

上边的代码段是QCPGraph的draw函数内容, 如注释所示, 绘制填充的地方, 在drawFill函数中

那么我们进入到drawFill函数中看一下

void QCPGraph::drawFill(QCPPainter *painter, QVector<QPointF> *lines) const{if (mLineStyle == lsImpulse) return; // fill doesn't make sense for impulse plotif (painter->brush().style() == Qt::NoBrush || painter->brush().color().alpha() == 0) return;applyFillAntialiasingHint(painter);const QVector<QCPDataRange> segments = getNonNanSegments(lines, keyAxis()->orientation());//如果没有设置与目标图层绘图的话, 就绘制一个从曲线到坐标轴的0位置的闭合图形if (!mChannelFillGraph){// draw base fill under graph, fill goes all the way to the zero-value-line:foreach (QCPDataRange segment, segments)painter->drawPolygon(getFillPolygon(lines, segment));}else //如果设置了目标图层填充, 那就绘制从当前图层到目标图层填充的闭合图形{// draw fill between this graph and mChannelFillGraph:QVector<QPointF> otherLines;mChannelFillGraph->getLines(&otherLines, QCPDataRange(0, mChannelFillGraph->dataCount()));if (!otherLines.isEmpty()){QVector<QCPDataRange> otherSegments = getNonNanSegments(&otherLines, mChannelFillGraph->keyAxis()->orientation());QVector<QPair<QCPDataRange, QCPDataRange> > segmentPairs = getOverlappingSegments(segments, lines, otherSegments, &otherLines);for (int i=0; i<segmentPairs.size(); ++i)painter->drawPolygon(getChannelFillPolygon(lines, segmentPairs.at(i).first, &otherLines, segmentPairs.at(i).second));}}}

上边的代码段我加了注释

mChannelFillGraph 变量是从哪来的, 我们可以看一下

void QCPGraph::setChannelFillGraph(QCPGraph *targetGraph){// prevent setting channel target to this graph itself:if (targetGraph == this){qDebug() << Q_FUNC_INFO << "targetGraph is this graph itself";mChannelFillGraph = nullptr;return;}// prevent setting channel target to a graph not in the plot:if (targetGraph && targetGraph->mParentPlot != mParentPlot){qDebug() << Q_FUNC_INFO << "targetGraph not in same plot";mChannelFillGraph = nullptr;return;}/* 没错, 我们之前测试的时候, 进行图层间填充, 调用了 setChannelFillGraph 函数, 而这个函数最终会修改 mChannelFillGraph 的值*/ mChannelFillGraph = targetGraph;}

如图所示, 如果我们调用了图层间填充的设置函数, drawFill函数, 就会进入到 else 选项中, 否则就是绘制从曲线到坐标轴0线的填充, painter->drawPolygon(), 这个函数是QPainter对象的绘制多边形的函数, 在这里我们暂且跳过, 我们需要关注的 是 getFillPolygon, 从名字上可以看出, 这个函数返回一个将要被填充的多边形范围

那么我们进入到getFillPolygon 函数看看

//此函数返回一个形状, 依靠这个形状进行绘图const QPolygonF QCPGraph::getFillPolygon(const QVector<QPointF> *lineData, QCPDataRange segment) const{if (segment.size() < 2)return QPolygonF();QPolygonF result(segment.size()+2);result[0] = getFillBasePoint(lineData->at(segment.begin()));std::copy(lineData->constBegin()+segment.begin(), lineData->constBegin()+segment.end(), result.begin()+1);result.push_back(getFillBasePoint(lineData->at(segment.end()-1)));result[result.size()-1] = getFillBasePoint(lineData->at(segment.end()-1));return result;}

getFillPolygon 函数, 通过传递进来的折线图的顶点列表, 再结合getFillBasePoint 函数获取基线坐标(基线坐标指的是 X轴为水平坐标轴的情况下,坐标轴 Y轴为0, X轴最左边和X轴最右边, 获取到的2个坐标). 这也就解释了为什么图形填充为什么总是填充到0

那么我们需要动的地方, 就在这里了, 有折线图的顶点坐标列表, 我们就能根据自己的需求, 实现一个我们想要的填充多边形

为了验证我们的猜想, 这里先进行一下测试, 直接返回一个固定形状, 看下是否会绘制出来

注意, 需要把 之前设置的图层间填充的代码屏蔽掉

// customPlot->graph(0)->setChannelFillGraph(customPlot->graph(1));

修改getFillPolygon函数如下

//此函数返回一个形状, 依靠这个形状进行绘图const QPolygonF QCPGraph::getFillPolygon(const QVector<QPointF> *lineData, QCPDataRange segment) const{// if (segment.size() < 2)// return QPolygonF();// QPolygonF result(segment.size()+2);// result[0] = getFillBasePoint(lineData->at(segment.begin()));// std::copy(lineData->constBegin()+segment.begin(), lineData->constBegin()+segment.end(), result.begin()+1);// result.push_back(getFillBasePoint(lineData->at(segment.end()-1)));// result[result.size()-1] = getFillBasePoint(lineData->at(segment.end()-1));// return result;//这里我们直接返回一个三角形QPolygonF result;result.append(QPointF(150, 50));result.append(QPointF(50, 150));result.append(QPointF(250, 150));return result;}

然后编译运行, 结果如下所示

可以看到, 和我们预想的一致, 确实绘制出来了一个三角形.

既然这样, 那我们完全可以按照顶点数据, 重新组组装出来一个多边形结构, 让它将超出我们设置的阈值的数据部分进行填充, 低于该值的则不填充.

理论可行, 接下来进行修改

再次回到源码部分,

//此函数返回一个形状, 依靠这个形状进行绘图const QPolygonF QCPGraph::getFillPolygon(const QVector<QPointF> *lineData, QCPDataRange segment) const{if (segment.size() < 2)return QPolygonF();QPolygonF result(segment.size()+2);result[0] = getFillBasePoint(lineData->at(segment.begin()));//这里把顶点坐标复制到了多边形顶点坐标中, 这里我们做点处理std::copy(lineData->constBegin()+segment.begin(), lineData->constBegin()+segment.end(), result.begin()+1);result.push_back(getFillBasePoint(lineData->at(segment.end()-1)));result[result.size()-1] = getFillBasePoint(lineData->at(segment.end()-1));return result;}

代码中的std::copy那一句, 把折线图的顶点坐标复制到了result容器中, result容器,正是保存填充色的容器, 我们就在复制这一步, 做点东西.

既然是超出阈值的才进行染色, 那么不难想到, 我们把数据低于阈值的, 全部设置成和阈值相等, 然后把基线也提升到和阈值相等,是不是就行了?

事实上,如果只这么操作的话, 是会有点问题的, 先看下效果

//此函数返回一个形状, 依靠这个形状进行绘图const QPolygonF QCPGraph::getFillPolygon(const QVector<QPointF> *lineData, QCPDataRange segment) const{if (segment.size() < 2)return QPolygonF();QPolygonF result(segment.size()+2);// result[0] = getFillBasePoint(lineData->at(segment.begin()));// std::copy(lineData->constBegin()+segment.begin(), lineData->constBegin()+segment.end(), result.begin()+1);//计算出染色基准线, 这里把阈值设置为20, 也就是说, Y轴数据超出20的部分进行染色double divding = 20;QPointF start((*lineData)[0].x(), mValueAxis->coordToPixel(divding));QPointF end((*lineData).last().x(), mValueAxis->coordToPixel(divding));//基线起点result[0] = start;double divdingPix = mValueAxis->coordToPixel(divding);for(int i=0; i<lineData->size(); i++){if((*lineData)[i].y() <= divdingPix) //注意, lineData里边保存的是折线图的图像坐标, 左上角是0,0, 而不是我们图形中的左下角是 0,0{result.push_back((*lineData)[i]);}else{//所有低于阈值的, 都设置成阈值result.push_back(QPointF((*lineData)[i].x(), divdingPix));}}//基线终点result[result.size()-1] = end;// result.push_back(getFillBasePoint(lineData->at(segment.end()-1)));// result[result.size()-1] = getFillBasePoint(lineData->at(segment.end()-1));return result;}

将基线位置, 和多边形生成逻辑进行修改, 最终效果如下图

可以看到, 低于阈值的数据确实没了, 但是图好像塌下来了, 其实这个也好理解, 我们把低于阈值的数据全部设置的和阈值相等, 所以只有Y轴被提上去了, 但是折线从阈值线上跨切过去的点并没有被添加到多边形顶点坐标中

所以在这之前, 还需要有一步, 就是把X轴的位置找出来, 也就是从阈值线上跨切过去的坐标

分析出来了原因, 那就继续往下走, 把这些跨切坐标找到, 并添加到多边形顶点坐标中

修改之后的代码如下所示

//此函数返回一个形状, 依靠这个形状进行绘图const QPolygonF QCPGraph::getFillPolygon(const QVector<QPointF> *lineData, QCPDataRange segment) const{//闭合区间第一个点: QPointF(40.2451,542)//使用交点检测方式插入数据if (segment.size() < 1)return QPolygonF();QPolygonF result(1);//计算出染色基准线double divding = 20;QPointF start((*lineData)[0].x(), mValueAxis->coordToPixel(divding));QPointF end((*lineData).last().x(), mValueAxis->coordToPixel(divding));//闭合区间第一个点result[0] = QPointF(start.x(), mValueAxis->coordToPixel(divding));//阈值线QLineF line1(start, end);QPointF po;int pointCount = 0;//生成一个带有跨切点的顶点坐标列表QVector<QPointF> out;for(int i=0; i<lineData->size()-1; i++){//使用QPointF类的判断线段交点的函数寻找交点QLineF line2((*lineData)[i], (*lineData)[i+1]);out.push_back((*lineData)[i]);if(line2.intersects(line1, &po)){out.push_back(po);++pointCount;}}out.push_back(lineData->last());//把缺失的最后一个点补上//这里遍历带有跨切点的像素坐标列表, 同时对数据大于限定值的数据进行限制for(int i=0; i<out.size(); i++){//这里比较的是像素, 因此坐标轴上小的值,在这里反而会比较大if(out[i].y() > start.y()){//如果数据值小于阈值线, 那就设置其和阈值线相等result.push_back(QPointF(out[i].x(), start.y()));}else{result.push_back(out[i]);}}//打印一下顶点数量和交点数量qDebug() << u8"总的点数:" << result.size() << u8"交点数量:" << pointCount;//闭合区间最后一个点result.push_back(end);return result;}

上边的代码片中, 使用QPointF类的intersects 函数, 找到了线段的交点, 然后将这个交点也加入到临时顶点坐标中. 然后对临时顶点坐标进行遍历, 并将小于阈值的数据修正到和阈值相等.

进行完了这一步之后, 输出就比较接近我们的需求了

调试输出打印:

总的点数: 19 交点数量: 7

不考虑调试输出的话, 效果似乎还不错, 但是, 调试输出显示, 顶点数量有19个, 但我们的折线图中, 其实是没这么多顶点的, 而导致顶点数增加的原因, 就在于小于阈值的点, 我们把它移动到了阈值的Y轴位置, 但这一步其实可以省略. 因为有了最左侧和最右侧的蓝色圈位置的顶点, 就已经可以决定填充色多边形的走向了. 因此, 可以把上边代码中的小于阈值部分, 移动到阈值部分的代码注释掉, 直接丢弃这个顶点坐标

//这里比较的是像素, 因此坐标轴上小的值,在这里反而会比较大if(out[i].y() > start.y()){//数据值小于阈值线, 那就完全可以丢弃了, 这里就不往顶点数据里边加了, 直接注释掉//result.push_back(QPointF(out[i].x(), start.y()));}else{result.push_back(out[i]);}

再次运行一下代码, 显示的结果一样, 但是顶点数量就少了

调试输出打印

总的点数: 14 交点数量: 7

可以看到, 相比原来的19个顶点, 现在变成了14个顶点, 这少掉的5个顶点, 其实就是小于阈值的点

这5个顶点, 从填充多边形的顶点中删除掉了, 但是由于跨切交点的存在, 所以对图形展现并没有影响, 等到数据量多起来的时候, 这一操作可以有效的省略掉大量的点.

比如我们把点数增加到100看看

总的点数: 168 交点数量: 66

总的点数: 118 交点数量: 66

仅仅100个数据点的情况下, 就少掉了50个点, 如果每一条线的点数达到几十K的时候, 这些点数对速度和内存的影响就会大起来.

写到这里, 后边其实也就没什么好说的了

把图形竖起来的话, 只需要把X轴设置成Y轴, Y轴设置成X轴就行了

customPlot->graph(0)->setKeyAxis(ui->customPlot->yAxis);customPlot->graph(0)->setValueAxis(ui->customPlot->xAxis);

然后再加个阈值变量, 基本上就完事了

6: 效果展示

最后, 在前边的代码基础上, 添加几个图层,加一些数据看下效果

改个颜色, 就得到了标题图

如果觉得《Qt 地震剖面图(或者叫地震摆动图 波形变面积图)》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。