【基于Qt和OpenCV的多线程图像识别应用】

news2024/11/19 23:40:20

基于Qt和OpenCV的多线程图像识别应用

  • 前言
  • 多线程编程
    • 为什么需要多线程
    • Qt如何实现多线程
    • 线程间通信
  • 图像识别
  • 项目代码
    • 项目结构
    • 各部分代码
  • 项目演示
  • 小结

前言

这是一个简单的小项目,使用Qt和OpenCV构建的多线程图像识别应用程序,旨在识别图像中的人脸并将结果保存到不同的文件夹中。这个项目结合了图像处理、多线程编程和用户界面设计。 用户可以通过界面选择要识别的文件夹和保存结果的文件夹。然后,启动识别进程。图像识别线程并行处理选定文件夹中的图像,检测图像中的人脸并将其保存到一个文件夹,同时将不包含人脸的图像保存到另一个文件夹。进度和结果将实时显示在用户界面上。

多线程编程

为什么需要多线程

1、并行处理:在处理大量图像时,使用单线程可能会导致应用程序变得非常慢,因为它必须依次处理每个图像(这里我没有去实现,感兴趣的小伙伴可以自己尝试一下)。多线程允许应用程序同时处理多个图像,从而提高了处理速度。

2、防止阻塞:如果在主线程中执行耗时的操作,比如图像识别,会导致用户界面在操作执行期间被冻结,用户无法与应用程序互动。多线程可以将这些耗时操作移到后台线程,以避免界面阻塞。

3、利用多核处理器:现代计算机通常具有多核处理器,多线程可以充分利用这些多核来加速任务的执行。

Qt如何实现多线程

在项目中,多线程编程主要使用了Qt的 QThread 类来实现。以下是在项目中使用多线程的关键步骤
1、继承QThread类: 首先,创建一个自定义的线程类,继承自 QThread 类。这个类将负责执行多线程任务。在项目中,这个自定义线程类是 ImageRecognitionThread。

2、重写run函数: 在自定义线程类中,重写 run 函数。run 函数定义了线程的执行体,也就是线程启动后会执行的代码。在本项目中,run 函数包含了图像识别的逻辑。

3、创建线程对象: 在应用程序中,创建自定义线程类的对象,例如 ImageRecognitionThread 的对象。然后,通过调用 start 函数来启动线程。

4、信号和槽机制: 使用Qt的信号和槽机制来实现线程间的通信。在项目中,使用信号来更新识别进度和结果,以便主线程可以实时显示这些信息。

5、线程安全性: 要确保多个线程安全地访问共享资源,例如文件系统或图像数据,通常需要使用互斥锁(Mutex)等机制来防止竞争条件和数据损坏。

线程间通信

在线程间进行通信是多线程编程中的关键概念,特别是在项目中,其中一个线程负责图像识别任务,另一个线程用于用户界面更新。在这个项目中,使用了Qt的信号和槽机制来实现线程间的通信,以便更新识别进度和结果。具体步骤如下:
1、信号和槽的定义
首先定义了信号和槽函数,分别用于更新进度和结果:

signals:
    void updateProgress(int progress);
    void updateResult(const QString& result);

private slots:
    void onProgressUpdate(int progress);
    void onResultUpdate(const QString& result);

updateProgress 信号用于更新识别进度,它接受一个整数参数,表示识别进度的百分比。
updateResult 信号用于更新识别结果,它接受一个字符串参数,表示识别的结果信息。
onProgressUpdate 槽函数用于接收进度更新信号,并在主线程中更新用户界面的进度条。
onResultUpdate 槽函数用于接收结果更新信号,并在主线程中更新用户界面的结果文本。

2、信号的发射
在 ImageRecognitionThread 类的 run 函数中,根据图像识别的进度和结果,使用以下方式发射信号:

// 发射进度更新信号
emit updateProgress(progress);

// 发射结果更新信号
emit updateResult("图像 " + imageFile + " 中检测到人脸并已保存。");

通过 emit 关键字,可以发射定义的信号,并传递相应的参数。

3、槽函数的连接
在主线程中,当创建 ImageRecognitionThread 的对象时,需要建立信号和槽的连接,以便接收来自线程的信号并执行槽函数。这通常在主窗口类的构造函数中完成。例如:

// 创建ImageRecognitionThread对象
imageThread = new ImageRecognitionThread(this);

// 连接信号和槽
connect(imageThread, &ImageRecognitionThread::updateProgress, this, &MainWindow::onProgressUpdate);
connect(imageThread, &ImageRecognitionThread::updateResult, this, &MainWindow::onResultUpdate);

这些连接操作确保当 ImageRecognitionThread 中的信号被发射时,相关的槽函数会在主线程中执行。

4、槽函数的执行

槽函数会在主线程中执行,因此可以直接更新用户界面的进度条和结果文本。例如:

void MainWindow::onProgressUpdate(int progress)
{
    ui->progressBar->setValue(progress);
}

void MainWindow::onResultUpdate(const QString& result)
{
    ui->resultTextEdit->append(result);
}

在这里,onProgressUpdate 槽函数更新了主窗口中的进度条,而 onResultUpdate 槽函数更新了结果文本框。
通过信号和槽机制,项目中的不同线程能够安全地进行通信,而不会导致竞争条件或数据损坏。这种机制允许图像识别线程实时更新识别进度和结果,同时保持了用户界面的响应性,提供了更好的用户体验。

图像识别

图像识别的流程是这个项目的核心部分,它包括了加载图像、使用OpenCV的人脸检测器识别人脸、以及根据结果保存图像等步骤。以下是详细描述的图像识别流程:

1、加载图像

首先,从用户选择的识别文件夹中加载图像。这个步骤包括以下操作:
获取用户选择的识别文件夹路径。
遍历该文件夹中的所有图像文件。
逐个加载图像文件。在项目中,可以使用OpenCV库的 cv::imread 函数来加载图像。

// 从文件夹中加载图像
cv::Mat image = cv::imread(imageFile.toStdString());

2、 人脸识别

一旦图像加载完成,接下来的任务是识别图像中的人脸。这个项目使用OpenCV提供的人脸检测器来完成这个任务,通常使用Haar级联分类器或深度学习模型。在本项目中,我们使用了OpenCV内置的Haar级联分类器。

创建一个 cv::CascadeClassifier 对象并加载Haar级联分类器的XML文件。

cv::CascadeClassifier faceCascade;
faceCascade.load("haarcascade_frontalface_default.xml");

使用加载的分类器检测图像中的人脸。这将返回一个矩形列表,每个矩形表示一个检测到的人脸的位置。

std::vector<cv::Rect> faces;
faceCascade.detectMultiScale(image, faces, scaleFactor, minNeighbors, flags, minSize, maxSize);

根据检测到的人脸位置,可以在图像上绘制矩形框,以标记人脸的位置。

for (const cv::Rect& faceRect : faces) {
    cv::rectangle(image, faceRect, cv::Scalar(0, 255, 0), 2); // 在图像上绘制矩形框
}

3、 结果保存
最后,根据识别的结果,将图像保存到相应的文件夹。在本项目中,根据是否检测到人脸,有两个不同的保存路径:一个用于保存包含人脸的图像,另一个用于保存不包含人脸的图像。

如果检测到了人脸,将图像保存到包含人脸的文件夹中。

if (!faces.empty()) {
    QString savePathWithFace = saveFolderPath + "/with_face/" + QFileInfo(imageFile).fileName();
    cv::imwrite(savePathWithFace.toStdString(), image);
}

如果没有检测到人脸,将图像保存到不包含人脸的文件夹中。

else {
    QString savePathWithoutFace = saveFolderPath + "/without_face/" + QFileInfo(imageFile).fileName();
    cv::imwrite(savePathWithoutFace.toStdString(), image);
}

以上就是图像识别的主要流程。通过这个流程,项目能够加载、识别和保存图像,根据识别结果将图像分别保存到两个不同的文件夹中,以实现人脸识别功能。这个流程结合了OpenCV的图像处理能力,为图像识别提供了一个基本框架。

项目代码

项目结构

项目分为两个主要部分:
1、用户界面:使用Qt框架创建,包括选择识别文件夹、选择保存结果文件夹、启动和停止识别等功能。
2、图像识别线程:使用Qt的QThread类创建,负责加载图像、识别人脸、保存结果,并通过信号和槽机制与用户界面通信。

各部分代码

1、imagerecognitionthread.h

#ifndef IMAGERECOGNITIONTHREAD_H
#define IMAGERECOGNITIONTHREAD_H

#include <QThread>
#include <QString>

class ImageRecognitionThread : public QThread
{
    Q_OBJECT

public:
    explicit ImageRecognitionThread(QObject* parent = nullptr);
    void setFolderPath(const QString& folderPath);
    void setSaveFolderPath(const QString& saveFolderPath); 

protected:
    void run() override;

signals:
    void updateProgress(int progress);
    void updateResult(const QString& result);

private:
    QString folderPath;
    QString saveFolderPath; 
};

#endif 

2、imagerecognitionthread.cpp

#include "imagerecognitionthread.h"
#include <opencv2/opencv.hpp>
#include <QDir>
ImageRecognitionThread::ImageRecognitionThread(QObject* parent)
    : QThread(parent), folderPath(""), saveFolderPath("")
{
  
}

void ImageRecognitionThread::setFolderPath(const QString& folderPath)
{
    this->folderPath = folderPath;
}

void ImageRecognitionThread::setSaveFolderPath(const QString& saveFolderPath)
{
    this->saveFolderPath = saveFolderPath;
}

void ImageRecognitionThread::run()
{
    QString faceCascadePath = "D:\\DownLoad\\opencv\\sources\\data\\haarcascades\\haarcascade_frontalface_default.xml";

    cv::CascadeClassifier faceCascade;
    if (!faceCascade.load(faceCascadePath.toStdString()))
    {
        emit updateResult("无法加载人脸检测器");
        return;
    }

    QDir imageDir(folderPath);
    QStringList imageFilters;
    imageFilters << "*.jpg" << "*.png";
    QStringList imageFiles = imageDir.entryList(imageFilters, QDir::Files);

    int totalImages = imageFiles.size();
    int processedImages = 0;

    QString faceSaveFolderPath = saveFolderPath + "/faces"; // 用于保存包含人脸的图像的文件夹
    QString noFaceSaveFolderPath = saveFolderPath + "/no_faces"; // 用于保存不包含人脸的图像的文件夹

    // 创建保存结果的文件夹
    QDir().mkpath(faceSaveFolderPath);
    QDir().mkpath(noFaceSaveFolderPath);

    for (const QString& imageFile : imageFiles)
    {
        processedImages++;
        int progress = (processedImages * 100) / totalImages;
        emit updateProgress(progress);

        QString imagePath = folderPath + "/" + imageFile;
        cv::Mat image = cv::imread(imagePath.toStdString());

        if (!image.empty())
        {
            std::vector<cv::Rect> faces;
            faceCascade.detectMultiScale(image, faces, 1.1, 4, 0 | cv::CASCADE_SCALE_IMAGE, cv::Size(30, 30));

            if (!faces.empty())
            {
                QString targetPath = faceSaveFolderPath + "/" + imageFile;
                cv::imwrite(targetPath.toStdString(), image);
                emit updateResult("图像 " + imageFile + " 中检测到人脸并已保存到人脸文件夹。");
            }
            else
            {
                QString targetPath = noFaceSaveFolderPath + "/" + imageFile;
                cv::imwrite(targetPath.toStdString(), image);
                emit updateResult("图像 " + imageFile + " 中未检测到人脸并已保存到非人脸文件夹。");
            }
        }
    }

    emit updateResult("识别完成,结果保存在相应文件夹中");
}

3、mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QLineEdit>
#include <QPushButton>
#include <QLabel>
#include <QProgressBar>
#include <QListWidget>
#include "imagerecognitionthread.h"

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget* parent = nullptr);

private slots:
    void startRecognition();
    void stopRecognition();
    void updateProgress(int progress);
    void updateResult(const QString& result);
    void selectRecognitionFolder();
    void selectSaveFolder();

private:
    void setupUi();
    void connectSignalsAndSlots();

    QLineEdit* folderPathLineEdit;
    QLineEdit* saveFolderPathLineEdit; 
    QPushButton* startButton;
    QPushButton* stopButton;
    QPushButton* selectRecognitionFolderButton; 
    QPushButton* selectSaveFolderButton; 
    QLabel* progressLabel;
    QProgressBar* progressBar;
    QLabel* resultsLabel;
    QListWidget* resultsList;

    ImageRecognitionThread* recognitionThread;
};

#endif // MAINWINDOW_H

4、mainwindow.cpp

#include "mainwindow.h"
#include "imagerecognitionthread.h"
#include <QVBoxLayout>
#include <QFileDialog>
#include <QDebug>

MainWindow::MainWindow(QWidget* parent)
    : QMainWindow(parent), recognitionThread(nullptr)
{
    setupUi();
    connectSignalsAndSlots();
}

void MainWindow::startRecognition()
{
    // 获取文件夹路径
    QString folderPath = folderPathLineEdit->text();
    QString saveFolderPath = saveFolderPathLineEdit->text(); // 获取保存结果的文件夹路径

    // 创建并启动识别线程
    recognitionThread = new ImageRecognitionThread(this);
    recognitionThread->setFolderPath(folderPath);
    recognitionThread->setSaveFolderPath(saveFolderPath); // 设置保存结果的文件夹路径
    connect(recognitionThread, &ImageRecognitionThread::updateProgress, this, &MainWindow::updateProgress);
    connect(recognitionThread, &ImageRecognitionThread::updateResult, this, &MainWindow::updateResult);
    recognitionThread->start();
}

void MainWindow::stopRecognition()
{
    // 如果识别线程正在运行,终止它
    if (recognitionThread && recognitionThread->isRunning())
    {
        recognitionThread->terminate();
        recognitionThread->wait();
    }
}

void MainWindow::updateProgress(int progress)
{
    progressBar->setValue(progress);
}

void MainWindow::updateResult(const QString& result)
{
    resultsList->addItem(result);
}

void MainWindow::setupUi()
{
    // 创建和布局UI组件
    folderPathLineEdit = new QLineEdit(this);
    saveFolderPathLineEdit = new QLineEdit(this); // 用于保存结果的文件夹路径
    startButton = new QPushButton("开始识别", this);
    stopButton = new QPushButton("停止识别", this);
    selectRecognitionFolderButton = new QPushButton("选择识别文件夹", this); // 选择识别文件夹按钮
    selectSaveFolderButton = new QPushButton("选择保存文件夹", this); // 选择保存文件夹按钮
    progressLabel = new QLabel("进度:", this);
    progressBar = new QProgressBar(this);
    resultsLabel = new QLabel("结果:", this);
    resultsList = new QListWidget(this);

    QVBoxLayout* layout = new QVBoxLayout();
    layout->addWidget(folderPathLineEdit);
    layout->addWidget(selectRecognitionFolderButton); // 添加选择识别文件夹按钮
    layout->addWidget(saveFolderPathLineEdit); // 添加用于保存结果的文件夹路径输入框
    layout->addWidget(selectSaveFolderButton); // 添加选择保存文件夹按钮
    layout->addWidget(startButton);
    layout->addWidget(stopButton);
    layout->addWidget(progressLabel);
    layout->addWidget(progressBar);
    layout->addWidget(resultsLabel);
    layout->addWidget(resultsList);

    QWidget* centralWidget = new QWidget(this);
    centralWidget->setLayout(layout);
    setCentralWidget(centralWidget);
}

void MainWindow::connectSignalsAndSlots()
{
    connect(startButton, &QPushButton::clicked, this, &MainWindow::startRecognition);
    connect(stopButton, &QPushButton::clicked, this, &MainWindow::stopRecognition);
    connect(selectRecognitionFolderButton, &QPushButton::clicked, this, &MainWindow::selectRecognitionFolder); // 连接选择识别文件夹按钮的槽函数
    connect(selectSaveFolderButton, &QPushButton::clicked, this, &MainWindow::selectSaveFolder); // 连接选择保存文件夹按钮的槽函数
}

void MainWindow::selectRecognitionFolder()
{
    QString folderPath = QFileDialog::getExistingDirectory(this, "选择识别文件夹", "", QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
    folderPathLineEdit->setText(folderPath);
}

void MainWindow::selectSaveFolder()
{
    QString saveFolderPath = QFileDialog::getExistingDirectory(this, "选择保存结果的文件夹", "", QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
    saveFolderPathLineEdit->setText(saveFolderPath);
}

项目演示

在这里插入图片描述在这里插入图片描述在这里插入图片描述

小结

特别提醒:在使用OpenCv的时候一定要配置好环境哦,这也是一个相对比较麻烦的事情,可以看看其他博主的教程!
点赞加关注,从此不迷路!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1046476.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

作为产品经理,你是如何分析和管理你的产品需求的?

作为一名产品经理&#xff0c;分析和管理产品需求是非常重要的工作。在产品开发周期中&#xff0c;需求调研、需求分析、需求管理等环节都是非常关键的&#xff0c;因为好的需求管理能够直接影响产品的质量和用户体验。 需求调研 在进行需求调研的过程中&#xff0c;我们首先…

App开发者如何从立项着手,奠定商业化基础,完成0到1转变?

随着移动互联技术的发展&#xff0c;流量即价值的观念深入人心&#xff0c;大量不同细分领域的移动应用进入市场。根据工信部公布数据&#xff0c;2023年上半年&#xff0c;我国国内市场上监测到活跃的APP数量为260万款&#xff08;包括安卓和苹果商店&#xff09;&#xff0c;…

Visual Studio 如何删除多余的空行,仅保留一行空行

1.CtrlH 打开替换窗口&#xff08;注意选择合适的查找范围&#xff09; VS2010: VS2017、VS2022: 2.复制下面正则表达式到上面的选择窗口&#xff1a; VS2010: ^(\s*)$\n\n VS2017: ^(\s*)$\n\n VS2022:^(\s*)$\n 3.下面的替换窗口皆写入 \n VS2010: \n VS2017: \n VS2022: \n …

铁路用热轧钢轨

声明 本文是学习GB-T 2585-2021 铁路用热轧钢轨. 而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 1 范围 本标准规定了铁路用钢轨的订货内容、分类、尺寸、外形、质量及允许偏差、技术要求、试验方法、检 验规则、标志及质量证明书。 本标准适用于3…

react import爆红

如上所示&#xff0c;会标红&#xff0c; 解决办法&#xff1a;在vscode内部SHiftCtrlP 输入Reload window, 如上的第一个&#xff0c;选中后回车&#xff0c;标红就没了&#xff0c;非常好用。

版本控制系统:Perforce Helix Core -2023

Perforce Helix Core是领先的版本控制系统&#xff0c;适用于需要加速大规模创新的团队。存储并跟踪您所有数字资产的更改&#xff0c;从源代码到二进制再到IP。连接您的团队&#xff0c;让他们更快地行动&#xff0c;更好地构建。 通过 Perforce 版本控制加速创新 Perforce H…

史上最严重的10起勒索软件攻击

与今天的勒索软件攻击相比&#xff0c;世界上首起勒索软件攻击简直就是小菜一碟。 1989年&#xff0c;出席世界卫生组织&#xff08;WHO&#xff09;艾滋病会议的数千名与会者回到家中&#xff0c;结果在自家的邮箱软盘里发现了一份关于感染艾滋病毒可能性的调查问卷&#xff…

手机能搜到某个wifi,电脑搜不到解决方法(也许有用)

方法一&#xff1a;更新驱动 下载驱动大师、驱动精灵等等驱动软件&#xff0c;更新网卡驱动 方法二 按 win 键&#xff0c;打开菜单 搜索 查看网络连接&#xff08;win11版本是搜这个名字&#xff09; 点击打开是这样式的 然后对 WLAN右击->属性->配置->高级 这…

等保二级测评国家收费标准是多少?统一的吗?

目前我国等保分为五个级别&#xff0c;不同级别要求和费用也不同。有小伙伴在问&#xff0c;等保二级测评国家收费标准是多少&#xff1f;统一的吗&#xff1f;这里就来给大家简单回答一下&#xff0c;仅供参考&#xff01; 等保二级测评国家收费标准是多少&#xff1f;统一的吗…

RabbitMQ消息可靠性保证机制--发送端确认

发送端确认机制 ​ RabbitMQ后来引入了一种轻量级的方式&#xff0c;叫发送方确认(publisher confirm)机制&#xff0c;生产者将信息设置成confirm&#xff08;确认&#xff09;模式&#xff0c;一旦信道进入了confirm模式&#xff0c;所有在该信道上面发送的消息都会被指派成…

python使用uiautomator2操作真机

测试环境&#xff1a;win10 64位&#xff0c;python3.10.4&#xff1b;真机&#xff0c;荣耀10青春版&#xff0c;Android版本10。 之前是在手机模拟器上操作的&#xff0c;参考我的文章python使用uiautomator2操作雷电模拟器_小小爬虾的博客-CSDN博客 一、将手机设置为开发者…

任务执行大数据量与高并发方案

大数据量高并发任务解决方案 场景 每个任务有十万条以上的数据&#xff0c;任务执行过程中对这些数据逐条做分析处理。 在同一段时间&#xff0c;会出现任务高并发执行&#xff0c;导致内存溢出 解决方案 1、分批处理 任务执行过程中&#xff0c;不一次性读取全量数据&…

将切分的图片筛选出有缺陷的

将切分的图片筛选出有缺陷的 需求代码 需求 由于之前切分的图像有一些存在没有缺陷&#xff0c;需要再次筛选 将可视化的图像更改后缀 更改为xml的 可视化代码 可视化后只有7000多个图像 原本的图像有1W多张 代码 # 按照xml文件删除对应的图片 # coding: utf-8 from P…

Java比较器之equals、comparable、comparator

文章目录 前言一、基本类型比较1.2.equals3.和equals的区别 二、对象的比较1.覆写基类的equals2.基于Comparable接口类的比较3.基于Comparator比较器比较4.三种方式对比 前言 在Java中&#xff0c;基本类型的对象可以直接比较&#xff0c;而自定义类型&#xff0c;默认是用equ…

秋招面经记录

秋招面经记录 MySQLRedis项目分布式框架java网络数据结构设计模式HR手撕 MySQL Mysql中有1000万条数据&#xff0c;每次查询10条&#xff0c;该如何优化&#xff08;答&#xff1a;Limit子查询优化&#xff09; select t.* from t_topic t LIMIT 90000,10; 对上面的mysql语句说…

【Java 进阶篇】数据定义语言(DDL)详解

数据定义语言&#xff08;DDL&#xff09;是SQL&#xff08;结构化查询语言&#xff09;的一部分&#xff0c;它用于定义、管理和控制数据库的结构和元素。DDL允许数据库管理员、开发人员和其他用户创建、修改和删除数据库对象&#xff0c;如表、索引、视图等。在本文中&#x…

【C++杂货店】类和对象(上)

【C杂货店】类和对象&#xff08;上&#xff09; 一、面向过程和面向对象初步认识二、类的引入三、类的定义四、类的访问限定符及封装4.1 访问限定符4.2 封装 五、类的作用域六、类的实例化七、类对象模型7.1 类对象的存储规则7.2 例题7.3结构体内存对齐规则 八、this指针8.2 t…

【Spring Cloud】认识微服务架构,拆分简单的 Demo 实现服务的远程调用

文章目录 前言一、认识微服务1.1 服务架构的演变&#xff1a;从单体到微服务单体架构分布式架构微服务架构 1.2 微服务技术的对比&#xff1a;Dubbo、Spring Cloud、Spring Cloud Alibaba技术对比公司需求的选择 1.3 Spring Cloud&#xff1a;微服务框架的精华什么是 Spring Cl…

【Vue.js】使用ElementUI实现增删改查(CRUD)及表单验证

前言&#xff1a; 本文根据上篇实现数据表格(查所有)完善增删改功能&#xff0c;数据表格》查看数据表格的实现链接 一&#xff0c;增删改查 ①后端Controller(增删改查方法)&#xff1a; package com.zking.ssm.controller;import com.zking.ssm.model.Book; import com.z…

新能源汽车行业出口ERP管理解决方案

中国汽车企业以史无前例的规模进军慕尼黑车展。本届展会&#xff0c;中国汽车参展企业数量达50家&#xff0c;是2021年的两倍。欧洲销售的新型电动汽车中&#xff0c;8%由中国品牌制造。2022年上半年&#xff0c;中国电动汽车的平均价格不到3.2万欧元&#xff08;3.5万美元&…