2024年3月

前言

最近我写的几篇线上问题相关的文章:《
糟糕,CPU100%了
》《
如何防止被恶意刷接口
》《
我调用第三方接口遇到的13大坑
》发表之后,在全网广受好评。

今天接着线上问题这个话题,跟大家一起聊聊线上服务出现OOM问题的6种场景,希望对你会有所帮助。

1 堆内存OOM

堆内存OOM是最常见的OOM了。

出现堆内存OOM问题的异常信息如下:

java.lang.OutOfMemoryError: Java heap space

此OOM是由于JVM中heap的最大值,已经不能满足需求了。

举个例子:

public class HeapOOMTest {

    public static void main(String[] args) {
        List<HeapOOMTest> list = Lists.newArrayList();
        while (true) {
            list.add(new HeapOOMTest());
        }
    }
}

这里创建了一个list集合,在一个死循环中不停往里面添加对象。

执行结果:

出现了java.lang.OutOfMemoryError: Java heap space的堆内存溢出。

很多时候,excel一次导出大量的数据,获取在程序中一次性查询的数据太多,都可能会出现这种OOM问题。

我们在日常工作中一定要避免这种情况。

2 栈内存OOM

有时候,我们的业务系统创建了太多的线程,可能会导致栈内存OOM。

出现堆内存OOM问题的异常信息如下:

java.lang.OutOfMemoryError: unable to create new native thread

给大家举个例子:

public class StackOOMTest {
    public static void main(String[] args) {
        while (true) {
            new Thread().start();
        }
    }
}

使用一个死循环不停创建线程,导致系统产生了大量的线程。

执行结果:

如果实际工作中,出现这个问题,一般是由于创建的线程太多,或者设置的单个线程占用内存空间太大导致的。

建议在日常工作中,多用线程池,少自己创建线程,防止出现这个OOM。

3 栈内存溢出

我们在业务代码中可能会经常写一些
递归
调用,如果递归的深度超过了JVM允许的最大深度,可能会出现栈内存溢出问题。

出现栈内存溢出问题的异常信息如下:

java.lang.StackOverflowError

例如:

public class StackFlowTest {
    public static void main(String[] args) {
        doSamething();
    }

    private static void doSamething() {
        doSamething();
    }
}

执行结果:

出现了java.lang.StackOverflowError栈溢出的错误。

我们在写递归代码时,一定要考虑递归深度。即使是使用parentId一层层往上找的逻辑,也最好加一个参数控制递归深度。防止因为数据问题导致无限递归的情况,比如:id和parentId的值相等。

4 直接内存OOM

直接内存
不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。

它来源于
NIO
,通过存在堆中的
DirectByteBuffer
操作Native内存,是属于
堆外内存
,可以直接向系统申请的内存空间。

出现直接内存OOM问题时异常信息如下:

java.lang.OutOfMemoryError: Direct buffer memory

例如下面这样的:

public class DirectOOMTest {

    private static final int BUFFER = 1024 * 1024 * 20;

    public static void main(String[] args) {
        ArrayList<ByteBuffer> list = new ArrayList<>();
        int count = 0;
        try {
            while (true) {
                // 使用直接内存
                ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);
                list.add(byteBuffer);
                count++;
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } finally {
            System.out.println(count);
        }
    }
}

执行结果:

会看到报出来java.lang.OutOfMemoryError: Direct buffer memory直接内存空间不足的异常。

5 GC OOM

GC OOM
是由于JVM在GC时,对象过多,导致内存溢出,建议调整GC的策略。

出现GC OOM问题时异常信息如下:

java.lang.OutOfMemoryError: GC overhead limit exceeded

为了方便测试,我先将idea中的最大和最小堆大小都设置成10M:

-Xmx10m -Xms10m

例如下面这个例子:

public class GCOverheadOOM {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(5);
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            executor.execute(() -> {
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                }
            });
        }
    }
}

执行结果:

出现这个问题是由于JVM在GC的时候,对象太多,就会报这个错误。

我们需要改变GC的策略。

在老代80%时就是开始GC,并且将-XX:SurvivorRatio(-XX:SurvivorRatio=8)和-XX:NewRatio(-XX:NewRatio=4)设置的更合理。

最近就业形式比较困难,为了感谢各位小伙伴对苏三一直以来的支持,我特地创建了一些工作内推群, 看看能不能帮助到大家。

你可以在群里发布招聘信息,也可以内推工作,也可以在群里投递简历找工作,也可以在群里交流面试或者工作的话题。

进群方式

添加苏三的私人微信:su_san_java,备注:博客园+所在城市,即可加入。

6 元空间OOM

JDK8
之后使用
Metaspace
来代替
永久代
,Metaspace是方法区在
HotSpot
中的实现。

Metaspace不在虚拟机内存中,而是使用本地内存也就是在JDK8中的
ClassMetadata
,被存储在叫做Metaspace的native memory。

出现元空间OOM问题时异常信息如下:

java.lang.OutOfMemoryError: Metaspace

为了方便测试,我修改一下idea中的JVM参数,增加下面的配置:

-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m

指定了元空间和最大元空间都是10M。

接下来,看看下面这个例子:

public class MetaspaceOOMTest {
    static class OOM {
    }

    public static void main(String[] args) {
        int i = 0;
        try {
            while (true) {
                i++;
                Enhancer enhancer = new Enhancer();
                enhancer.setSuperclass(OOM.class);
                enhancer.setUseCache(false);
                enhancer.setCallback(new MethodInterceptor() {
                    @Override
                    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                        return methodProxy.invokeSuper(o, args);
                    }
                });
                enhancer.create();
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
}

执行结果:

程序最后会报java.lang.OutOfMemoryError: Metaspace的元空间OOM。

这个问题一般是由于加载到内存中的类太多,或者类的体积太大导致的。

好了,今天的内容先分享到这里,下一篇文章重点给大家讲讲,如何用工具定位OOM问题,敬请期待。

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,您的支持是我坚持写作最大的动力。
求一键三连:点赞、转发、在看。
关注公众号:【苏三说技术】,在公众号中回复:面试、代码神器、开发手册、时间管理有超赞的粉丝福利,另外回复:加群,可以跟很多BAT大厂的前辈交流和学习。

Qt 是一个跨平台C++图形界面开发库,利用Qt可以快速开发跨平台窗体应用程序,在Qt中我们可以通过拖拽的方式将不同组件放到指定的位置,实现图形化开发极大的方便了开发效率,本章将重点介绍如何运用
QTcpSocket
组件实现基于TCP的网络通信功能。

QTcpSocket

QTcpServer
是Qt中用于实现基于TCP(Transmission Control Protocol)通信的两个关键类。TCP是一种面向连接的协议,它提供可靠的、双向的、面向字节流的通信。这两个类允许Qt应用程序在网络上建立客户端和服务器之间的连接。

以下是
QTcpSocket
类的一些常用函数:

函数 描述
QTcpSocket() 构造函数,创建一个新的
QTcpSocket
对象。
~QTcpSocket() 析构函数,释放
QTcpSocket
对象及其资源。
void connectToHost(const QString &hostName, quint16 port) 尝试与指定主机名和端口建立连接。
void disconnectFromHost() 断开与主机的连接。
QAbstractSocket::SocketState state() const 返回套接字的当前状态。
QHostAddress peerAddress() const 返回与套接字连接的远程主机的地址。
quint16 peerPort() const 返回与套接字连接的远程主机的端口。
QAbstractSocket::SocketError error() const 返回套接字的当前错误代码。
qint64 write(const char *data, qint64 maxSize) 将数据写入套接字,返回实际写入的字节数。
qint64 read(char *data, qint64 maxSize) 从套接字读取数据,返回实际读取的字节数。
void readyRead() 当套接字有可供读取的新数据时发出信号。
void bytesWritten(qint64 bytes) 当套接字已经写入指定字节数的数据时发出信号。
void error(QAbstractSocket::SocketError socketError) 当套接字发生错误时发出信号。

以下是
QTcpServer
类的一些常用函数及其简要解释:

函数 描述
QTcpServer() 构造函数,创建一个新的
QTcpServer
对象。
~QTcpServer() 析构函数,释放
QTcpServer
对象及其资源。
bool listen(const QHostAddress &address = QHostAddress::Any, quint16 port = 0) 开始监听指定的地址和端口。
void close() 停止监听并关闭服务器。
bool isListening() const 返回服务器是否正在监听连接。
QList<QTcpSocket*> pendingConnections() 返回等待处理的挂起连接的列表。
virtual void incomingConnection(qintptr socketDescriptor) 当有新连接时调用,可以在子类中实现以处理新连接。
void maxPendingConnections() const 返回允许的最大挂起连接数。
void setMaxPendingConnections(int numConnections) 设置允许的最大挂起连接数。
QNetworkProxy proxy() const 返回服务器的代理设置。
void setProxy(const QNetworkProxy &networkProxy) 设置服务器的代理设置。
QAbstractSocket::SocketError serverError() const 返回服务器的当前错误代码。
QString errorString() const 返回服务器的错误消息字符串。
void pauseAccepting() 暂停接受新连接,但保持现有连接。
void resumeAccepting() 恢复接受新连接。
void close() 关闭服务器。

如上这些只是常用函数的简要描述,详细的函数说明和用法可以参考Qt官方文档或相关文档。

1.1 通信的流程

1.1.1 服务端流程

在使用TCP通信时同样需要导入
Qt+=network
模块,并在头文件中引入
QTcpServer

QTcpSocket
两个模块,当有了模块的支持,接着就是侦听套接字,此处可通过调用
server.listen
来实现侦听,此函数原型如下;

bool QTcpServer::listen(
    const QHostAddress &address = QHostAddress::Any, 
    quint16 port = 0
);

这个函数用于开始在指定的地址和端口上监听连接。它的参数包括:

  • address
    :一个
    QHostAddress
    对象,指定要监听的主机地址。默认为
    QHostAddress::Any
    ,表示监听所有可用的网络接口。
  • port
    :一个
    quint16
    类型的端口号,指定要监听的端口。如果设置为0,系统将选择一个可用的未使用端口。

函数返回一个
bool
值,表示是否成功开始监听。如果成功返回
true
,否则返回
false
,并且可以通过调用
errorString()
获取错误消息。

紧随套接字侦听其后,通过使用一个
waitForNewConnection
等待新的连接到达。它的原型如下:

bool QTcpServer::waitForNewConnection(
    int msec = 0, 
    bool *timedOut = nullptr
);

该函数在服务器接受新连接之前会一直阻塞。参数包括:

  • msec
    :等待连接的超时时间(以毫秒为单位)。如果设置为0(默认值),则表示无限期等待,直到有新连接到达。
  • timedOut
    :一个可选的布尔指针,用于指示等待是否超时。如果传递了此参数,并且等待时间达到了指定的超时时间,
    *timedOut
    将被设置为
    true
    ,否则为
    false
    。如果不关心超时,可以将此参数设置为
    nullptr

函数返回一个布尔值,表示是否成功等待新连接。如果在超时时间内有新连接到达,返回
true
,否则返回
false
。如果等待超时,可以通过检查
timedOut
参数来确定。如果函数返回
false
,可以通过调用
errorString()
获取错误消息。

套接字的接收会使用
nextPendingConnection()
函数来实现,
nextPendingConnection

QTcpServer
类的成员函数,用于获取下一个已接受的连接的套接字(
QTcpSocket
)。它的原型如下:

QTcpSocket *QTcpServer::nextPendingConnection();

函数返回一个指向新连接套接字的指针。如果没有已接受的连接,则返回
nullptr

使用这个函数,你可以在服务器接受连接之后获取相应的套接字,以便进行数据传输和通信。一般来说,在收到
newConnection
信号后,你可以调用这个函数来获取新连接的套接字。

当有了套接字以后,就可以通过
QTcpServer
指针判断对应的套接字状态,一般套接字的状态被定义在
QAbstractSocket
类内。以下是
QAbstractSocket
类中定义的一些状态及其对应的标志:

状态标志 描述
UnconnectedState 未连接状态,套接字没有连接到远程主机。
HostLookupState 正在查找主机地址状态,套接字正在解析主机名。
ConnectingState 连接中状态,套接字正在尝试与远程主机建立连接。
ConnectedState 已连接状态,套接字已经成功连接到远程主机。
BoundState 已绑定状态,套接字已经与地址和端口绑定。
ClosingState 关闭中状态,套接字正在关闭连接。
ListeningState 监听中状态,用于
QTcpServer
,表示服务器正在监听连接。

这些状态反映了套接字在不同阶段的连接和通信状态。在实际使用中,可以通过调用
state()
函数获取当前套接字的状态,并根据需要处理相应的状态。例如,可以使用信号和槽机制来捕获状态变化,以便在连接建立或断开时执行相应的操作。

当套接字被连接后则可以通过
socket->write()
方法向上线客户端发送一个字符串,此处我们以发送
lyshark
为例,发送时需要向
write()
中传入两个参数。其原型如下:

qint64 QTcpSocket::write(const char *data, qint64 maxSize);

该函数接受两个参数:

  • data
    :指向要写入套接字的数据的指针。
  • maxSize
    :要写入的数据的最大字节数。

函数返回实际写入的字节数,如果发生错误,则返回 -1。在写入数据之后,可以使用
bytesWritten
信号来获取写入的字节数。此外,你也可以使用
waitForBytesWritten
函数来阻塞等待直到所有数据都被写入。

至此服务端代码可总结为如下案例;

#include <QCoreApplication>
#include <QTcpServer>
#include <QTcpSocket>
#include <iostream>

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    QTcpServer server;

    server.listen(QHostAddress::Any,9000);
    server.waitForNewConnection(100000);

    QTcpSocket *socket;

    socket = server.nextPendingConnection();
    if(socket->state() && QAbstractSocket::ConnectedState)
    {
        QByteArray bytes = QString("lyshark").toUtf8();
        socket->write(bytes.data(),bytes.length());
    }

    socket->close();
    server.close();
    return a.exec();
}

1.1.2 客户端流程

客户端的流程与服务端基本保持一致,唯一的区别在于将
server.listen
更换为
socket.connectToHost
连接到对应的主机,
QTcpSocket

connectToHost
函数的原型如下:

void QTcpSocket::connectToHost(
const QString &hostName, 
quint16 port, 
OpenMode openMode = ReadWrite
);
  • hostName
    :远程主机的主机名或IP地址。
  • port
    :要连接的端口号。
  • openMode
    :套接字的打开模式,默认为
    ReadWrite

函数用于初始化与指定远程主机和端口的连接。在实际使用中,你可以通过调用这个函数来发起与目标主机的连接尝试。

读取数据时可以使用
readAll
函数来实现,
socket.readAll()

QTcpSocket
类的成员函数,用于读取所有可用的数据并返回一个
QByteArray
对象。其函数函数原型如下:

QByteArray QTcpSocket::readAll();

该函数返回一个包含从套接字中读取的所有数据的
QByteArray
对象。通常,你可以通过这个函数来获取已经到达的所有数据,然后对这些数据进行进一步的处理。其客户端功能如下所示;

#include <QCoreApplication>
#include <QTcpServer>
#include <QTcpSocket>
#include <iostream>

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    QTcpSocket socket;
    socket.connectToHost(QHostAddress::LocalHost,9000);

    if(socket.state() && QAbstractSocket::ConnectedState)
    {
        socket.waitForReadyRead(10000);

        QByteArray ref = socket.readAll();

        QString ref_string;

        ref_string.prepend(ref);

        std::cout << ref_string.toStdString() << std::endl;
    }

    socket.close();
    return a.exec();
}

1.2 图形化应用

1.2.1 服务端流程

与命令行版本的网络通信不同,图形化部分需要使用信号与槽函数进行绑定,所有的通信流程都是基于信号的,对于服务端而言我们需要导入
QTcpServer

QtNetwork

QTcpSocket
模块,并新增四个槽函数分别对应四个信号;

信号 槽函数 描述
connected() onClientConnected()
tcpSocket
成功连接到远程主机时触发,执行
onClientConnected()
函数。
disconnected() onClientDisconnected()
tcpSocket
断开连接时触发,执行
onClientDisconnected()
函数。
stateChanged(QAbstractSocket::SocketState) onSocketStateChange(QAbstractSocket::SocketState)
tcpSocket
的状态发生变化时触发,执行
onSocketStateChange()
函数,传递新的状态。
readyRead() onSocketReadyRead()
tcpSocket
有可读取的新数据时触发,执行
onSocketReadyRead()
函数。

在程序入口处我们通过
new QTcpServer(this)
新建TCP套接字类,并通过
connect()
连接到初始化槽函数上,当程序运行后会首先触发
newConnection
信号,执行
onNewConnection
槽函数。

MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 新建TCP套接字类
    tcpServer=new QTcpServer(this);

    // 连接信号初始化其他信号
    connect(tcpServer,SIGNAL(newConnection()),this,SLOT(onNewConnection()));
}

而在槽函数
onNewConnection
中,通过
nextPendingConnection
新建一个套接字,并绑定其他四个槽函数,这里的槽函数功能各不相同,将其对应的信号绑定到对应槽函数上即可;

// 初始化信号槽函数
void MainWindow::onNewConnection()
{
    // 创建新套接字
    tcpSocket = tcpServer->nextPendingConnection();

    // 连接触发信号
    connect(tcpSocket, SIGNAL(connected()),this, SLOT(onClientConnected()));
    onClientConnected();

    // 关闭触发信号
    connect(tcpSocket, SIGNAL(disconnected()),this, SLOT(onClientDisconnected()));

    // 状态改变触发信号
    connect(tcpSocket,SIGNAL(stateChanged(QAbstractSocket::SocketState)),this,SLOT(onSocketStateChange(QAbstractSocket::SocketState)));
    onSocketStateChange(tcpSocket->state());

    // 读入数据触发信号
    connect(tcpSocket,SIGNAL(readyRead()),this,SLOT(onSocketReadyRead()));
}

当读者点击侦听时则直接调用
tcpServer->listen
实现对本地IP的
8888
端口的侦听功能,停止侦听则是调用
tcpServer->close
函数实现,如下所示;

// 开始侦听
void MainWindow::on_pushButton_2_clicked()
{
    // 此处指定绑定本机的8888端口
    tcpServer->listen(QHostAddress::LocalHost,8888);
    ui->plainTextEdit->appendPlainText("[+] 开始监听");
    ui->plainTextEdit->appendPlainText(" 服务器地址:" + tcpServer->serverAddress().toString() +
                                       " 服务器端口:"+QString::number(tcpServer->serverPort())
                                       );
}

// 停止侦听
void MainWindow::on_pushButton_3_clicked()
{
    if (tcpServer->isListening())
    {
        tcpServer->close();
    }
}

对于读取数据可以通过
canReadLine()
函数判断行,并通过
tcpClient->readLine()
逐行读入数据,相对应的发送数据可通过调用
tcpSocket->write
函数实现,在发送之前需要将其转换为
QByteArray
类型的字符串格式,如下所示;

// 读取数据
void MainWindow::onSocketReadyRead()
{
    while(tcpSocket->canReadLine())
        ui->plainTextEdit->appendPlainText("[接收] | " + tcpSocket->readLine());
}

// 发送数据
void MainWindow::on_pushButton_clicked()
{
    QString  msg=ui->lineEdit->text();
    ui->plainTextEdit->appendPlainText("[发送] | " + msg);

    QByteArray str=msg.toUtf8();
    str.append('\n');
    tcpSocket->write(str);
}

运行服务端程序,并点击侦听按钮此时将会在本地的
8888
端口上启用侦听,如下图所示;

1.2.2 客户端流程

对于客户端而言同样需要绑定四个信号并对应到特定的槽函数上,其初始化部分与服务端保持一致,唯一不同的是客户端使用
connectToHost
函数链接到服务端上,断开连接时使用的是
disconnectFromHost
函数,如下所示;

// 连接服务器时触发
void MainWindow::on_pushButton_2_clicked()
{
    // 连接到8888端口
    tcpClient->connectToHost(QHostAddress::LocalHost,8888);
}

// 断开时触发
void MainWindow::on_pushButton_3_clicked()
{
    if (tcpClient->state()==QAbstractSocket::ConnectedState)
        tcpClient->disconnectFromHost();
}

此处的读取数据与服务端保持一致,发送数据时则是通过
tcpClient->write(str)
函数直接传递给客户端,代码如下所示;

// 读取数据时触发
void MainWindow::onSocketReadyRead()
{
    while(tcpClient->canReadLine())
    {
        ui->plainTextEdit->appendPlainText("[接收] | " + tcpClient->readLine());
    }
}

// 发送消息时触发
void MainWindow::on_pushButton_clicked()
{
    QString msg=ui->lineEdit->text();
    ui->plainTextEdit->appendPlainText("[发送] | " + msg);
    QByteArray str=msg.toUtf8();
    str.append('\n');
    tcpClient->write(str);
}

运行后,服务端启用侦听等待客户端连接,客户端连接后,双方则可以实现数据的收发功能,由于采用了信号机制,两者的收发并不会阻断可同时进行,如下图所示;

Supervision库是一款出色的Python计算机视觉低代码工具,其设计初衷在于为用户提供一个便捷且高效的接口,用以处理数据集以及直观地展示检测结果。Supervision库的官方开源仓库地址为:
supervision
,官方文档地址为:
supervision-doc

Supervision库需要在Python3.8及以上版本的环境下运行。如果需要支持包含OpenCV的GUI组件以支持显示图像和视频,supervision安装方式如下:

pip install supervision[desktop]

如果仅仅想部署应用,而不需要GUI界面,supervision安装方式如下:

pip install supervision

注意,由于supervision版本经常变动,所提供的接口函数会相应发生变化。

import supervision as sv
# 打印supervision的版本
sv.__version__
'0.19.0'

1 不同任务的处理

1.1 目标检测与语义分割

1.1.1 结果分析

supervision提供了多种接口来支持对目标检测或语义分割结果的分析。supervision.Detections为主流目标检测或语义分割模型的输出结果分析提供了多种接口,常用的几个接口如下:

  • from_ultralytics(Ultralytics, YOLOv8)
  • from_detectron2(Detectron2)
  • from_mmdetection(MMDetection)
  • from_yolov5(YOLOv5)
  • from_sam(Segment Anything Model)
  • from_transformers(HuggingFace Transformers)
  • from_paddledet(PaddleDetecticon)

以上接口的具体使用见:
supervision-doc-detection
。下面以YOLOv8的结果分析为例,来说明相关代码的使用,以下代码输入图片如下:

img/cat.png

import cv2
import supervision as sv
from ultralytics import YOLO

model = YOLO("yolov8n.pt")
# model = YOLO("yolov8n-seg.pt")
image = cv2.imread("img/dog.png")
results = model(image, verbose=False)[0]
# 从YOLOv8中加载数据结果
detections = sv.Detections.from_ultralytics(results)
# 查看输出结果
detections
Detections(xyxy=array([[     255.78,      432.86,      612.01,      1078.5],
       [     255.98,      263.57,      1131.1,       837.2],
       [     927.72,      143.31,      1375.6,      338.81],
       [     926.96,      142.04,      1376.7,      339.12]], dtype=float32), mask=None, confidence=array([    0.91621,     0.85567,     0.56325,     0.52481], dtype=float32), class_id=array([16,  1,  2,  7]), tracker_id=None, data={'class_name': array(['dog', 'bicycle', 'car', 'truck'], dtype='<U7')})
# 查看输出结果预测框个数
len(detections)
4
# 查看第一个框输出结果
detections[0]
Detections(xyxy=array([[     255.78,      432.86,      612.01,      1078.5]], dtype=float32), mask=None, confidence=array([    0.91621], dtype=float32), class_id=array([16]), tracker_id=None, data={'class_name': array(['dog'], dtype='<U7')})
# 查看每一个目标边界框的面积
detections.box_area
array([ 2.3001e+05,  5.0201e+05,       87569,       88643], dtype=float32)
# 可视化识别结果

# 确定可视化参数
bounding_box_annotator = sv.BoundingBoxAnnotator()
label_annotator = sv.LabelAnnotator()

labels = [
    model.model.names[class_id]
    for class_id
    in detections.class_id
]

annotated_image = bounding_box_annotator.annotate(
    scene=image, detections=detections)
annotated_image = label_annotator.annotate(
    scene=annotated_image, detections=detections, labels=labels)
sv.plot_image(annotated_image)

png

在上图标注了三个检测框,然而实际的检测结果中却包含了四个框。这是由于图中的汽车同时被识别为卡车(truck)和轿车(car)。

len(detections)
detections
Detections(xyxy=array([[     255.78,      432.86,      612.01,      1078.5],
       [     255.98,      263.57,      1131.1,       837.2],
       [     927.72,      143.31,      1375.6,      338.81],
       [     926.96,      142.04,      1376.7,      339.12]], dtype=float32), mask=None, confidence=array([    0.91621,     0.85567,     0.56325,     0.52481], dtype=float32), class_id=array([16,  1,  2,  7]), tracker_id=None, data={'class_name': array(['dog', 'bicycle', 'car', 'truck'], dtype='<U7')})
labels
['dog', 'bicycle', 'car', 'truck']

解决的办法是,对目标检测结果执行执行类无关的非最大抑制NMS,代码如下:

detections = detections.with_nms(threshold=0.5, class_agnostic=True)
# 打印输出结果
for class_id in detections.class_id:
    print(model.model.names[class_id])
dog
bicycle
car

1.1.2 辅助函数

计算Intersection over Union(IOU)

import supervision as sv
import numpy as np
box1 = np.array([[50, 50, 150, 150]])  # (x_min, y_min, x_max, y_max)
box2 = np.array([[100, 100, 200, 200]])

print(sv.box_iou_batch(box1,box2))
[[    0.14286]]

计算Non-Maximum Suppression (NMS)

import supervision as sv
box = np.array([[50, 50, 150, 150, 0.2],[100, 100, 200, 200, 0.5]])  # (x_min, y_min, x_max, y_max, score)

# 返回哪些边界框需要保存
# 参数:输入框数组和阈值
print(sv.box_non_max_suppression(box,0.1))
[False  True]

从多边形生成mask

import cv2
import supervision as sv
import numpy as np

# 多边形
vertices = np.array([(50, 50), (30, 50), (60,20), (70, 50), (90, 10)])

# 创建遮罩mask
# 参数:输入框数组和输出mask的宽高
mask = sv.polygon_to_mask(vertices, (100,60))
# mask中白色(像素值为1)表示多边形,其他区域像素值为0
sv.plot_image(mask)

# 从mask生成多边形
# vertices = sv.mask_to_polygons(mask)

png

根据面积过滤多边形

import supervision as sv
import numpy as np

# 创建包含多个多边形的示例列表
polygon1 = np.array([[0, 0], [0, 1], [1, 1], [1, 0]])
polygon2 = np.array([[0, 0], [0, 2], [2, 2], [2, 0]])
polygon3 = np.array([[0, 0], [0, 3], [3, 3], [3, 0]])

polygons = [polygon1, polygon2, polygon3]

# 参数:输入多边形列表,面积最小值,面积最大值(为None表示无最大值限制)
filtered_polygons = sv.filter_polygons_by_area(polygons, 2.5, None)

print("原始多边形数组个数:", len(polygons))
print("筛选后的多边形数组:", len(filtered_polygons))
原始多边形数组个数: 3
筛选后的多边形数组: 2

缩放边界框

import numpy as np
import supervision as sv

boxes = np.array([[10, 10, 20, 20], [30, 30, 40, 40]])
# 表示按比例缩放长方体尺寸的因子。大于1的因子将放大长方体,而小于1的因子将缩小长方体
factor = 1.2
scaled_bb = sv.scale_boxes(boxes, factor)
print(scaled_bb)
[[          9           9          21          21]
 [         29          29          41          41]]

1.2 目标跟踪

supervision中内置了
ByteTrack
目标跟踪器,ByteTrack与基于ReID特征进行匹配的目标跟踪方法不同,ByteTrack主要依赖目标检测器提供的目标框信息进行跟踪。因此,目标检测器的准确性和稳定性会直接影响到ByteTrack的跟踪效果。

通过supervision.ByteTrack类即可初始化ByteTrack追踪器,supervision.ByteTrack类的初始化参数如下:

  • track_thresh(float, 可选,默认0.25): 检测置信度阈值
  • track_buffer(int,可选,默认30): 轨道丢失时要缓冲的帧数。
  • match_thresh(float,可选,默认0.8): 将轨道与检测相匹配的阈值。
  • frame_rate(int,可选,默认30): 视频的帧速率。

supervision.ByteTrack类的主要类函数如下:

  • reset():重置ByteTrack跟踪器的内部状态。
  • update_with_detections(detections):使用提供的检测更新跟踪器并返回更新的检测结果,detections为supervision的目标检测结果。

示例代码如下:

import supervision as sv
from ultralytics import YOLO
import numpy as np
model = YOLO("yolov8n.pt")

# 初始化目标跟踪器
tracker = sv.ByteTrack()

bounding_box_annotator = sv.BoundingBoxAnnotator()
label_annotator = sv.LabelAnnotator()

def callback(frame: np.ndarray, index: int) -> np.ndarray:
    results = model(frame)[0]
    # 获得Detections结果
    detections = sv.Detections.from_ultralytics(results)
    # 轨迹跟踪
    detections = tracker.update_with_detections(detections)

    labels = [f"#{tracker_id}" for tracker_id in detections.tracker_id]

    annotated_frame = bounding_box_annotator.annotate(scene=frame.copy(), detections=detections)
    annotated_frame = label_annotator.annotate( scene=annotated_frame, detections=detections, labels=labels)
    return annotated_frame

sv.process_video(
    source_path="https://media.roboflow.com/supervision/video-examples/people-walking.mp4",
    # 输出结果参考:https://media.roboflow.com/supervision/video-examples/how-to/track-objects/annotate-video-with-traces.mp4
    target_path="output.mp4",
    callback=callback
)

由于示例代码用的是
yolov8n.pt
,跟踪效果会很不稳定,可以考虑使用性能更强的目标跟踪器。

1.3 图像分类

supervision支持分析clip,timm,YOLOv8分类模型结果的输出,但是功能很弱,仅支持输出top-k及概率。

以下代码输入图片如下:

img/cat.png

import cv2
from ultralytics import YOLO
import supervision as sv

# 加载图片和模型
image = cv2.imread("img/cat.png")
# 加载分类模型
model = YOLO('yolov8n-cls.pt')

output = model(image)[0]
# 将YOLO的分类输出导入supervision
classifications = sv.Classifications.from_ultralytics(output)
# 除此之外还支持from_clip和from_timm两类模型

# 打印top2,输出类别和概率
print(classifications.get_top_k(2))
0: 224x224 tiger_cat 0.29, tabby 0.23, Egyptian_cat 0.15, Siamese_cat 0.05, Pembroke 0.03, 36.7ms
Speed: 6.3ms preprocess, 36.7ms inference, 0.0ms postprocess per image at shape (1, 3, 224, 224)
(array([282, 281]), array([    0.29406,     0.22982], dtype=float32))

2 数据展示与辅助处理

2.1 颜色设置

supervision提供Color类和ColorPalette类来设置颜色(调色板)和转换颜色。具体如下:

# 获得默认颜色
import supervision as sv

# WHITE BLACK RED GREEN	BLUE YELLOW	ROBOFLOW
sv.Color.ROBOFLOW
Color(r=163, g=81, b=251)
# 获得颜色的bgr值
sv.Color(r=255, g=255, b=0).as_bgr()
# 获得rgb值
# sv.Color(r=255, g=255, b=0).as_rgb()
(0, 255, 255)
# 获得颜色的16进制值
sv.Color(r=255, g=255, b=0).as_hex()
'#ffff00'
# 基于16进制色Color对象
sv.Color.from_hex('#ff00ff')
Color(r=255, g=0, b=255)
# 返回默认调色板
sv.ColorPalette.DEFAULT
# sv.ColorPalette.ROBOFLOW
# sv.ColorPalette.LEGACY
ColorPalette(colors=[Color(r=163, g=81, b=251), Color(r=255, g=64, b=64), Color(r=255, g=161, b=160), Color(r=255, g=118, b=51), Color(r=255, g=182, b=51), Color(r=209, g=212, b=53), Color(r=76, g=251, b=18), Color(r=148, g=207, b=26), Color(r=64, g=222, b=138), Color(r=27, g=150, b=64), Color(r=0, g=214, b=193), Color(r=46, g=156, b=170), Color(r=0, g=196, b=255), Color(r=54, g=71, b=151), Color(r=102, g=117, b=255), Color(r=0, g=25, b=239), Color(r=134, g=58, b=255), Color(r=83, g=0, b=135), Color(r=205, g=58, b=255), Color(r=255, g=151, b=202), Color(r=255, g=57, b=201)])
# 返回调试第i个颜色
color_palette = sv.ColorPalette.from_hex(['#ff0000', '#00ff00', '#0000ff'])
color_palette.by_idx(1)
Color(r=0, g=255, b=0)
# 从matpotlib导入调色板
sv.ColorPalette.from_matplotlib('tab20', 5)
ColorPalette(colors=[Color(r=31, g=119, b=180), Color(r=152, g=223, b=138), Color(r=140, g=86, b=75), Color(r=199, g=199, b=199), Color(r=158, g=218, b=229)])

2.2 识别结果可视化示例

supervision提供了多种函数来对识别结果进行可视化(主要针对目标检测和目标跟踪任务)。本文主要介绍目标检测边界框的各种展示效果。关于supervision所有数据注释可视化示例函数见:
supervision-doc-annotators

以下是主要示例:

# 获得数据结果
import cv2
import supervision as sv
from ultralytics import YOLO

model = YOLO("yolov8n.pt")
image = cv2.imread("img/person.png")
results = model(image, verbose=False)[0]
# 从YOLOv8中加载数据结果
detections = sv.Detections.from_ultralytics(results)
# 查看输出结果维度
len(detections)
32

目标框绘制

import supervision as sv

# 设置边界框绘制器
# 参数:color-设置颜色,thickness-线条粗细,color_lookup-颜色映射策略/选项有INDEX、CLASS、TRACK。
bounding_box_annotator = sv.BoundingBoxAnnotator(color= sv.ColorPalette.DEFAULT, thickness = 2, color_lookup = sv.ColorLookup.CLASS)
annotated_frame = bounding_box_annotator.annotate(
    scene=image.copy(),
    detections=detections
)

sv.plot_image(annotated_frame)

png

圆角目标框绘制

# roundness-边界框边缘的圆度百分比
round_box_annotator = sv.RoundBoxAnnotator(color_lookup = sv.ColorLookup.INDEX, roundness=0.6)

annotated_frame = round_box_annotator.annotate(
    scene=image.copy(),
    detections=detections
)

sv.plot_image(annotated_frame)

png

角点边界框绘制

import supervision as sv

# corner_length-每个角线的长度,
corner_annotator = sv.BoxCornerAnnotator(corner_length=12, color=sv.Color(r=255, g=255, b=0))
annotated_frame = corner_annotator.annotate(
    scene=image.copy(),
    detections=detections
)
sv.plot_image(annotated_frame)

png

遮罩边界框绘制

# 颜色遮罩的不透明度
color_annotator = sv.ColorAnnotator(opacity=0.4)
annotated_frame = color_annotator.annotate(
    scene=image.copy(),
    detections=detections
)
sv.plot_image(annotated_frame)

png

圆形边界框绘制

circle_annotator = sv.CircleAnnotator(color=sv.Color(r=255, g=255, b=128))
annotated_frame = circle_annotator.annotate(
    scene=image.copy(),
    detections=detections
)
sv.plot_image(annotated_frame)

png

点形边界框绘制

Supervision提供DotAnnotator绘制类以在图像上的目标检测框特定位置绘制关键点,该绘制类有两个独有参数:radius(点的半径),position(点在边界框上的绘制)。position可选参数如下:

  • CENTER = "CENTER"
  • CENTER_LEFT = "CENTER_LEFT"
  • CENTER_RIGHT = "CENTER_RIGHT"
  • TOP_CENTER = "TOP_CENTER"
  • TOP_LEFT = "TOP_LEFT"
  • TOP_RIGHT = "TOP_RIGHT"
  • BOTTOM_LEFT = "BOTTOM_LEFT"
  • BOTTOM_CENTER = "BOTTOM_CENTER"
  • BOTTOM_RIGHT = "BOTTOM_RIGHT"
  • CENTER_OF_MASS = "CENTER_OF_MASS"

通过代码查看position可选参数实现如下:

for i in sv.Position:
    print(i)
dot_annotator = sv.DotAnnotator(radius=4)
annotated_frame = dot_annotator.annotate(
    scene=image.copy(),
    detections=detections
)
sv.plot_image(annotated_frame)

png

三角形边界框绘制

# base/height-三角形的宽高,position-位置
triangle_annotator = sv.TriangleAnnotator(base = 30, height = 30, position = sv.Position['TOP_CENTER'])
annotated_frame = triangle_annotator.annotate(
    scene=image.copy(),
    detections=detections
)
sv.plot_image(annotated_frame)

png

椭圆形边界框绘制

# start_angle/end_angle-椭圆开始/结束角度
ellipse_annotator = sv.EllipseAnnotator(start_angle=-45, end_angle=215)
annotated_frame = ellipse_annotator.annotate(
    scene=image.copy(),
    detections=detections
)
sv.plot_image(annotated_frame)

png

置信度边界框绘制

# 用于展示置信度百分比
# border_color-百分比条颜色
# position-位置
# width/height-百分比条宽/高
percentage_bar_annotator = sv.PercentageBarAnnotator(border_color = sv.Color(r=128, g=0, b=0), position=sv.Position['BOTTOM_CENTER'],
                                                    width = 100, height = 20)
annotated_frame = percentage_bar_annotator.annotate(
    scene=image.copy(),
    detections=detections
)
sv.plot_image(annotated_frame)

png

文字描述框绘制

# color-文字背景色,text_color-文字颜色,text_scale-文字大小
# text_position-文字位置,text_thickness-文字粗细,text_padding-文字填充距离
label_annotator = sv.LabelAnnotator(color=sv.Color(r=255, g=255, b=255),text_color=sv.Color(r=128, g=0, b=128), text_scale=2, 
                                    text_position=sv.Position.TOP_CENTER, text_thickness=2,text_padding=10)

# 获得各边界框的标签
labels = [
    model.model.names[class_id]
    for class_id
    in detections.class_id
]

annotated_frame = label_annotator.annotate(
    scene=image.copy(),
    detections=detections,
    labels=labels
)
sv.plot_image(annotated_frame)

png

像素化目标

# pixel_size-像素化的大小。
pixelate_annotator = sv.PixelateAnnotator(pixel_size=12)
annotated_frame = pixelate_annotator.annotate(
    scene=image.copy(),
    detections=detections
)
# 叠加其他边界框展示效果
annotated_frame = label_annotator.annotate(
    scene=annotated_frame.copy(),
    detections=detections,
    labels=labels
)
sv.plot_image(annotated_frame)

png

2.3 辅助函数

2.3.1 视频相关

读取视频信息

import supervision as sv
# 读取视频文件的宽度、高度、fps和总帧数。
video_info = sv.VideoInfo.from_video_path(video_path="https://media.roboflow.com/supervision/video-examples/people-walking.mp4")
video_info
VideoInfo(width=1920, height=1080, fps=25, total_frames=341)

视频读写

import supervision as sv
from tqdm import tqdm
video_path="https://media.roboflow.com/supervision/video-examples/people-walking.mp4"
video_info = sv.VideoInfo.from_video_path(video_path)
# 获取一个生成视频帧的生成器
# stride: 指示返回帧的时间间隔,默认为1
# start: 开始帧编号,默认为0
# end:结束帧编号,默认为None(一直到视频结束)
frames_generator = sv.get_video_frames_generator(source_path=video_path, stride=10, start=0, end=100)
TARGET_VIDEO_PATH = "out.avi"
# target_path保存路径
with sv.VideoSink(target_path=TARGET_VIDEO_PATH, video_info=video_info) as sink:
    for frame in tqdm(frames_generator):
        sink.write_frame(frame=frame)
10it [00:24,  2.47s/it]

fps计算

import supervision as sv

frames_generator = sv.get_video_frames_generator(source_path="https://media.roboflow.com/supervision/video-examples/people-walking.mp4")
# 初始化fps监视器
fps_monitor = sv.FPSMonitor()

for frame in frames_generator:
    # 添加时间戳
    fps_monitor.tick()
# 根据存储的时间戳计算并返回平均 FPS。
fps = fps_monitor.fps
fps
174.4186046525204

2.3.2 图像相关

# 保存图片
import supervision as sv
# 创建图像保存类
# target_dir_path-保存路径
# overwrite-是否是否覆盖保存路径,默认False
# image_name_pattern-图像文件名模式。 默认为“image_{:05d}.png”。
with sv.ImageSink(target_dir_path='output', overwrite=True, image_name_pattern= "img_{:05d}.png") as sink:
    for image in sv.get_video_frames_generator( source_path='out.avi', stride=2):
        sink.save_image(image=image)
# 根据给定的边界框裁剪图像。
import supervision as sv
import cv2
import supervision as sv
from ultralytics import YOLO

model = YOLO("yolov8n.pt")
image = cv2.imread("img/person.png")
results = model(image)[0]
# 从YOLOv8中加载数据结果
detections = sv.Detections.from_ultralytics(results)
with sv.ImageSink(target_dir_path='output') as sink:
    for xyxy in detections.xyxy:
        # 获得边界框裁剪图像
        cropped_image = sv.crop_image(image=image, xyxy=xyxy)
        sink.save_image(image=cropped_image)
0: 384x640 31 persons, 1 bird, 76.8ms
Speed: 2.5ms preprocess, 76.8ms inference, 2.7ms postprocess per image at shape (1, 3, 384, 640)

2.4 其他函数

supervision中还有其他常用类,本文将不对其进行详细介绍,具体情况如下:

3 面向实际任务的工具

3.1 越线数量统计

supversion提供了LineZone类来实现越线数量统计功能,原理很简单就是目标检测+目标跟踪,然后根据车辆的边界框中心点来判断是否穿过预设线,从而实现越线数量统计。代码如下:

import supervision as sv
from ultralytics import YOLO

model = YOLO("yolov8n.pt")
tracker = sv.ByteTrack()
frames_generator = sv.get_video_frames_generator("https://media.roboflow.com/supervision/video-examples/vehicles.mp4",start=0,end=500)
video_info = sv.VideoInfo.from_video_path("https://media.roboflow.com/supervision/video-examples/vehicles.mp4")
w = video_info.width
h = video_info.height
# 设置预设线(从左至右)
start, end = sv.Point(x=0, y=int(h/2)), sv.Point(x=w, y=int(h/2))
# 初始预线检测器
line_zone = sv.LineZone(start=start, end=end)
# 初始化可视化对象
trace_annotator = sv.TraceAnnotator()
label_annotator = sv.LabelAnnotator(text_scale=2,text_color= sv.Color.BLACK)
line_zone_annotator = sv.LineZoneAnnotator(thickness=4, text_thickness=4, text_scale=1)
with sv.ImageSink(target_dir_path='output', overwrite=False, image_name_pattern= "img_{:05d}.png") as sink:
    for frame in frames_generator:
        result = model(frame)[0]
        detections = sv.Detections.from_ultralytics(result)
        # 更新目标跟踪器
        detections = tracker.update_with_detections(detections)
        # 更新预线检测器,crossed_in是否进入结果,crossed_out是否出去结果
        crossed_in, crossed_out = line_zone.trigger(detections)
        

        # 获得各边界框的标签
        labels = [
            f"#{tracker_id} {model.model.names[class_id]}"
            for class_id, tracker_id
            in zip(detections.class_id, detections.tracker_id)
        ]
        
        # 绘制轨迹
        annotated_frame = trace_annotator.annotate(scene=frame.copy(), detections=detections)
        # 绘制标签
        annotated_frame = label_annotator.annotate(scene=annotated_frame, detections=detections, labels=labels)
        # 绘制预制线
        annotated_frame = line_zone_annotator.annotate(annotated_frame, line_counter=line_zone)
        # 数据展示
        # sv.plot_image(annotated_frame)
        # 保存可视化结果
        # sink.save_image(image=annotated_frame)

# 从外到内越线的对象数量,从内到外越线的对象数量。
print(line_zone.in_count, line_zone.out_count)
# 代码输出结果见:https://media.roboflow.com/supervision/cookbooks/count-objects-crossing-the-line-result-1280x720.mp4

3.2 对特定区域进行检测跟踪

supversion提供了PolygonZone类来对特定区域进行检测跟踪,原理很简单就是目标检测或加上目标跟踪,然后选取特定区域来判断目标是否在此区域以及统计当前区域的目标个数。代码如下:

import numpy as np
import supervision as sv

from ultralytics import YOLO

model = YOLO('yolov8n.pt')

# 视频路径
video_path = "https://media.roboflow.com/supervision/video-examples/vehicles-2.mp4"
# 查看视频信息
video_info = sv.VideoInfo.from_video_path(video_path)
print(video_info)
# 读取视频
generator = sv.get_video_frames_generator(video_path)

# 设置要监控的区域
polygons = [
  np.array([
    [718, 595],[927, 592],[851, 1062],[42, 1059]
  ]),
  np.array([
    [987, 595],[1199, 595],[1893, 1056],[1015, 1062]
  ])
]


# 设置调色盘
colors = sv.ColorPalette.DEFAULT
zones = [
    # 定义多边形区域以检测对象。
    sv.PolygonZone(
        polygon=polygon, # 输入多边形
        frame_resolution_wh=video_info.resolution_wh # 全图尺寸
    )
    for polygon in polygons
]
# 初始化可视化对象
zone_annotators = [
    # 对不同监控区域分开进行可视化
    sv.PolygonZoneAnnotator(
        zone=zone,
        color=colors.by_idx(index), # 颜色
        thickness=4, # 线宽
        text_thickness=8, # 文本粗细
        text_scale=4, # 文本比例
        display_in_zone_count=False # 是否展示目标统计个数
    )
    for index, zone in enumerate(zones)
]
# 分开为检测区域定义不同的边界框展示
box_annotators = [
    sv.BoxAnnotator(
        color=colors.by_idx(index),
        thickness=4,
        text_thickness=4,
        text_scale=2
        )
    for index in range(len(polygons))
]

with sv.ImageSink(target_dir_path='output', overwrite=False, image_name_pattern= "img_{:05d}.png") as sink:
    for frame in generator:
        # 为提高识别精度,需要设置模型各大的输入尺寸
        results = model(frame, imgsz=1280, verbose=False)[0]
        detections = sv.Detections.from_ultralytics(results)

        for zone, zone_annotator, box_annotator in zip(zones, zone_annotators, box_annotators):
            # 确定哪些目标检测结果位于多边形区域
            mask = zone.trigger(detections=detections)
            detections_filtered = detections[mask]
            frame = box_annotator.annotate(scene=frame, detections=detections_filtered)
            frame = zone_annotator.annotate(scene=frame)
        # 数据展示
        sv.plot_image(frame, (16, 16))
        # 保存可视化结果
        # sink.save_image(image=annotated_frame)
# 代码输出结果见:https://blog.roboflow.com/content/media/2023/03/trim-counting.mp4

3.3 切片推理

supervision支持对图片进行切片推理以优化小目标识别,即基于SAHI(Slicing Aided Hyper Inference,切片辅助超推理)通过图像切片的方式来检测小目标。SAHI检测过程可以描述为:通过滑动窗口将图像切分成若干区域,各个区域分别进行预测,同时也对整张图片进行推理。然后将各个区域的预测结果和整张图片的预测结果合并,最后用NMS(非极大值抑制)进行过滤。SAHI的具体使用见:
基于切片辅助超推理库SAHI优化小目标识别

supervision通过SAHI进行切片推理的示例代码如下所示:

import cv2
import supervision as sv
from ultralytics import YOLO
import numpy as np

model = YOLO("yolov8n.pt")
image = cv2.imread("img/person.png")
results = model(image,verbose=False)[0]
# 从YOLOv8中加载数据结果
detections = sv.Detections.from_ultralytics(results)
# 查看输出结果维度
print("before slicer",len(detections))

# 切片回调函数
def callback(image_slice: np.ndarray) -> sv.Detections:
    result = model(image_slice,verbose=False)[0]
    return sv.Detections.from_ultralytics(result)

# 设置 Slicing Adaptive Inference(SAHI)处理对象
# callback-对于切片后每张子图进行处理的回调函数
# slice_wh-切片后子图的大小
# overlap_ratio_wh-连续切片之间的重叠率
# iou_threshold-子图合并时用于nms的iou阈值
# thread_workers-处理线程数
slicer = sv.InferenceSlicer(callback = callback, slice_wh=(320,320),
                            overlap_ratio_wh=(0.3,0.3), iou_threshold=0.4, thread_workers=4)

detections = slicer(image)
# 查看输出结果维度
print("after slicer",len(detections))
before slicer 32
after slicer 53

3.4 轨迹平滑

supervision提供了用于平滑视频跟踪轨迹的实用类DetectionsSmoother。 DetectionsSmoother维护每个轨迹的检测历史记录,并根据这些历史记录提供平滑的预测。具体代码如下:

import supervision as sv

from ultralytics import YOLO

video_path = "https://media.roboflow.com/supervision/video-examples/grocery-store.mp4"
video_info = sv.VideoInfo.from_video_path(video_path=video_path)
frame_generator = sv.get_video_frames_generator(source_path=video_path)

model = YOLO("yolov8n.pt")
tracker = sv.ByteTrack(frame_rate=video_info.fps)
# 跟踪结果平滑器,length-平滑检测时要考虑的最大帧数
smoother = sv.DetectionsSmoother(length=4)

annotator = sv.BoundingBoxAnnotator()

with sv.VideoSink("output.mp4", video_info=video_info) as sink:
    for frame in frame_generator:
        result = model(frame)[0]
        detections = sv.Detections.from_ultralytics(result)
        detections = tracker.update_with_detections(detections)
        # 平滑目标跟踪轨迹
        detections = smoother.update_with_detections(detections)

        annotated_frame = annotator.annotate(frame.copy(), detections)
        # 数据展示
        sv.plot_image(annotated_frame, (16, 16))
        # sink.write_frame(annotated_frame)

# 代码输出结果见:https://media.roboflow.com/supervision-detection-smoothing.mp4

4 参考

作者:卢文双 资深数据库内核研发

本文首发于 2023-11-30 20:47:35

https://dbkernel.com

问题描述

当主从复制采用 binlog 的行模式时,如果从库启用 slow_query_log、log_slow_replica_statements 且从库重放 CREATE TABLE、DROP TABLE 时因特殊情况(比如被从库其他 SQL 占用 MDL 锁)执行耗时较长,会被从库记录到慢日志(slow log),而 ALTER TABLE 却不会被记录到慢日志。

ALTER TABLE 等管理语句是否会记录到慢日志,受参数 slow_query_log、log_slow_admin_statements 控制。

本文基于 MySQL 8.0.30 版本。

复现步骤

1. 搭建主从复制

主(master)、从(replica)my.cnf 中启用 binlog 的行模式:

binlog_format=ROW # 行模式

2. 从库动态设置配置参数

set global long_query_time=0.0001;
# 当然,除了这种方法,还有另一种方法:
# 主库执行DROP TABLE db1.tbl 语句之前,在从库先用事务阻塞住 DROP TABLE db1.tbl 的重放(会处于 Waiting for table metadata lock 状态):
# begin; select count(*) from db1.tbl for update;
# 等待几秒后(大于long_query_time的配置即可),再 commit

set global slow_query_log=on;
set global log_slow_replica_statements=on;

mysql> show variables like '%slow%';
+-----------------------------+----------------------------------------------------------+
| Variable_name               | Value                                                    |
+-----------------------------+----------------------------------------------------------+
| log_slow_admin_statements   | OFF                                                      |
| log_slow_extra              | OFF                                                      |
| log_slow_replica_statements | ON                                                       |
| log_slow_slave_statements   | ON                                                       |
| slow_launch_time            | 2                                                        |
| slow_query_log              | ON                                                       |
| slow_query_log_file         | /home/wslu/work/mysql/mysql80-data/s1-slave1/vm-slow.log |
+-----------------------------+----------------------------------------------------------+
7 rows in set (0.01 sec)

3. 主库执行 SQL 语句

CREATE TABLE db1.tbl(a int, b int);
DROP TABLE db1.tbl;

4. 查看从库慢日志

查看从库
slow_query_log_file
参数指定的慢日志文件,其中出现 DROP TABLE 语句:

# Time: 2023-11-30T09:36:32.202303+08:00
# User@Host: skip-grants user[] @  []  Id:    41
# Query_time: 0.060373  Lock_time: 0.000143 Rows_sent: 0  Rows_examined: 0
SET timestamp=1701308185;
CREATE TABLE `tbl` (
  `my_row_id` bigint unsigned NOT NULL AUTO_INCREMENT /*!80023 INVISIBLE */,
  `a` int DEFAULT NULL,
  `b` int DEFAULT NULL,
  PRIMARY KEY (`my_row_id`)
);
# Time: 2023-11-30T09:36:37.768072+08:00
# User@Host: skip-grants user[] @  []  Id:    41
# Query_time: 0.025328  Lock_time: 0.000089 Rows_sent: 0  Rows_examined: 0
SET timestamp=1701308197;
DROP TABLE `tbl` /* generated by server */;

初步分析

这与官方对 log_slow_slave_statements 参数的描述不符

When the slow query log is enabled,
log_slow_replica_statements
enables logging for queries that have taken more than
long_query_time
seconds to execute on the replica. Note that if row-based replication is in use (
binlog_format=ROW
),
log_slow_replica_statements
has no effect. Queries are only added to the replica's slow query log when they are logged in statement format in the binary log, that is, when
binlog_format=STATEMENT
is set, or when
binlog_format=MIXED
is set and the statement is logged in statement format. Slow queries that are logged in row format when
binlog_format=MIXED
is set, or that are logged when
binlog_format=ROW
is set, are not added to the replica's slow query log, even if
log_slow_replica_statements
is enabled.

Setting
log_slow_replica_statements
has no immediate effect. The state of the variable applies on all subsequent
START REPLICA
statements. Also note that the global setting for
long_query_time
applies for the lifetime of the SQL thread. If you change that setting, you must stop and restart the replication SQL thread to implement the change there (for example, by issuing
STOP REPLICA
and
START REPLICA
statements with the
SQL_THREAD
option).

按照官方的描述,在 binlog_format 是行模式的情况下,即使启用
log_slow_replica_statements
参数,从库重放时也不该产生慢日志。

补充说明

按照上述同样的步骤执行 ALTER TABLE 语句,则不会记录到 slow log

通过阅读手册及自行验证,ALTER TABLE 等管理语句是否记录到从库的 slow log 受参数
log_slow_admin_statements
控制。

log_slow_admin_statements

Include slow administrative statements in the statements written to the slow query log. Administrative statements include
ALTER TABLE
,
ANALYZE TABLE
,
CHECK TABLE
,
CREATE INDEX
,
DROP INDEX
,
OPTIMIZE TABLE
, and
REPAIR TABLE
.

代码解读

函数堆栈:

#0  Query_logger::slow_log_write (this=0xaaaaef99e760 <query_logger>, thd=0xffff3c291be0, query=0xffff34165cb8 "DROP TABLE `tbl` /* generated by server */",
    query_length=42, aggregate=false, lock_usec=0, exec_usec=0) at /home/wslu/work/mysql/mac-mysql-server/sql/log.cc:1334
#1  0x0000aaaaead81368 in log_slow_do (thd=0xffff3c291be0) at /home/wslu/work/mysql/mac-mysql-server/sql/log.cc:1643
#2  0x0000aaaaead813a0 in log_slow_statement (thd=0xffff3c291be0) at /home/wslu/work/mysql/mac-mysql-server/sql/log.cc:1660
#3  0x0000aaaaeb9ce438 in Query_log_event::do_apply_event (this=0xffff341ce540, rli=0xffff3402ad80,
    query_arg=0xffff34165cb8 "DROP TABLE `tbl` /* generated by server */", q_len_arg=42) at /home/wslu/work/mysql/mac-mysql-server/sql/log_event.cc:4884
#4  0x0000aaaaeb9cc840 in Query_log_event::do_apply_event (this=0xffff341ce540, rli=0xffff3402ad80)
    at /home/wslu/work/mysql/mac-mysql-server/sql/log_event.cc:4447
#5  0x0000aaaaeb9f43c4 in Log_event::do_apply_event_worker (this=0xffff341ce540, w=0xffff3402ad80)
    at /home/wslu/work/mysql/mac-mysql-server/sql/log_event.cc:1087
#6  0x0000aaaaebacb3a4 in Slave_worker::slave_worker_exec_event (this=0xffff3402ad80, ev=0xffff341ce540)
    at /home/wslu/work/mysql/mac-mysql-server/sql/rpl_rli_pdb.cc:1733
#7  0x0000aaaaebacda04 in slave_worker_exec_job_group (worker=0xffff3402ad80, rli=0xaaab2f98d4d0)
    at /home/wslu/work/mysql/mac-mysql-server/sql/rpl_rli_pdb.cc:2457
#8  0x0000aaaaebae84d4 in handle_slave_worker (arg=0xffff3402ad80) at /home/wslu/work/mysql/mac-mysql-server/sql/rpl_replica.cc:5913
#9  0x0000aaaaec8356f0 in pfs_spawn_thread (arg=0xffff784dd4e0) at /home/wslu/work/mysql/mac-mysql-server/storage/perfschema/pfs.cc:2942
#10 0x0000ffff928bd5c8 in start_thread (arg=0x0) at ./nptl/pthread_create.c:442
#11 0x0000ffff92925d1c in thread_start () at ../sysdeps/unix/sysv/linux/aarch64/clone.S:79

最终会调用
Query_logger::slow_log_write
函数:

bool Query_logger::slow_log_write(THD *thd, const char *query,
                                  size_t query_length, bool aggregate,
                                  ulonglong lock_usec, ulonglong exec_usec) {
  assert(thd->enable_slow_log && opt_slow_log);

  if (!(*slow_log_handler_list)) return false;

  /* do not log slow queries from replication threads */
  if (thd->slave_thread && !opt_log_slow_replica_statements) return false; // ====> 关键位置

  /* fill in user_host value: the format is "%s[%s] @ %s [%s]" */
  char user_host_buff[MAX_USER_HOST_SIZE + 1];
  Security_context *sctx = thd->security_context();
  LEX_CSTRING sctx_user = sctx->user();
  LEX_CSTRING sctx_host = sctx->host();
  LEX_CSTRING sctx_ip = sctx->ip();
  size_t user_host_len =
      (strxnmov(user_host_buff, MAX_USER_HOST_SIZE, sctx->priv_user().str, "[",
                sctx_user.length ? sctx_user.str : "", "] @ ",
                sctx_host.length ? sctx_host.str : "", " [",
                sctx_ip.length ? sctx_ip.str : "", "]", NullS) -
       user_host_buff);
  ulonglong current_utime = my_micro_time();
  ulonglong query_utime, lock_utime;
  if (aggregate) {
    query_utime = exec_usec;
    lock_utime = lock_usec;
  } else if (thd->start_utime) {
    query_utime = (current_utime - thd->start_utime);
    lock_utime = thd->get_lock_usec();
  } else {
    query_utime = 0;
    lock_utime = 0;
  }

  bool is_command = false;
  if (!query) {
    is_command = true;
    const std::string &cn = Command_names::str_global(thd->get_command());
    query = cn.c_str();
    query_length = cn.length();
  }

  mysql_rwlock_rdlock(&LOCK_logger);

  bool error = false;
  for (Log_event_handler **current_handler = slow_log_handler_list;
       *current_handler;) {
    error |= (*current_handler++)
                 ->log_slow(thd, current_utime,
                            (thd->start_time.tv_sec * 1000000ULL) +
                                thd->start_time.tv_usec,
                            user_host_buff, user_host_len, query_utime,
                            lock_utime, is_command, query, query_length); // 写慢日志
  }

  mysql_rwlock_unlock(&LOCK_logger);

  return error;
}

结论

我查看了 8.0.31-8.0.35 版本的 change log,其中并无对
DROP TABLE
相关的 Bug Fix,说明该问题官方尚未修复。

可行的修改思路有两种:

  1. 比较直接的方式是修改
    Query_logger::slow_log_write
    函数中的逻辑,添加额外的条件判断(见后文)。
  2. 借鉴参数
    log_slow_admin_statements
    的处理逻辑。如果启用
    log_slow_admin_statements
    参数且管理语句执行时长大于 long_query_time,则会将其记录到慢日志,最终也会调用到
    Query_logger::slow_log_write
    函数;反之,如果未启用该参数,则不会记录管理语句到慢日志。这说明是在中间过程中判断并过滤的,本文不再展开说明。

公司同事向官方提交了 BUG,官方已经确认,其中的 patch 采用的思路 1:

MySQL Bugs: #113251: the slow log in slave is logged ,when binlog_format is row

--- a/sql/log.cc
+++ b/sql/log.cc
@@ -1295,6 +1295,9 @@ bool Query_logger::slow_log_write(THD *t
   /* do not log slow queries from replication threads */
   if (thd->slave_thread && !opt_log_slow_replica_statements) return false;

+  /*when binlog_format=MIXED is set, or that are logged when binlog_format=ROW is set, are not added to the replica's slow query log, even if log_slow_replica_statements is enabled.*/
+  if (thd->slave_thread && opt_log_slow_replica_statements && (thd->current_stmt_binlog_format == BINLOG_FORMAT_MIXED ||thd->current_stmt_binlog_format == BINLOG_FORMAT_ROW) ) return false;
+
   /* fill in user_host value: the format is "%s[%s] @ %s [%s]" */
   char user_host_buff[MAX_USER_HOST_SIZE + 1];
   Security_context *sctx = thd->security_context();


欢迎关注我的微信公众号【数据库内核】:分享主流开源数据库和存储引擎相关技术。

欢迎关注公众号数据库内核

标题 网址
GitHub https://dbkernel.github.io
知乎 https://www.zhihu.com/people/dbkernel/posts
思否(SegmentFault) https://segmentfault.com/u/dbkernel
掘金 https://juejin.im/user/5e9d3ed251882538083fed1f/posts
CSDN https://blog.csdn.net/dbkernel
博客园(cnblogs) https://www.cnblogs.com/dbkernel

平时很少读经管类的书籍,作为豆瓣经管类 No.1 的书籍,《纳瓦尔宝典》被许多人推荐过,虽然是经管类,但书籍内容却并不是教人如何经商和投资股票,而是硅谷的著名天使投资人纳瓦尔·拉威康特(Naval Ravikant)的智慧箴言录,我感觉就像是一个智者和你寻寻叨叨符合当下时代潮流的成长思维和心理学知识,近期我读完了该书,有感于某些思想和哲理深刻,故而记录一下我的读书体会。

创造财富

复制的边际成本为零

纳瓦尔在书中的观点明确,反复的表达,个人仅依靠出售也不可能向财富靠近,为什么呢?因为个人的时间是不可复制资源,时间只有一份且拥有排他性,比如你用时间去度假了,那么这段时间就不能用来去工作。所以就算单份的时间单价再高也好,也和财富无关。换一个角度来说,任何不能直接复制反复出售多份的事情都不可能让人实现财务自由。那么做怎样的事情可以实现财富呢 ?

作者给出了一些方向性的指引:尽可能的去构建出版书籍,专栏,博客,或者视频等等,因为这些事情都有一个特质就是具有 “
一次构建,多次销售
” 的属性,既复制的边际成本几乎为零,且不受时间和空间的限制,只有具有以上属性的资产,才能帮助你走上财富自由之路。这也是这几年自媒体这么火的原因,例如最近很火的李一舟为代表,就拥有独自构建复制边际成本为零的商品和营销的能力,从而创造了巨大到财富。当然李一舟是一个反例,他提供的服务和宣传不符,不仅损害自身声誉,且已经涉嫌诈骗的嫌疑。所以作者也一直在重申:不管是任何行业,要维护好自己的职业口碑是非常重要,提供的内容要和价值匹配,因为声誉是一个人的隐形财富,具有复利效应,需要持续累积,不要做损人利己的事情,不仅会遭受法律的处罚,个人声誉也一落千丈。不再有任何翻身的可能。

构建的前提

构建复制边际为零的商品还有一个前提,就是你已经在所处的行业已经处于顶尖,才通过互联网作为放大器去无限放大自己的价值。如果你还在是学生,那么应该花时间和精力在选择和在专业能力的精进上。学习专业不能只局限在自己的本专业上面,例如你是计算机专业,那么有余力可以再多学习历史,经济学,心理学,博弈论,沟通和演讲等等相关专业。了解多个学科,可以开阔思维和眼界。所以作者提倡顺序是:

  1. 首选选择一个你非常热爱且能长期持续投入的专业,日拱一卒,不期速成。
  2. 成为所在行业的顶尖的专业人士,累积经验,然后能够通过分享经验帮助他人
  3. 通过互联网作为放大器,增加 “一次构建,多次出售” 等复制边际成本为零的商品,创造更大的价值,也可以增加被动收入
  4. 持续累积和输出,耐心等待,学会和时间做朋友,让专业和财富的雪球慢慢越滚越大

然而,在输出和分享的之前,很多人都担心自己的内容不够有吸引力,或者写的不好(我自己也有这种担忧),书中作者给出的观点是:互联网其实很大的范围,每个人都有其自己的特点,而且特别是内容领域的特色是:“文无第一,武无第二”,只要你能持续的提供对别人有价值的内容,就是是小众专栏,也能累积属于自己独一无二的受众群体。因为互联网很大且没有地理位置的限制。只要能在自己领域提供对别人有帮助的信息,持续的累积,创建的价值会持续为你带来经济上的回报,甚至有可能会逐渐超过你的主业,而且通过互联网提供信息能帮助不同的人,价值和意义也是很大的。

不可替代的优势

在自己专业上的投入有复利效应的,所以需要有价值投资和长期持有的思维,只要找到了正确的事业,那么就全身心的投入,持续的精进几十年,就能从中开始收获回报了。除专业的知识外,构建自己的不可替代优势,那么要学会去拥有
不能通过培训习得的技能
就非常重要了。因为可以通过培训量产的技能,不具备稀缺性。那么有什么能力是不能通过培训获取的,我个人归纳有如下几点:

  1. 创造力:总是提出解决问题的方案的人,用创新方式去解决问题的人(因为大多数人都是只提问题)
  2. 责任心和同理心:具备换位思考能力,良好的情绪控制能力,重视信用并且承担责任的能力,都是稀缺的品质
  3. 领导力:领导团队达成目标,能看到每个人的长处,并且将合适的事情安排给合适的人去做等等
  4. 行业洞察力:具备对事物深刻的认知,能洞察问题的本质,了解事情的前因后果
  5. 其他。。。。

这部分属于不可描述型知识了,就是老子所说 “道可道,非常道” 的部分,因为不可被量产,所以稀缺,软能力的构建不仅可以避免被他人取代,也可以避免被现在火热的 AI 取代。因为 AI 的学习方式是通过数据和知识库去学习技能的。所以 AI 能掌握的知识也都是可以通过培训量产的知识,AI 无法构建不可描述型知识的能力。除此之外快速的学习能力,在当下的社会也是非常的重要,以前学一门专业可能可以工作一辈子到退休,但是日新月异的现代,可能还没毕业所学的知识就已经过时了,应对的方法是,需要具备能快速快速学习的能力,例如 4 个月掌握一门技术,然后 4 年后它可能就淘汰了,但是你可以在中间几年赚到钱,然后再去学习前沿的技术,依次循环往复,持续交替。

时间成本策略

上面提到的要做以上那么多的事情,还要兼顾生活和家庭上的琐事,时间上肯定是不够用的。所以要想从时间成本的角度做决策。作者给出的建议是要从时间成本上去考虑做决策,例如一项工作的外包成本低于时薪,那么就将工作外包出去,例如:

  • 清洁服务:雇佣专业的保洁阿姨完成家里搞卫生,保洁等工作
  • 育儿服务:请专门的育儿保姆完成将接送孩子,洗衣做饭的工作
  • 通勤服务:请专职的司机解决自己和家人的通勤需求,等等
  • 等等…………

基于时间成本决策,通过购买专业人士的服务来减少自己在消耗型工作上所花费的时间,从而将自己的时间累积下来在自己的专业上创造更大的机制。

总结:最后在作者眼里,未来随着互联网发展,它将人区分为两种 “会用杠杆的人” 和 “不会用杠杆的人”。这么说虽然有些夸张,但是这本书前面一般章节都在将如何利用杠杆实现财务自由。所以,重点就是:财富 = (努力 + 学习 + 专业)* 时间 * 杠杆

幸福人生

三件大事

下半篇的内容,作者表达写书的目的不会教人赚钱,而是想教人如何过上幸福的生活,因为是赚钱只是实现幸福人生的一种手段而已。让生活幸福才是最终的目的。下半篇幅有很多心理学的理论在里面(可能是作者读书涉猎很广的原因)。探讨的是如果过上幸福的人生。

作者认为如果一定要在人生三件大事上,做出正确的选择,例如:

  • 在哪里生活
  • 和谁在一起
  • 从事什么职业

选择往往比努力要重要的多,花费时间精力去做出正确的选择,是值得的,只有在正确的选择上去努力,那么人生大概率会是幸福。在个人的品行上也要有良好的道德品质,自律的人更加自爱,具备良好道德品质的人容易产生自信。自信的人对待生活也会更加的热爱,也会吸引到相同品质的人。

痛苦的选择

如果我们在一个选择上犹豫不决,应该选择短期内会更痛苦的事情,因为人性的真相是:人的天性是短视的,做困难的选择往往可以带来更加长期的轻松。相反,如果选择短期内轻松的事情,那么就要承受长期的痛苦。例如:

  1. 在看书和玩游戏之间,你选择玩游戏,逐渐学习荒废,沉迷游戏或者其他
  2. 在运动和美食之间,你选择了美食,逐渐让身体变的肥胖,失去健康

所以要做出符合个人长期利益的选择,需要持续的锻炼大脑,要让大脑习惯选择短期痛苦的事情,压制短期想要放纵的欲望。这些逆人性习惯都是需要时间累积来锻炼的。

不要将自己的快乐建立在多巴胺带来的刺激上。例如:当你想要一辆车,对于物质欲望的本能会驱使你天天去关注它的动态信息。但是当你开上一段时间这种感觉便不会再存在了。这是典型多巴胺刺激带来的快乐。多巴胺带来的并不是真正的快乐,而是一种渴望,当欲望被满足,下次就需要更大的刺激才能换来多巴胺带来的同等快乐。彻底陷入基因的成瘾陷阱里。不要以为多巴胺能让你永远快乐,但是真相是能够永远给你带来满足和快乐的恰恰是一颗平静的内心。

平静和理性

尽量每天都阅读,不设限制,历史,哲学,经济等等学科都尽量有所涉猎,应该将买书视为一项投资,因为读到一本好书对自己获益是无穷的,不仅要多读书,还要多思考。实际上我们目前处于一个学习资源极其丰富的年代,只要用手机打开读书 APP,何时何地,人类的任何经典图书和知识都随手可得。但是我们现在不缺学习的资料,缺乏的是一颗求知的心。大多数人宁愿用短视频来打发空闲时间。也不会打开读书 APP。现在短视频等及时反馈的 APP 也正在让大多数人失去耐心和专注的能力。所以在当下这个时代只要每天能平静下来 30 分钟读书,运动。长期累积下来,这些隐形的财富就已经超过大多数人了。

生活中的任何事物都有它相对的一面,没有绝对好坏,一件事情如果能看到它的消极的一面,才能真正的去追求它积极的一面。对待事物要懂得区分事实和观点,现实生活中太多人分不清观点和事实了,充斥着对观点的争论。很多时候应该抛开观点,聚焦在事实上面,不要考虑观点的问题(因为表达观点是廉价的)。懂得区分它们很重要,尊重事实才能在复杂的事物中找到问题的真相。

活在当下

活在当下,对于已经过去的事无法改变,不要在意,不要有任何遗憾。也不要把当下的希望寄托于未来。努力的活在当下才是最重要的事情。在心态上应该在意自己的感受,不要去取悦他人,实际上你开心了你身边的人才会开心。那些需要你牺牲感受去取悦的人都是不值得让你付出的人。

总结:用价值投资和长期主义的心态去面对生活,无论是财务,朋友,爱情,健康,还是知识。它们的都需要长期的累积的投入才能带来回报。就像选择股票,前提是在选择的时候已经尽可能的完成谨慎和认真分析。不要浮躁,不要用任何短期投机的心理去面对生活中的人和事情,不要盲目的追热点,涉足自己不熟悉领域,例如最近看到比特币、AI 概念股火热就跟风去炒,最终吃亏的是自己。