【QCustomPlot】性能提升之修改源码(版本 V2.x.x)
说明
使用 QCustomPlot 绘图库的过程中,有时候觉得原生的功能不太够用,比如它没有曲线平滑功能;有时候又觉得更新绘图数据时逐个赋值效率太低,如果能直接操作内存就好了;还有时候希望减轻 CPU 压力,启用 GPU 加速。好在 QCustomPlot 是开源项目,源码编写十分规范,想要理解它的可视化思路不算特别困难。我在这篇随笔中总结一下常用的源码修改技巧,下面的每一个技巧都是独立的,不同技巧中添加的代码无任何依赖关系,相互之间也不会引发任何冲突,不会影响 QCustomPlot 原生的接口。示例中使用的 QCustomPlot 版本号为 2.0.1,但在更高的 2.x.x 版本中也适用。
1. 技巧一:启用 GPU 加速
这里选用 FreeGlut 库。
1.1 下载并编译 FreeGlut 库
去 https://freeglut.sourceforge.net/index.php 下载 freeglut 源码,编译出 freeglut 库,编译过程不做介绍。然后将编译出来的库以及 GL 文件夹下的五个头文件都包含进项目中,我使用的是 MSVC2015 64bit 静态库,因此在 pro/pri 文件中添加以下代码(因人而异):
HEADERS += \
$$PWD/GL/freeglut.h \
$$PWD/GL/freeglut_ext.h \
$$PWD/GL/freeglut_std.h \
$$PWD/GL/freeglut_ucall.h \
$$PWD/GL/glut.h
CONFIG(debug, debug | release) {
LIBS += -L$$PWD/lib64 -lfreeglut_staticd
LIBS += -L$$PWD/lib64 -lfreeglutd
}
CONFIG(release, debug | release) {
LIBS += -L$$PWD/lib64 -lfreeglut_static
LIBS += -L$$PWD/lib64 -lfreeglut
}
1.2 在 qcustomplot.cpp 文件中添加代码
在文件的前面几行(比如 #include "qcustomplot.h" 的后面)添加以下代码:
#define GLUT_DISABLE_ATEXIT_HACK
#include <GL/freeglut.h>
若同一个界面上有多个 QCustimPlot 窗口对象,且都开启了 GPU 加速,则在窗口切换时图形显示可能会出现错乱(被称为上下文异常),为了避免这种现象,需要在 QCPPaintBufferGlFbo::draw 函数里面添加以下代码:
/* inherits documentation from base class */
void QCPPaintBufferGlFbo::draw(QCPPainter *painter) const
{
if (!painter || !painter->isActive())
{
qDebug() << Q_FUNC_INFO << "invalid or inactive painter passed";
return;
}
if (!mGlFrameBuffer)
{
qDebug() << Q_FUNC_INFO << "OpenGL frame buffer object doesn't exist, reallocateBuffer was not called?";
return;
}
// 这个 if 语句是新添加的
if(QOpenGLContext::currentContext() != mGlContext.data())
{
mGlContext.data()->makeCurrent(mGlContext.data()->surface());
}
painter->drawImage(0, 0, mGlFrameBuffer->toImage());
}
1.3 在 pro 文件中添加代码
在 pro 文件中,添加以下代码:
QT += printsupport opengl
DEFINES += QCUSTOMPLOT_USE_OPENGL
这个 printsupport 是使用 QCustomPlot 时需要添加的,不论是否启用 GPU 加速都需要添加。后面的 opengl 则是为了启用 GPU 加速而新添的,此外,还需要使用 DEFINES 添加 QCUSTOMPLOT_USE_OPENGL 宏。
1.4 启用 GPU 加速
对 QCustomPlot 对象使用 setOpenGl() 函数设置是否启用 OpenGL,如下所示:
ui->Plot->setOpenGl(true);
可以通过 openGl() 函数的返回值判断是否成功启用了 GPU 加速:
qDebug() << "启用状态" << ui->Plot->openGl();
需要注意的是,当绘制的图形有大块填充区域,尤其是半透明的填充时,GPU 加速的效果才明显,这个时候才能减轻 CPU 压力。如果仅仅绘制一些简单的曲线图还开启 OpenGL,结果往往会适得其反,CPU 压力不减反增,有兴趣的可以进行测试,打开任务管理器观察启用前后 CPU 的占用百分比即可。
1.5 加速效果
绘制实时更新的、含有填充区域的图像,未开启 GPU 加速前的效果:

开启 GPU 加速后的效果:

以上演示例中并没有更改数据刷新频率(都为 10 ms 间隔)及数据量大小(都为 100 个点),两者仅有的差别为是否调用了 setOpenGl(true) 开启了 GPU 加速。从结果中可以看到,开启 OpenGL 后,CPU 占用率从 16%~17% 下降到 7%~8%,GPU 占用率从 0% 上升到 41%~43%,并且从视觉效果上看,刷新变得更快了,这可能是因为 CPU 被减轻了压力,单次计算后显示所需时间更短了。
2. 技巧二:添加曲线平滑功能
思路是先计算贝塞尔控制点,然后使用 QPainterPath 绘制平滑曲线,参考资料:
2.1 在 qcustomplot.h 文件中添加代码
在原生的 class QCP_LIB_DECL QCPGraph 类定义中(使用搜索功能找到对应位置)添加以下两个内容,注意 public 与 protected 限定符:
class QCP_LIB_DECL QCPGraph : public QCPAbstractPlottable1D<QCPGraphData>
{
public:
...
void setSmooth(bool smooth); // 新增内容
protected:
...
bool mSmooth; // 新增内容
}
在 qcustomplot.h 文件的末尾(#endif 的上一行)添加 SmoothCurveGenerator 类定义的代码:
class SmoothCurveGenerator
{
protected:
static QPainterPath generateSmoothCurveImp(const QVector<QPointF> &points) {
QPainterPath path;
int len = points.size();
if (len < 2) {
return path;
}
QVector<QPointF> firstControlPoints;
QVector<QPointF> secondControlPoints;
calculateControlPoints(points, &firstControlPoints, &secondControlPoints);
path.moveTo(points[0].x(), points[0].y());
// Using bezier curve to generate a smooth curve.
for (int i = 0; i < len - 1; ++i) {
path.cubicTo(firstControlPoints[i], secondControlPoints[i], points[i+1]);
}
return path;
}
public:
static QPainterPath generateSmoothCurve(const QVector<QPointF> &points) {
QPainterPath result;
int segmentStart = 0;
int i = 0;
int pointSize = points.size();
while (i < pointSize) {
if (qIsNaN(points.at(i).y()) || qIsNaN(points.at(i).x()) || qIsInf(points.at(i).y())) {
QVector<QPointF> lineData(i - segmentStart); std::copy(points.constBegin() + segmentStart, points.constBegin() + i - segmentStart, lineData.begin());
result.addPath(generateSmoothCurveImp(lineData));
segmentStart = i + 1;
}
++i;
}
QVector<QPointF> lineData(i - segmentStart); std::copy(points.constBegin() + segmentStart, points.constBegin() + i - segmentStart, lineData.begin());
result.addPath(generateSmoothCurveImp(lineData));
return result;
}
static QPainterPath generateSmoothCurve(const QPainterPath &basePath, const QVector<QPointF> &points) {
if (points.isEmpty()) return basePath;
QPainterPath path = basePath;
int len = points.size();
if (len == 1) {
path.lineTo(points.at(0));
return path;
}
QVector<QPointF> firstControlPoints;
QVector<QPointF> secondControlPoints;
calculateControlPoints(points, &firstControlPoints, &secondControlPoints);
path.lineTo(points.at(0));
for (int i = 0; i < len - 1; ++i)
path.cubicTo(firstControlPoints[i], secondControlPoints[i], points[i+1]);
return path;
}
static void calculateFirstControlPoints(double *&result, const double *rhs, int n) {
result = new double[n];
double *tmp = new double[n];
double b = 2.0;
result[0] = rhs[0] / b;
// Decomposition and forward substitution.
for (int i = 1; i < n; i++) {
tmp[i] = 1 / b;
b = (i < n - 1 ? 4.0 : 3.5) - tmp[i];
result[i] = (rhs[i] - result[i - 1]) / b;
}
for (int i = 1; i < n; i++) {
result[n - i - 1] -= tmp[n - i] * result[n - i]; // Backsubstitution.
}
delete[] tmp;
}
static void calculateControlPoints(const QVector<QPointF> &knots,
QVector<QPointF> *firstControlPoints,
QVector<QPointF> *secondControlPoints) {
int n = knots.size() - 1;
firstControlPoints->reserve(n);
secondControlPoints->reserve(n);
for (int i = 0; i < n; ++i) {
firstControlPoints->append(QPointF());
secondControlPoints->append(QPointF());
}
if (n == 1) {
// Special case: Bezier curve should be a straight line.
// P1 = (2P0 + P3) / 3
(*firstControlPoints)[0].rx() = (2 * knots[0].x() + knots[1].x()) / 3;
(*firstControlPoints)[0].ry() = (2 * knots[0].y() + knots[1].y()) / 3;
// P2 = 2P1 – P0
(*secondControlPoints)[0].rx() = 2 * (*firstControlPoints)[0].x() - knots[0].x();
(*secondControlPoints)[0].ry() = 2 * (*firstControlPoints)[0].y() - knots[0].y();
return;
}
// Calculate first Bezier control points
double *xs = nullptr;
double *ys = nullptr;
double *rhsx = new double[n]; // Right hand side vector
double *rhsy = new double[n]; // Right hand side vector
// Set right hand side values
for (int i = 1; i < n - 1; ++i) {
rhsx[i] = 4 * knots[i].x() + 2 * knots[i + 1].x();
rhsy[i] = 4 * knots[i].y() + 2 * knots[i + 1].y();
}
rhsx[0] = knots[0].x() + 2 * knots[1].x();
rhsx[n - 1] = (8 * knots[n - 1].x() + knots[n].x()) / 2.0;
rhsy[0] = knots[0].y() + 2 * knots[1].y();
rhsy[n - 1] = (8 * knots[n - 1].y() + knots[n].y()) / 2.0;
// Calculate first control points coordinates
calculateFirstControlPoints(xs, rhsx, n);
calculateFirstControlPoints(ys, rhsy, n);
// Fill output control points.
for (int i = 0; i < n; ++i) {
(*firstControlPoints)[i].rx() = xs[i];
(*firstControlPoints)[i].ry() = ys[i];
if (i < n - 1) {
(*secondControlPoints)[i].rx() = 2 * knots[i + 1].x() - xs[i + 1];
(*secondControlPoints)[i].ry() = 2 * knots[i + 1].y() - ys[i + 1];
} else {
(*secondControlPoints)[i].rx() = (knots[n].x() + xs[n - 1]) / 2;
(*secondControlPoints)[i].ry() = (knots[n].y() + ys[n - 1]) / 2;
}
}
delete xs;
delete ys;
delete[] rhsx;
delete[] rhsy;
}
};
2.2 在 qcustomplot.cpp 文件中添加代码
在原生的 QCPGraph::QCPGraph(QCPAxis *keyAxis, QCPAxis *valueAxis) 构造函数(使用搜索功能找到对应位置)实现中,添加 mSmooth 成员变量的初始化代码:
QCPGraph::QCPGraph(QCPAxis *keyAxis, QCPAxis *valueAxis) :
QCPAbstractPlottable1D<QCPGraphData>(keyAxis, valueAxis)
{
...
mSmooth = false; // 新增内容
}
在对应位置添加 QCPGraph::setSmooth() 成员函数的实现(比如写在 void QCPGraph::setAdaptiveSampling(bool enabled) 的后面):
void QCPGraph::setSmooth(bool smooth)
{
mSmooth = smooth;
}
将原生的 QCPGraph::drawLinePlot 成员函数(使用搜索功能找到对应位置)修改为如下形式,实质上只添加了个 if 语句:
void QCPGraph::drawLinePlot(QCPPainter *painter, const QVector<QPointF> &lines) const
{
if (painter->pen().style() != Qt::NoPen && painter->pen().color().alpha() != 0)
{
applyDefaultAntialiasingHint(painter);
if (mSmooth && mLineStyle == lsLine) painter->drawPath(SmoothCurveGenerator::generateSmoothCurve(lines));
else drawPolyline(painter, lines);
}
}
2.3 启用曲线平滑
对 QCPGraph 对象使用 setSmooth() 函数设置是否启用曲线平滑,如下所示:
ui->Plot->graph(0)->setSmooth(true);
2.4 平滑效果
绘制 50 个点,未启用曲线平滑时的效果:

启用曲线平滑时的效果:

3. 技巧三:导出一维绘图数据地址
3.1 一维绘图数据的内存结构
一维绘图数据都存储在 QCPDataContainer 这个类里面,绘图数据存储的容器为 QVector<DataType>,详见 qcustomplot.h 文件中 QCPDataContainer 的类定义。不同的一维绘图类型有着不同的底层数据类型:
- 对于
QCPGraph绘图类型,这个DataType为QCPGraphData,查看QCPGraphData类定义,它有且仅有两个double类型的成员变量key和value。因此QCPGraph的绘图数据被存储在一块连续的内存块中(类似于double数组),绘图数据在内存中按顺序x0-y0-x1-y1-x2-y2...这样依次排列,xi和yi分别表示第i个横轴数据和第i个纵轴数据。 - 对于
QCPCurve绘图类型,这个DataType为QCPCurveData,查看QCPCurveData类定义,它有且仅有三个double类型的成员变量t、key和value。因此QCPCurve的绘图数据在内存中按顺序t0-x0-y0-t1-x1-y1-t2-x2-y2...这样依次排列,这个t表示参数曲线对应的参变量。 - 对于
QCPBars绘图类型,这个DataType为QCPBarsData,查看QCPBarsData类定义,它有且仅有两个double类型的成员变量key和value。因此QCPBars绘图数据与QCPGraph绘图数据的内存排列方式一样。 QCPStatisticalBox与QCPFinancial这两个绘图类型就相对复杂些,但不变的是,绘图数据仍被依次存储在一块连续的内存块中,感兴趣的可以看下QCPStatisticalBoxData与QCPFinancialData的类定义。
更新一维绘图数据时,QCustomPlot 提供了一些接口,分别为:
// QCPGraph 4个接口
void setData(QSharedPointer<QCPGraphDataContainer> data)
void setData(const QVector<double> &keys, const QVector<double> &values, bool alreadySorted=false)
void addData(const QVector<double> &keys, const QVector<double> &values, bool alreadySorted=false)
void addData(double key, double value)
// QCPCurve 7个接口
void setData(QSharedPointer<QCPCurveDataContainer> data)
void setData(const QVector<double> &t, const QVector<double> &keys, const QVector<double> &values, bool alreadySorted=false)
void setData(const QVector<double> &keys, const QVector<double> &values)
void addData(const QVector<double> &t, const QVector<double> &keys, const QVector<double> &values, bool alreadySorted=false)
void addData(const QVector<double> &keys, const QVector<double> &values)
void addData(double t, double key, double value)
void addData(double key, double value)
// QCPBars 4个接口
void setData(QSharedPointer<QCPBarsDataContainer > data)
void setData(const QVector< double > &keys, const QVector<double> &values, bool alreadySorted=false)
void addData(const QVector< double > &keys, const QVector<double> &values, bool alreadySorted=false)
void addData(double key, double value)
// QCPStatisticalBox 4个接口
void setData(QSharedPointer<QCPStatisticalBoxDataContainer> data)
void setData(const QVector<double> &keys, const QVector<double> &minimum, const QVector<double> &lowerQuartile, const QVector<double> &median, const QVector<double> &upperQuartile, const QVector<double> &maximum, bool alreadySorted=false)
void addData(const QVector<double> &keys, const QVector<double> &minimum, const QVector<double> &lowerQuartile, const QVector<double> &median, const QVector<double> &upperQuartile, const QVector<double> &maximum, bool alreadySorted=false)
void addData(double key, double minimum, double lowerQuartile, double median, double upperQuartile, double maximum, const QVector<double> &outliers=QVector<double>())
// QCPFinancial 4个接口
void setData(QSharedPointer<QCPFinancialDataContainer> data)
void setData(const QVector<double> &keys, const QVector<double> &open, const QVector<double> &high, const QVector<double> &low, const QVector<double> &close, bool alreadySorted=false)
void addData(const QVector<double> &keys, const QVector<double> &open, const QVector<double> &high, const QVector<double> &low, const QVector<double> &close, bool alreadySorted=false)
void addData(double key, double open, double high, double low, double close)
其中第一个接口暴露出来的指针并没有直接指向绘图数据所在内存的首地址,也无法通过这个指针来获得 QVector<DataType> 这个容器的地址。除第一个接口外,原生的 setData() 与 addData() 接口内部都会调用 QVector 相关的 resize()、size()、std::sort()、std::inplace_merge() 等函数,还存在很多 if 语句。在一些时候,特别是数据点数固定但数值更新速率很高时,频繁的调用 size() 等函数会大大延长刷新时间,此时原生接口中的很多操作都是不必要的,因此不妨直接将存储绘图数据的 QVector<DataType> 容器地址交给使用者,以获得更佳的性能,缩短更新时间。
3.2 在 qcustomplot.h 文件中添加代码
在 QCPDataContainer 类定义的 public 区域,添加以下一行代码即可:
template <class DataType>
class QCPDataContainer // no QCP_LIB_DECL, template class ends up in header (cpp included below)
{
public:
...
// 新添内容
QVector<DataType>* coreData() { return &mData; }
}
3.3 使用绘图数据地址来更新数据
对相应的绘图对象使用 coreData() 函数获得绘图数据的地址,如下所示:
QVector<QCPGraphData> *mData = ui->Plot->graph(0)->data()->coreData();
得到这个地址后,就可以用数组访问的方式逐点更新数据,或者使用 memcpy() 做一次更新。后面绘图时会默认数据已经排好了序,不会再进行排序操作,因此若需要重排数据顺序,需人工提前排好。
// 可能需要预分配容器内存,预分配内存仅需一次
mData->reserve(totalSize);
mData->resize(totalSize);
// 逐点更新 xi = 5.0;
(*mData)[i].key = 5.0;
// 逐点更新 yi = sin(5.0);
(*mData)[i].value = sin(5.0);
// 一次更新
memcpy((char*)mData, (char*)pData, sizeof(double)*totalSize*2);
注意:使用 memcpy() 一次更新时,这个 pData 为存储新数据的内存首地址,pData 所指空间中数据的排列方式必须和对应绘图数据的内存排列方式保持一致。
4. 技巧四:导出 QCPColorMap 绘图数据地址
4.1 QCPColorMap 绘图数据的内存结构
QCPColorMap 绘图数据存储在 QCPColorMapData 这个类里面,详见 qcustomplot.h 文件中 QCPColorMapData 的类定义,绘图数据存储的容器为一维 double 数组,按行进行存储,纵坐标小的排在数组前面。纵坐标最小的一行排在数组最前面,纵坐标最大的一行排在数组最后面;存储每行时,横坐标最小的排在数组前面,横坐标最大的排在数组后面。QCustomPlot 提供的数据更新接口有:
// QCPColorMapData
void setData(double key, double value, double z)
void setCell(int keyIndex, int valueIndex, double z)
void fill(double z)
// QCPColorMap
void setData(QCPColorMapData *data, bool copy=false)
同样在数据点数固定但数值更新速率很高时,原生接口中的很多操作都是不必要的。
4.2 在 qcustomplot.h 文件中添加代码
在 QCPColorMapData 类定义的 public 区域,添加以下一行代码即可:
class QCP_LIB_DECL QCPColorMapData
{
public:
...
// 新添内容
double *coreData() { mDataModified = true; return mData; }
}
4.3 使用绘图数据地址来更新数据
对 QCPColorMap 对象使用 coreData() 函数获得绘图数据的地址,如下所示:
double *mData = m_pColorMap->data()->coreData();
得到这个地址后,就可以用数组访问的方式逐点更新数据,或者使用 memcpy() 做一次更新。
// 不要在外部使用 new 来分配内存,而应使用原生接口来做内存预分配
m_pColorMap->data()->setSize(xsize, ysize);
// 逐点更新 m[xi][yj] = 5.0; 其中 xi,yj 为非负整型索引值
mData[(yj-1)*xsize+xi] = 5.0;
// 一次更新
memcpy((char*)mData, (char*)pData, sizeof(double)*xsize*ysize);
注意:使用 memcpy() 一次更新时,这个 pData 为存储新数据的内存首地址,pData 所指空间中数据的排列方式必须和 QCPColorMap 绘图数据的内存排列方式保持一致。
【QCustomPlot】性能提升之修改源码(版本 V2.x.x)的更多相关文章
- Windows7 64位环境6sv2.1大气传输模型修改源码添加国产高分卫星GF-1 GF-2光谱响应支持
下面开始添加国产卫星光谱响应的支持: 以下主要参考文章“6S大气传输模型修改源码添加.自定义卫星光谱响应(以HJ-1B CCD为例)”网址:http://blog.csdn.net/sam92/art ...
- element-ui 修改源码实践 --tranfer
1.element-ui 地址:https://github.com/ElemeFE/element 2.修改elelment-ui版本:2.2.2(请选择和项目相对应的版本) 3.修改内容:穿梭框组 ...
- Hadoop 修改源码以及将修改后的源码应用到部署好的Hadoop中
我的Hadoop版本是hadoop-2.7.3, 我们可以去hadoop官网下载源码hadoop-2.7.3-src,以及编译好的工程文件hadoop-2.7.3, 后者可以直接部署. 前者hadoo ...
- postgres外部表如何修改源码适配pg升级
postgres中外部表的应用如下: 但是许多在github上的fdw开源代码都是基于9.3以及9.4版本开发,原作者没有随着pg的版本升级而将外部表扩展升级,那只能靠自己去手动修改源码来让这些扩展能 ...
- Sentinel控制台1.8.3修改源码,修改配置后推送到Nacos
目录 1. 接着上一篇 2. 思路 3. 下载Sentinel源码 4. 看Gateway里面读取的配置信息 5. 修改Sentinel控制台源码 6. 熔断规则测试 7. 限流规则测试 8. 打包使 ...
- Android5.1.1 - APK签名校验分析和修改源码绕过签名校验
Android5.1.1 - APK签名校验分析和修改源码绕过签名校验 作者:寻禹@阿里聚安全 APK签名校验分析 找到PackageParser类,该类在文件“frameworks/base/cor ...
- zookeeper-如何修改源码-《每日五分钟搞定大数据》
本篇文章仅仅是起一个抛砖迎玉的作用,举一个如何修改源码的例子.文章的灵感来自 ZOOKEEPER-2784. 提一个问题先 之前的文章讲过zxid的设计,我们先复习下: zxid有64位,分成两部分: ...
- Python paramiko 修改源码实现用户命令抓取
paramiko 源码修改 paramiko主要用来实现ssh客户端.服务端链接,上一节我们说到了堡垒机,堡垒机内有一个需求是“用户行为审计”,在这里我们就可以通过修改paramiko内文件的源码来实 ...
- XFF的学习+修改源码--Are you in class
这几天有做天枢CTF的“Are you in class”的题目,虽然以前了解过XFF,但还是没有很好地应用,而且最后居然掉进了一个大坑,且听我细细讲来. 打开题目,首先有个提示“在不在学校主要看 ...
- solr-用mmseg4j配置同义词索引和检索(IKanlyzer需要修改源码适应solr接口才能使用同义词功能)
概念说明:同义词大体的意思是指,当用户输入一个词时,solr会把相关有相同意思的近义词的或同义词的term的语段内容从索引中取出,展示给用户,提高交互的友好性(当然这些同义词的定义是要在配置文件中事先 ...
随机推荐
- Java面试——JVM知识
一.什么情况下会发生栈内存溢出 [1]线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常.递归的调用一个简单的方法,不断累积就会抛出 StackOverflow ...
- [整理] FFmpeg官方文档树
扫了一遍官方文档,整理张官文树. 当然还有很多细节,可以慢慢沿着树根填,有需要可以联系我要ProcessON源文件,我尽量给个最新的出来. 官文 : http://ffmpeg.org/documen ...
- kubernetes(k8s) 安装 Prometheus + Grafana
kubernetes(k8s) 安装 Prometheus + Grafana 组件说明 MetricServer:是kubernetes集群资源使用情况的聚合器,收集数据给kubernetes集群内 ...
- 在 k8s(kubernetes)中使用 Loki 进行日志监控
安装helm环境 [root@hello ~/yaml]# [root@hello ~/yaml]# curl https://baltocdn.com/helm/signing.asc | sudo ...
- 生产事故-记一次特殊的OOM排查
入职多年,面对生产环境,尽管都是小心翼翼,慎之又慎,还是难免捅出篓子.轻则满头大汗,面红耳赤.重则系统停摆,损失资金.每一个生产事故的背后,都是宝贵的经验和教训,都是项目成员的血泪史.为了更好地防范和 ...
- pandas安装1
Python 官方标准发行版并没有自带 Pandas 库,因此需要另行安装.除了标准发行版外,还有一些第三方机构发布的 Python 免费发行版, 它们在官方版本的基础上开发而来,并有针对性的提前安装 ...
- pnpm 之降本增效
作者:京东科技 于振京 受众简介 前端研发工程师 还在为npm i安装大量依赖等待时间较长,npm扁平化node_modules依赖版本冲突在苦恼吗,不用苦恼pnpm为你保驾护航 主要影响:安装依赖包 ...
- MySQL(七)索引
索引的数据结构 1 为什么使用索引 索引概述 索引(Index)是帮助MySQL高效获取数据的数据结构.是"排好序的快速查找结构",满足特定的查找算法 索引是在存储引擎中实现的,每 ...
- YOLO1论文中文版
文章目录 YOLO1中文版 摘要 1. 引言 2. 统一检测 2.1 网络设计 2.2 训练 2.3 推断 2.4 YOLO的限制 3. 与其它检测系统的比较 4. 实验 4. 1 与其它实时系统的比 ...
- 数据分析01-(numpy概述及使用)
数据分析-01 数据分析 numpy numpy概述 numpy`历史` numpy的核心:多维数组 numpy基础 ndarray数组 内存中的ndarray对象 ndarray数组对象的特点 nd ...