2023年3月

python常规的用法,众多pythoner早已​熟烂于心,如:

1、当一个元组只有一个元素时

a = (1, )

2、当表示解包一个容器时

a = [('amo', 1), ('bmo', 1)]
for c, k in a:
    ...

3、当表示连续赋值时

a, b = 1, 2

4、在print中表示元素连接

for i in range(1,5):
    print(i,)
>>>1 2 3 4
# 如果不使用逗号,输入每个元素就会换行

恕我孤陋寡闻,今天手欠,发现了一个隐藏用法,有一个字典列表

d = [
    {'amo': 1},
    {'bmo': 2},
    {'cmo': 3}
]

需求:拿到每个字典的键

当然,有很多方法都可以实现,但论简单快捷,还属下方整个实现:

for i, in d:
    print(i)
>>>
amo
bmo
cmo

我真的是孤陋寡闻了,之前各种keys()的操作才能拿到键。

  • 本文实现一个Echo TCP Server

interface/tcp/Handler.go

type Handler interface {
   Handle(ctx context.Context, conn net.Conn)
   Close() error
}
  • Handler:业务逻辑的处理接口
    • Handle(ctx context.Context, conn net.Conn) 处理连接

tcp/server.go

type Config struct {
    Address string
}

func ListenAndServeWithSignal(cfg *Config, handler tcp.Handler) error {
    closeChan := make(chan struct{})
    listen, err := net.Listen("tcp", cfg.Address)
    if err != nil {
       return err
   }
    logger.Info("start listen")
    ListenAndServe(listen, handler, closeChan)
    return nil
}

func ListenAndServe(listener net.Listener,
                    handler tcp.Handler,
                    closeChan <-chan struct{}) {
    ctx := context.Background()
    var waitDone sync.WaitGroup
    for true {
        conn, err := listener.Accept()
        if err != nil {
            break
        }
        logger.Info("accept link")
        waitDone.Add(1)
        go func() {
            defer func() {
                waitDone.Done()
            }()
            handler.Handler(ctx, conn)
        }()
    }
    waitDone.Wait()
}
  • Config:启动tcp服务器的配置
    • Address:监听地址
  • ListenAndServe:ctx是上下文,可以传递一些参数。死循环中接收到新连接时,让一个协程去处理连接
  • 如果listener.Accept()出错了就会break跳出来,这时候需要等待已经服务的客户端退出。使用WaitGroup等待客服端退出
func ListenAndServe(listener net.Listener,
                    handler tcp.Handler,
                    closeChan <-chan struct{}) {

    go func() {
       <-closeChan
       logger.Info("shutting down...")
       _ = listener.Close()
       _ = handler.Close()
   }()

    defer func() {
       _ = listener.Close()
       _ = handler.Close()
   }()

    ......
}

listener和handler在退出的时候需要关掉。如果用户直接kill掉了程序,我们也需要关掉listener和handler,这时候要使用closeChan,一旦接收到关闭信号,就执行关闭逻辑

func ListenAndServeWithSignal(cfg *Config, handler tcp.Handler) error {

    closeChan := make(chan struct{})
    sigCh := make(chan os.Signal)
    signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT)
    go func() {
       sig := <-sigCh
       switch sig {
          case syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
          closeChan <- struct{}{}
      }
   }()
    listen, err := net.Listen("tcp", cfg.Address)
    if err != nil {
       return err
   }
    logger.Info("start listen")
    ListenAndServe(listen, handler, closeChan)
    return nil
}

当系统对程序发送信号时,sigCh会接收到信号

tcp/echo.go

type EchoHandler struct {
   activeConn sync.Map
   closing    atomic.Boolean
}

EchoHandler:

  • activeConn:记录连接
  • closing:是否正在关闭,有并发竞争,使用atomic.Boolean
type EchoClient struct {
   Conn    net.Conn
   Waiting wait.Wait
}

func (c *EchoClient) Close() error {
	c.Waiting.WaitWithTimeout(10 * time.Second)
	_ = c.Conn.Close()
	return nil
}

EchoClient:一个客户端就是一个连接。Close方法关闭客户端连接,超时时间设置为10s

func MakeHandler() *EchoHandler {
	return &EchoHandler{}
}

func (h *EchoHandler) Handle(ctx context.Context, conn net.Conn) {
   // 连接正在关闭,不接收新连接
   if h.closing.Get() {
      _ = conn.Close()
   }

   client := &EchoClient{
      Conn: conn,
   }
   h.activeConn.Store(client, struct{}{})

   reader := bufio.NewReader(conn)
   for {
      msg, err := reader.ReadString('\n')
      if err != nil {
         if err == io.EOF {
            logger.Info("connection close")
            h.activeConn.Delete(client)
         } else {
            logger.Warn(err)
         }
         return
      }
      // 正在处理业务,不要关掉
      client.Waiting.Add(1)
      // 将数据原封不动写回去,测试
      b := []byte(msg)
      _, _ = conn.Write(b)
      client.Waiting.Done()
   }
}

func (h *EchoHandler) Close() error {
   logger.Info("handler shutting down...")
   h.closing.Set(true)
   h.activeConn.Range(func(key interface{}, val interface{}) bool {
      client := key.(*EchoClient)
      _ = client.Close()
      return true
   })
   return nil
}
  • MakeEchoHandler:创建EchoHandler
  • Handle:处理客户端的连接。
    • 1.连接正在关闭时,不接收新连接
    • 2.存储新连接,value用空结构体
    • 3.使用缓存区接收用户发来的数据,使用\n作为结束的标志
  • Close:将所有客户端连接关掉

main.go

const configFile string = "redis.conf"

var defaultProperties = &config.ServerProperties{
   Bind: "0.0.0.0",
   Port: 6379,
}

func fileExists(filename string) bool {
   info, err := os.Stat(filename)
   return err == nil && !info.IsDir()
}

func main() {
   logger.Setup(&logger.Settings{
      Path:       "logs",
      Name:       "godis",
      Ext:        "log",
      TimeFormat: "2022-02-02",
   })

   if fileExists(configFile) {
      config.SetupConfig(configFile)
   } else {
      config.Properties = defaultProperties
   }

   err := tcp.ListenAndServeWithSignal(
      &tcp.Config{
         Address: fmt.Sprintf("%s:%d",
            config.Properties.Bind,
            config.Properties.Port),
      },
      EchoHandler.MakeHandler())
   if err != nil {
      logger.Error(err)
   }
}

测试

image.png

监控二叉树

力扣题目链接(opens new window)

给定一个二叉树,我们在树的节点上安装摄像头。

节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。

计算监控树的所有节点所需的最小摄像头数量。

示例 1:

img

  • 输入:[0,0,null,0,0]
  • 输出:1
  • 解释:如图所示,一台摄像头足以监控所有节点。

示例 2:

img

  • 输入:[0,0,null,0,null,0,null,null,0]
  • 输出:2
  • 解释:需要至少两个摄像头来监视树的所有节点。 上图显示了摄像头放置的有效位置之一。

提示:

  • 给定树的节点数的范围是 [1, 1000]。
  • 每个节点的值都是 0。

思路


难题警告

题意解读

由题目和示例可知,本题的目标是
在二叉树的合适的节点上"装摄像头"
,那么装了摄像头的节点
可以覆盖的范围是本层加上其两个子节点(如果有的话)和一个父节点
,即一共可以覆盖三层

由此可以推知,我们
应该尽量在叶子节点的父节点处放置摄像头
,这样可以
充分利用其三层覆盖范围

所以我们可以从下往上推,在叶子节点的父节点放摄像头,然后其父节点的上一个节点(即父节点的父节点)再隔一个节点放置摄像头

对应颜色的摄像头的覆盖范围如图所示,我们在橙色的节点处放置了摄像头,其有两个叶子节点,因此这是符合我们的推断的

该摄像头节点的覆盖范围包含了其父节点,因此我们无需在其父节点的父节点处放置摄像头,因为这
会导致覆盖范围重叠,进而浪费一个覆盖范围
,这不符合我们设置最少摄像头的目标

贪心点

这里还会出现一个问题,就是在根节点处放摄像头其实也浪费了一个覆盖范围(就是其父节点的范围)

所以如果严格按照设置最少摄像头的目标来做的话,此时我们应该将摄像头设置在根节点的父节点

但是,
实际上这么做只是在局部上满足了最优解,在全局上并不是最优的

因为叶子节点的数量远大于根节点的数量,如果按照“在根节点的父节点设置摄像头”的规则来做的话,那么会影响到叶子节点设置摄像头的方法,进而影响最后总摄像头的数量

因此,
不能为了节省根节点浪费的一个范围牺牲其他更多更合理的摄像头设置位置

总结一下摄像头安放原则:
优先在叶子节点的父节点安放摄像头,然后从下往上推,每隔两个节点再放一个摄像头,直到遍历至根节点

状态设定

(仅用于模拟,无需选择最优状态)

通过上面的分析,我们遍历二叉树的顺序自然的应该选择
后序遍历

问题又来了,怎么控制每隔两个节点放一个摄像头呢?

这里可以设置几个状态,然后
记录每个节点的状态
,根据左右子节点的状态去确定父节点的状态

通过节点间的状态关系,来确定某处是否应该安放摄像头

根据分析,可以得出以下三种节点状态:

  • 没被覆盖(0)
  • 有摄像头(1)
  • 被覆盖(2)

这里要么节点是被覆盖,要么是没被覆盖,没必要设置一个没有摄像头的状态(有摄像头其实也是被覆盖的)

然后这里需要讨论二叉树中
空节点的状态设置问题

以上述二叉树为例,因为就三种状态嘛,那这个NULL就有三种情况,看看哪种情况符合我们的要求


情况1: NULL节点没被覆盖(0)

​ 因为这个NULL节点被设置为无覆盖状态,那按题意一定要有一个摄像头去覆盖它,因此NULL的父节点,也就是
叶子节点就需要放置摄像头

​ 而这与我们前面设计的放置原则(优先在叶子节点的父节点安放摄像头)有冲突,且推到后面,根节点处会没有摄像头(反正就是不合适)

​ 因此情况1否了


情况2: NULL节点有摄像头(1)

​ 这种情况也不行,因为如果NULL节点有摄像头,就意味着叶子节点已经被覆盖了

​ 那么下一步需要隔两个节点再放置摄像头,那叶子节点的父节点就又没有摄像头了,即又不满足设计初衷

​ 否了


情况3: NULL节点被覆盖(2)

​ 前两种都不行,那这个肯定行了,简单推一下就知道这种情况是满足条件的

因此,
空节点应该被设置为有覆盖状态(2)

状态转移

规定好状态之后,如何在遍历过程中对每个节点的状态进行转换并统计摄像头个数呢?

这里又有几种情况需要讨论:(后序遍历)

情况1:左右子节点都有覆盖

某个节点的左右子节点均被覆盖时,覆盖它们的摄像头肯定是不同的,因此当前节点应该设置为
无覆盖(0)
,等待之后遍历到父节点设置摄像头将其覆盖

情况2:左右子节点至少有一个无覆盖

不论左右,当某个节点的子节点出现无覆盖状态时,当前节点都应该设置为
摄像头状态(1)
,这样才能不漏掉这个没被覆盖的节点

情况3: 左右子节点至少有一个有摄像头

因为摄像头的覆盖范围是三层,某个节点的子节点有摄像头,那该节点肯定是被覆盖到了的,因此节点状态设为
有覆盖(2)

情况4:遍历完毕根节点还是没被覆盖

确实是有这种情况,此时需要对根节点进行单独修改,放置一个
摄像头(1)

虽然这样会浪费一个覆盖范围,但是也没办法

至此,所有状态转移的情况分析完毕(够复杂够阴间了吧...)

代码

要使用后序遍历,因此可以直接使用二叉树后序递归遍历的模板(
详见

为了复习之前的知识,这里还是使用递归三部曲的方式写一下代码

递归三部曲

1、确定递归函数的参数和返回值

根据分析,我们在遍历二叉树的过程中是通过判断当前节点的状态(依据是左右子节点的状态)来决定是否要设置摄像头的

因此递归的返回值应该是一个节点的状态(0、1、2其中之一),因此
递归函数有返回值int

输入参数肯定是
待判断状态的节点

class Solution {
private:
    int res = 0;//记录摄像头数量
    int traversal(TreeNode* cur){//确定递归函数参数与返回值

	}
public:
    int minCameraCover(TreeNode* root) {     
    }
};

2、确定终止条件

遇到叶子节点就终止,和遍历模板中一致

class Solution {
private:
    int res = 0;//记录摄像头数量
    int traversal(TreeNode* cur){//确定递归函数参数与返回值
		if(cur == NULL) return 2;//空节点按规则返回2状态
	}
public:
    int minCameraCover(TreeNode* root) {     
    }
};

3、确定单层处理逻辑

这里就是完成左右中的后序遍历逻辑就行

需要在中的位置处理我们的3中左右子节点的情况,来给当前节点返回一个状态(第四种状态在主函数中处理)

class Solution {
private:
    int res = 0;//记录摄像头数量
    int traversal(TreeNode* cur){//确定递归函数参数与返回值
    	//确定终止条件,遇到叶子节点终止,该节点有覆盖(因为是空节点)
    	if(cur == NULL) return 2;
    	
    	//确定单层处理逻辑,即左右中
    	int left = traversal(cur->left);//左
        int right = traversal(cur->right);//右

        //中,处理4种左右节点的情况
        if(left == 2 && right == 2) return 0;//左右子节点都有覆盖(2)
        if(left == 0 || right == 0){//左右子节点至少有一个无覆盖
            res++;
            return 1;
        }
        if(left == 1 || right == 1) return 2;//左右子节点至少有一个有摄像头(1)
        return -1;//需要一个返回值,但代码不会运行到这
	}
public:
    int minCameraCover(TreeNode* root) {
    }
};

注意,递归函数有返回值,需要用变量接一下,并且最后需要给一个返回值-1(尽管不会执行到此处)

完整代码

剩下的一种情况是遍历结束根节点没被摄像头覆盖,我们可以在主函数中处理该情况

只要在调用递归函数后判断一下最终返回值是否为0即可(注意返回值不是res)

如果是0表示当前二叉树在遍历结束后,根节点的状态没有变为被覆盖(2),要多设置一个摄像头(就让res再加1即可)

class Solution {
private:
    int res = 0;//记录摄像头数量
    int traversal(TreeNode* cur){//确定递归函数参数与返回值
    	//确定终止条件,遇到叶子节点终止,该节点有覆盖(因为是空节点)
    	if(cur == NULL) return 2;
    	
    	//确定单层处理逻辑,即左右中
    	int left = traversal(cur->left);//左
        int right = traversal(cur->right);//右

        //中,处理4种左右节点的情况
        if(left == 2 && right == 2) return 0;//左右子节点都有覆盖(2)
        if(left == 0 || right == 0){//左右子节点至少有一个无覆盖
            res++;
            return 1;
        }
        if(left == 1 || right == 1) return 2;//左右子节点至少有一个有摄像头(1)
        return -1;//需要一个返回值,但代码不会运行到这
	}
public:
    int minCameraCover(TreeNode* root) {
        //在这里处理root无覆盖的情况
        if(traversal(root) == 0){//调用递归进行后序遍历,如果根节点没被覆盖,额外加个摄像头
            res++;
            return res;
        }
        return res;
    }
};

说明

使用 VLD 内存泄漏检测工具辅助开发时整理的学习笔记。


1. 使用前的准备

参考本人另一篇博客
安装 Visual Leak Detector
下载
vld-2.5.1-setup.exe
并按步骤安装 VLD。这一种使用方式的特点是,在一台电脑上安装完成后,将 VLD 安装目录下的 lib 库及 include 文件拷贝到项目目录中,在项目
pro
文件中指明库及头文件的路径,并将
vld.ini
文件和 VLD 安装目录 bin 文件夹下的全部文件拷贝到项目生成目录下,最后在
mian.cpp
文件中
#include "vld.h"
。优点是,当把项目拷贝到别的电脑上编译运行时,该电脑无需安装 VLD,也不需要更改任何代码。

2. 在 QT 中使用 VLD

我的 VLD 安装目录为
D:\Program Files (x86)\Visual Leak Detector
。安装完成后,文件列表如下:

Oh Shit!-图片走丢了-打个广告-欢迎来博客园关注“木三百川”

需要用到的是
bin

include

lib
三个文件夹,以及
vld.ini
文件。下文示例项目所在路径为
E:\Cworkspace\Qt 5.9.0\QtDemo\testVLD
,项目路径下的文件列表如下:

Oh Shit!-图片走丢了-打个广告-欢迎来博客园关注“木三百川”

2.1 复制 lib 库及头文件

拷贝
include
文件夹中的
vld.h

vld_def.h
到项目路径下,拷贝整个
lib
文件夹到项目路径下,这两步拷贝完成后,项目路径下的文件列表如下:

Oh Shit!-图片走丢了-打个广告-欢迎来博客园关注“木三百川”

2.2 在项目 .pro 文件中指明路径

在项目对应的
pro
文件中添加 VLD 的头文件和
lib
库,
pro
文件中添加如下代码:

HEADERS += \
    vld.h \
    vld_def.h

win32{
    CONFIG(debug, debug | release) {
        contains(QT_ARCH, x86_64){
            LIBS += -L$$PWD/lib/Win64 -lvld
        }else{
            LIBS += -L$$PWD/lib/Win32 -lvld
        }
    }
}

2.3 配置 bin 文件夹下的依赖库

拷贝
bin\Win32
文件夹中的四个文件
dbghelp.dll

Microsoft.DTfW.DHL.manifest

vld_x86.dll

vld_x86.pdb
到 32 位 MSVC 在 Debug 模式下的生成目录中,若不使用
DESTDIR
指令,但勾选
Shadow build
,默认的生成路径为
E:\Cworkspace\Qt 5.9.0\QtDemo\build-testVLD-Desktop_Qt_5_9_2_MSVC2015_32bit-Debug\debug
,拷贝结果如下:

Oh Shit!-图片走丢了-打个广告-欢迎来博客园关注“木三百川”

64 位的做类似操作,拷贝
bin\Win64
文件夹中的四个文件
dbghelp.dll

Microsoft.DTfW.DHL.manifest

vld_x64.dll

vld_x64.pdb
到 64 位 MSVC 在 Debug 模式下的生成目录中,若不使用
DESTDIR
指令,但勾选
Shadow build
,默认的生成路径为
E:\Cworkspace\Qt 5.9.0\QtDemo\build-testVLD-Desktop_Qt_5_9_2_MSVC2015_64bit-Debug\debug
,拷贝结果如下:

Oh Shit!-图片走丢了-打个广告-欢迎来博客园关注“木三百川”

更佳的做法是使用
DESTDIR
指令,实现 32 位、64 位在指定路径下生成
exe
,这样可以将
exe
直接生成在对应的
Win32

Win64
路径下,而不需要将上述 4 个文件分别拷贝到对应的
debug
目录

。为实现这种效果,首先将整个
bin
文件拷贝到项目路径下,拷贝完成后,项目路径下的文件列表如下:

Oh Shit!-图片走丢了-打个广告-欢迎来博客园关注“木三百川”

在项目对应的
pro
文件中使用
DESTDIR
指令设置生成路径,添加如下代码:

contains(QT_ARCH, x86_64){
    DESTDIR = $$PWD/bin/Win64
}else{
    DESTDIR = $$PWD/bin/Win32
}

同时,为将
release

debug
两种版本区分出来,不至于在同一个文件夹中引起混乱,在
pro
文件中额为添加如下代码:

TARGET_NAME = testVLD
CONFIG(debug, debug|release) {
    TARGET_NAME = $${TARGET_NAME}-d
}
TARGET = $${TARGET_NAME}

这样设置之后,生成的
debug
版结果将比
release
版结果多一个
-d
后缀,便于区分。

2.4 复制 vld.ini 文件

vld.ini
是 VLD 工具的配置文件,可以修改
vld.ini
内容以定制内存泄漏检测报告。没有该文件其实也能正常运行,但为了后续可定制,最好还是将
vld.ini
拷贝到生成目录下。比如在上一步中的
E:\Cworkspace\Qt 5.9.0\QtDemo\build-testVLD-Desktop_Qt_5_9_2_MSVC2015_32bit-Debug\debug

E:\Cworkspace\Qt 5.9.0\QtDemo\build-testVLD-Desktop_Qt_5_9_2_MSVC2015_64bit-Debug\debug
,若在上一步中使用了
DESTDIR
指令,则生成目录变为
E:\Cworkspace\Qt 5.9.0\QtDemo\testVLD\bin\Win32

E:\Cworkspace\Qt 5.9.0\QtDemo\testVLD\bin\Win64

2.5 在 main.cpp 文件中添加头文件

在项目的
main.cpp
文件中,添加头文件:

#include "vld.h"

选择 MSVC 32bit 或者 MSVC 64bit 编译器,选择
Debug
模式,编译运行,就可以正常使用了。

2.6 无内存泄漏时的输出报告

程序运行结束后,若没有检测到内存泄漏,VLD 会输出以下 4 行报告:

Visual Leak Detector read settings from: E:\Cworkspace\Qt 5.9.0\QtDemo\testVLD\bin\Win32\vld.ini
Visual Leak Detector Version 2.5.1 installed.
No memory leaks detected.
Visual Leak Detector is now exiting.

需要注意的是,此时读取的配置文件
vld.ini
已经不是 VLD 安装路径下的那个了,从第一行可以看到具体路径。使用 64 位 MSVC 时的输出如下:

Visual Leak Detector read settings from: E:\Cworkspace\Qt 5.9.0\QtDemo\testVLD\bin\Win64\vld.ini
Visual Leak Detector Version 2.5.1 installed.
No memory leaks detected.
Visual Leak Detector is now exiting.

因为使用了
DESTDIR
指令, 32 位和 64 位会在所指定的路径下生成,符合预期结果。

3. 无法正常使用的可能原因

按前述步骤进行配置后,就可以卸载已经安装的 VLD 工具了,到 VLD 安装路径下,双击
unins000.exe
,弹窗点击“是(Y)” 按钮卸载 VLD,重新编译运行程序,仍可正常使用。当把项目拷贝到别的电脑上编译运行时,新电脑环境无需安装 VLD,也不需要更改任何代码。若无法正常使用,考虑以下可能的原因。

  • 检查编译器版本
    ,VLD 无法在 minGW 下使用,只能使用 MSVC 编译器。
  • 检查是否是 Debug 模式
    ,VLD 无法直接在 Release 模式下使用。
  • 检查文件的位数是否正确
    ,32 bit /64 bit 不能混用。
  • 清除上一次的编译文件,重新编译运行一下。
  • 若路径中存在空格,添加库时一定要使用
    $$quote()
    将路径括起来,否则路径解析不正确。
  • 检查生成目录下是否包含有 VLD 的 4 个依赖文件,32 位为
    dbghelp.dll

    Microsoft.DTfW.DHL.manifest

    vld_x86.dll

    vld_x86.pdb
    ,64 位为
    dbghelp.dll

    Microsoft.DTfW.DHL.manifest

    vld_x64.dll

    vld_x64.pdb

4. 示例源码

4.1 工程 .pro 文件

# testVLD.pro

QT -= gui

CONFIG += c++11 console
CONFIG -= app_bundle

SOURCES += main.cpp

HEADERS += \
    vld.h \
    vld_def.h

win32{
    CONFIG(debug, debug | release) {
        contains(QT_ARCH, x86_64){
            LIBS += -L$$PWD/lib/Win64 -lvld
        }else{
            LIBS += -L$$PWD/lib/Win32 -lvld
        }
    }
}

contains(QT_ARCH, x86_64){
    DESTDIR = $$PWD/bin/Win64
}else{
    DESTDIR = $$PWD/bin/Win32
}

TARGET_NAME = testVLD
CONFIG(debug, debug|release) {
    TARGET_NAME = $${TARGET_NAME}-d
}
TARGET = $${TARGET_NAME}

4.2 主函数 main.cpp 文件

// mian.cpp

#include <QCoreApplication>
#include "vld.h"

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

    return a.exec();
}

4.3 示例工程目录结构

工程目录结构如下:

E:\Cworkspace\Qt 5.9.0\QtDemo\testVLD
│  main.cpp
│  testVLD.pro
│  testVLD.pro.user
│  vld.h
│  vld_def.h
│
├─bin
│  ├─Win32
│  │      dbghelp.dll
│  │      Microsoft.DTfW.DHL.manifest
│  │      testVLD-d.exe
│  │      testVLD-d.ilk
│  │      testVLD-d.pdb
│  │      testVLD.exe
│  │      testVLD.pdb
│  │      vld.ini
│  │      vld_x86.dll
│  │      vld_x86.pdb
│  │
│  └─Win64
│          dbghelp.dll
│          Microsoft.DTfW.DHL.manifest
│          testVLD-d.exe
│          testVLD-d.ilk
│          testVLD-d.pdb
│          testVLD.exe
│          testVLD.pdb
│          vld.ini
│          vld_x64.dll
│          vld_x64.pdb
│
└─lib
    ├─Win32
    │      vld.lib
    │
    └─Win64
            vld.lib

Oh Shit!-图片走丢了-打个广告-欢迎来博客园关注“木三百川”

摘要:基于深度学习的车型识别系统用于识别不同类型的车辆,应用YOLO V5算法根据不同尺寸大小区分和检测车辆,并统计各类型数量以辅助智能交通管理。本文详细介绍车型识别系统,在介绍算法原理的同时,给出 P y t h o n 的实现代码以及 P y Q t 的UI界面。在界面中可以选择各种图片、视频进行检测识别;可对图像中存在的多目标进行识别分类,检测速度快、识别精度高。博文提供了完整的Python代码和使用教程,适合新入门的朋友参考,完整代码资源文件请转至文末的下载链接。本博文目录如下:

➷点击跳转至文末所有涉及的 完整代码文件 下载页☇

完整代码下载: https://mbd.pub/o/bread/ZJaXlZlx

参考视频演示: https://www.bilibili.com/video/BV1yM411p7kq/

离线依赖库下载https://pan.baidu.com/s/1hW9z9ofV1FRSezTSj59JSg?pwd=oy4n (提取码:oy4n )


前言

智能交通系统是现代化交通的重要组成部分,是未来交通系统的发展趋势。在智能交通中,车型的自动识别是一个重要的研究方向。其在停车场车辆管理、道路交通状况监管和车流量统计等众多领域有着广泛的应用。针对交通视频,车型识别系统主要利用图像处理和模式识别技术来实时进行分析处理视频监控数据。

本系统基于YOLOv5,采用登录注册进行用户管理,对于图片、视频和摄像头捕获的实时画面,可检测车型,系统支持结果记录、展示和保存,每次检测的结果记录在表格中。对此这里给出博主设计的界面,同款的简约风,功能也可以满足图片、视频和摄像头的识别检测,希望大家可以喜欢,初始界面如下图:

检测类别时的界面截图(点击图片可放大)如下图,可识别画面中存在的多个类别,也可开启摄像头或视频检测:

详细的功能演示效果参见博主的B站视频或下一节的动图演示,觉得不错的朋友敬请点赞、关注加收藏!系统UI界面的设计工作量较大,界面美化更需仔细雕琢,大家有任何建议或意见和可在下方评论交流。


1. 效果演示

一款软件的颜值和功能同样重要,首先我们还是通过动图看一下识别的效果,系统主要实现的功能是对图片、视频和摄像头画面中的车型进行识别,识别的结果可视化显示在界面和图像中,另外提供多个目标的显示选择功能,演示效果如下。

(一)系统介绍

基于深度学习的车型识别系统主要用于不同尺寸类型的车辆识别,利用摄像设备采集的图像、视频或实时画面,应用深度学习技术识别多种包括小型车、中型车、大型车、小型卡车、大型卡车等7种类型车辆,在软件界面中标记检测框和车型类别,并可视化数量;软件准确定位检测车辆并记录在界面中显示记录结果,支持各个类型车辆数目、类别、置信度等结果可视化、展示和保存;软件提供登录注册功能,可进行用户管理。

(二)技术特点

(1)检测模型支持更换,模型采用YOLOv5训练;
(2)摄像头实时检测车型,展示、记录和保存识别结果;
(3)可检测图片、视频等文件,统计结果实时可视化;
(4)支持用户登录、注册,检测结果可视化功能;

(三)用户注册登录界面

这里设计了一个登录界面,可以注册账号和密码,然后进行登录。界面还是参考了当前流行的UI设计,左侧是一个LOGO图,右侧输入账号、密码、验证码等等。

(四)选择图片识别

系统允许选择图片文件进行识别,点击图片选择按钮图标选择图片后,显示所有识别的结果,可通过下拉选框查看单个结果,以便具体判断某一特定目标。本功能的界面展示如下图所示:

(五)视频识别效果展示

很多时候我们需要识别一段视频中的多个车辆,这里设计了视频选择功能。点击视频按钮可选择待检测的视频,系统会自动解析视频逐帧识别多个车型,并将车型的分类和计数结果记录在右下角表格中,效果如下图所示:

(六)摄像头检测效果展示

在真实场景中,我们往往利用道路的摄像头获取实时画面,同时需要对车型进行识别,因此本文考虑到此项功能。如下图所示,点击摄像头按钮后系统进入准备状态,系统显示实时画面并开始检测画面中的车型,识别结果展示可见本人视频。


2. 车型数据集及训练

(一)YOLOv5模型简介

本文借助YOLOv5实现对不同大小车辆的类型进行识别,YOLOv5的调用、训练和预测都十分方便,并且它为不同的设备需求和不同的应用场景提供了大小和参数数量不同的网络。

YOLOv5模型是一个在COCO数据集上预训练的物体检测架构和模型系列,它是YOLO系列的一个延伸,能够很好的用来进行车型的特征提取,其网络结构共分为:input、backbone、neck和head四个模块,yolov5对yolov4网络的优点在于:在input端使用了Mosaic数据增强、自适应锚框计算、自适应图片缩放; 在backbone端使用了Focus结构与CSP结构;在neck端添加了FPN+PAN结构;在head端改进了训练时的损失函数,使用GIOU_Loss,以及预测框筛选的DIOU_nms。除了模型结构,yolov5使用Pytorch框架,对用户非常友好;代码易读;模型训练快速;能够直接对图像,视频进行推理;能直接部署到手机应用端;预测速度非常快。YoloV5模型详解可以参照 链接

(1)主干部分:使用了Focus网络结构,具体操作是在一张图片中每隔一个像素拿到一个值,这个时候获得了四个独立的特征层,然后将四个独立的特征层进行堆叠,此时宽高信息就集中到了通道信息,输入通道扩充了四倍。该结构在YoloV5第5版之前有所应用,最新版本中未使用。

(2)数据增强:Mosaic数据增强、Mosaic利用了四张图片进行拼接实现数据中增强,优点是可以丰富检测物体的背景,且在BN计算的时候可以计算四张图片的数据。

(3)多正样本匹配:在之前的Yolo系列里面,在训练时每一个真实框对应一个正样本,即在训练时,每一个真实框仅由一个先验框负责预测。YoloV5中为了加快模型的训练效率,增加了正样本的数量,在训练时,每一个真实框可以由多个先验框负责预测。

(二)车型识别数据集

这里我们使用的车型数据集,其中训练集包含1488张图片,验证集包含507张图片,测试集包含31张图片,共计2026张图片。部分图片和标注情况如下图所示。

每张图像均提供了图像类标记信息,图像中车型的bounding box,车型的关键part信息,以及车型的属性信息,数据集并解压后得到如下的图片

该数据集分为7类,分别有小型车,中型车,大型车,小型卡车,大型卡车,油罐车,特种车。

Chinese_name = {'tiny-car': "小型车", 'mid-car': "中型车", 'big-car': "大型车", 'small-truck': "小型卡车",
                'big-truck': "大型卡车", 'oil-truck': "油罐车", 'special-car': "特种车"}

我们分析一下数据集的组成结构,第4类也就是小型卡车的的图片最多,并且x,y坐标主要集中在0.5,0.5的位置。

这里我们开始训练和测试自己的数据集,在cmd终端中运行train.py进行训练,以下是训练过程中的结果截图。

在深度学习中,我们通常通过损失函数下降的曲线来观察模型训练的情况。而YOLOv5训练时主要包含三个方面的损失:矩形框损失(box_loss)、置信度损失(obj_loss)和分类损失(cls_loss),在训练结束后,我们也可以在logs目录下找到生成对若干训练过程统计图。下图为博主训练车型类识别的模型训练曲线图。

一般我们会接触到两个指标,分别是召回率recall和精度precision,两个指标p和r都是简单地从一个角度来判断模型的好坏,均是介于0到1之间的数值,其中接近于1表示模型的性能越好,接近于0表示模型的性能越差,为了综合评价目标检测的性能,一般采用均值平均密度map来进一步评估模型的好坏。我们通过设定不同的置信度的阈值,可以得到在模型在不同的阈值下所计算出的p值和r值,一般情况下,p值和r值是负相关的,绘制出来可以得到如下图所示的曲线,其中曲线的面积我们称AP,目标检测模型中每种目标可计算出一个AP值,对所有的AP值求平均则可以得到模型的mAP值。

以PR-curve为例,你可以看到我们的模型在验证集上的均值平均准确率为0.992。

3. 车型检测识别

在训练完成后得到最佳模型,接下来我们将帧图像输入到这个网络进行预测,从而得到预测结果,预测方法(testVideo.py)部分的代码如下所示:

def predict(img):
    img = torch.from_numpy(img).to(device)
    img = img.half() if half else img.float()
    img /= 255.0
    if img.ndimension() == 3:
        img = img.unsqueeze(0)
 
    t1 = time_synchronized()
    pred = model(img, augment=False)[0]
    pred = non_max_suppression(pred, opt.conf_thres, opt.iou_thres, classes=opt.classes,
                               agnostic=opt.agnostic_nms)
    t2 = time_synchronized()
    InferNms = round((t2 - t1), 2)
 
    return pred, InferNms
 
 
def plot_one_box(img, x, color=None, label=None, line_thickness=None):
    # Plots one bounding box on image img
    tl = line_thickness or round(0.002 * (img.shape[0] + img.shape[1]) / 2) + 1  # line/font thickness
    color = color or [random.randint(0, 255) for _ in range(3)]
    c1, c2 = (int(x[0]), int(x[1])), (int(x[2]), int(x[3]))
    cv2.rectangle(img, c1, c2, color, thickness=tl, lineType=cv2.LINE_AA)
    if label:
        tf = max(tl - 1, 1)  # font thickness
        t_size = cv2.getTextSize(label, 0, fontScale=tl / 3, thickness=tf)[0]
        c2 = c1[0] + t_size[0], c1[1] - t_size[1] - 3
        cv2.rectangle(img, c1, c2, color, -1, cv2.LINE_AA)  # filled
        cv2.putText(img, label, (c1[0], c1[1] - 2), 0, tl / 3, [225, 255, 255], thickness=tf, lineType=cv2.LINE_AA)

执行得到的结果如下图所示,图中车型的种类和置信度值都标注出来了,预测速度较快。基于此模型我们可以将其设计成一个带有界面的系统,在界面上选择图片、视频或摄像头然后调用模型进行检测。

if __name__ == '__main__':
    # video_path = 0
    video_path = "./UI_rec/test_/test.mp4"
    # 初始化视频流
    vs = cv2.VideoCapture(video_path)
    (W, H) = (None, None)
    frameIndex = 0  # 视频帧数

    try:
        prop = cv2.CAP_PROP_FRAME_COUNT
        total = int(vs.get(prop))
        # print("[INFO] 视频总帧数:{}".format(total))
    # 若读取失败,报错退出
    except:
        print("[INFO] could not determine # of frames in video")
        print("[INFO] no approx. completion time can be provided")
        total = -1

    fourcc = cv2.VideoWriter_fourcc(*'XVID')
    ret, frame = vs.read()
    vw = frame.shape[1]
    vh = frame.shape[0]
    print("[INFO] 视频尺寸:{} * {}".format(vw, vh))
    output_video = cv2.VideoWriter("./results.avi", fourcc, 20.0, (vw, vh))  # 处理后的视频对象

    # 遍历视频帧进行检测
    while True:
        # 从视频文件中逐帧读取画面
        (grabbed, image) = vs.read()

        # 若grabbed为空,表示视频到达最后一帧,退出
        if not grabbed:
            print("[INFO] 运行结束...")
            output_video.release()
            vs.release()
            exit()

        # 获取画面长宽
        if W is None or H is None:
            (H, W) = image.shape[:2]

        image = cv2.resize(image, (850, 500))
        img0 = image.copy()
        img = letterbox(img0, new_shape=imgsz)[0]
        img = np.stack(img, 0)
        img = img[:, :, ::-1].transpose(2, 0, 1)  # BGR to RGB, to 3x416x416
        img = np.ascontiguousarray(img)

        pred, useTime = predict(img)

        det = pred[0]
        p, s, im0 = None, '', img0
        if det is not None and len(det):  # 如果有检测信息则进入
            det[:, :4] = scale_coords(img.shape[1:], det[:, :4], im0.shape).round()  # 把图像缩放至im0的尺寸
            number_i = 0  # 类别预编号
            detInfo = []
            for *xyxy, conf, cls in reversed(det):  # 遍历检测信息
                c1, c2 = (int(xyxy[0]), int(xyxy[1])), (int(xyxy[2]), int(xyxy[3]))
                # 将检测信息添加到字典中
                detInfo.append([names[int(cls)], [c1[0], c1[1], c2[0], c2[1]], '%.2f' % conf])
                number_i += 1  # 编号数+1

                label = '%s %.2f' % (names[int(cls)], conf)

                # 画出检测到的目标物
                plot_one_box(image, xyxy, label=label, color=colors[int(cls)])

        # 实时显示检测画面
        cv2.imshow('Stream', image)
        image = cv2.resize(image, (vw, vh))
        output_video.write(image)  # 保存标记后的视频
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break

        # print("FPS:{}".format(int(0.6/(end-start))))
        frameIndex += 1

执行得到的结果如下图所示,图中车辆的种类和置信度值都标注出来了,预测速度较快。基于此模型我们可以将其设计成一个带有界面的系统,在界面上选择图片、视频或摄像头然后调用模型进行检测。

博主对整个系统进行了详细测试,最终开发出一版流畅得到清新界面,就是博文演示部分的展示,完整的UI界面、测试图片视频、代码文件,以及Python离线依赖包(方便安装运行,也可自行配置环境),均已打包上传,感兴趣的朋友可以通过下载链接获取。


下载链接

若您想获得博文中涉及的实现完整全部程序文件(包括测试图片、视频, py, UI 文件等,如下图),这里已打包上传至博主的面包多平台,见可参考博客与视频,已将所有涉及的文件同时打包到里面,点击即可运行,完整文件截图如下:

在文件夹下的资源显示如下,下面的链接中也给出了Python的离线依赖包,读者可在正确安装Anaconda和Pycharm软件后,复制离线依赖包至项目目录下进行安装,离线依赖的使用详细演示也可见本人B站视频: win11从头安装软件和配置环境运行深度学习项目Win10中使用pycharm和anaconda进行python环境配置教程

注意 :该代码采用Pycharm+Python3.8开发,经过测试能成功运行,运行界面的主程序为runMain.py和LoginUI.py,测试图片脚本可运行testPicture.py,测试视频脚本可运行testVideo.py。为确保程序顺利运行,请按照requirements.txt配置Python依赖包的版本。 Python版本:3.8 ,请勿使用其他版本,详见requirements.txt文件;

完整资源中包含数据集及训练代码,环境配置与界面中文字、图片、logo等的修改方法请见视频, 项目完整文件下载请见以下链接处给出 :➷➷➷

完整代码下载: https://mbd.pub/o/bread/ZJaXlZlx

参考视频演示: https://www.bilibili.com/video/BV1yM411p7kq/

离线依赖库下载https://pan.baidu.com/s/1hW9z9ofV1FRSezTSj59JSg?pwd=oy4n (提取码:oy4n )


界面中文字、图标和背景图修改方法:

在Qt Designer中可以彻底修改界面的各个控件及设置,然后将ui文件转换为py文件即可调用和显示界面。如果只需要修改界面中的文字、图标和背景图的,可以直接在ConfigUI.config文件中修改,步骤如下:
(1)打开UI_rec/tools/ConfigUI.config文件,若乱码请选择GBK编码打开。
(2)如需修改界面文字,只要选中要改的字符替换成自己的就好。
(3)如需修改背景、图标等,只需修改图片的路径。例如,原文件中的背景图设置如下:

mainWindow = :/images/icons/back-image.png

可修改为自己的名为background2.png图片(位置在UI_rec/icons/文件夹中),可将该项设置如下即可修改背景图:

mainWindow = ./icons/background2.png

结束语

由于博主能力有限,博文中提及的方法即使经过试验,也难免会有疏漏之处。希望您能热心指出其中的错误,以便下次修改时能以一个更完美更严谨的样子,呈现在大家面前。同时如果有更好的实现方法也请您不吝赐教。