提交 079ad695 编写于 作者: H HydrogenSulfate

Merge branch 'develop' into add_ShiTuV2_tipc

......@@ -4,64 +4,75 @@
## 简介
飞桨图像识别套件PaddleClas是飞桨为工业界和学术界所准备的一个图像识别和图像分类任务的工具集,助力使用者训练出更好的视觉模型和应用落地。
<div align="center">
<img src="./docs/images/class_simple.gif" width = "600" />
<p>PULC实用图像分类模型效果展示</p>
</div>
&nbsp;
飞桨图像识别套件PaddleClas是飞桨为工业界和学术界所准备的一个图像识别和图像分类任务的工具集,助力使用者训练出更好的视觉模型和应用落地。&nbsp;
<div align="center">
<img src="./docs/images/recognition.gif" width = "400" />
<p>PP-ShiTu图像识别系统效果展示</p>
<p>PP-ShiTuV2图像识别系统效果展示</p>
</div>
## 近期更新
- 📢将于**6月15-6月17日晚20:30** 进行为期三天的课程直播,详细介绍超轻量图像分类方案,对各场景模型优化原理及使用方式进行拆解,之后还有产业案例全流程实操,对各类痛难点解决方案进行手把手教学,加上现场互动答疑,抓紧扫码上车吧!
<div align="center">
<img src="https://user-images.githubusercontent.com/45199522/173483779-2332f990-4941-4f8d-baee-69b62035fc31.png" width = "200" height = "200"/>
<img src="./docs/images/class_simple.gif" width = "600" />
<p>PULC实用图像分类模型效果展示</p>
</div>
- 🔥️ 2022.6.15 发布[PULC超轻量图像分类实用方案](docs/zh_CN/PULC/PULC_train.md),CPU推理3ms,精度比肩SwinTransformer,覆盖人、车、OCR场景九大常见任务。
- 2022.5.26 [飞桨产业实践范例直播课](http://aglc.cn/v-c4FAR),解读**超轻量重点区域人员出入管理方案**
- 2022.5.23 新增[人员出入管理范例库](https://aistudio.baidu.com/aistudio/projectdetail/4094475),具体内容可以在 AI Studio 上体验。
## 近期更新
- 🔥️ 发布[PP-ShiTuV2](./docs/zh_CN/PPShiTu/PPShiTuV2_introduction.md),recall1精度提升10个点,覆盖20+识别场景,新增库管理工具,Android Demo全新体验。
- 2022.9.4 新增[生鲜产品自主结算范例库](https://aistudio.baidu.com/aistudio/projectdetail/4486158),具体内容可以在AI Studio上体验。
- 2022.6.15 发布[PULC超轻量图像分类实用方案](docs/zh_CN/PULC/PULC_train.md),CPU推理3ms,精度比肩SwinTransformer,覆盖人、车、OCR场景九大常见任务。
- 2022.5.23 新增[人员出入管理范例库](https://aistudio.baidu.com/aistudio/projectdetail/4094475),具体内容可以在 AI Studio 上体验。
- 2022.5.20 上线[PP-HGNet](./docs/zh_CN/models/PP-HGNet.md), [PP-LCNetv2](./docs/zh_CN/models/PP-LCNetV2.md)
- 2022.4.21 新增 CVPR2022 oral论文 [MixFormer](https://arxiv.org/pdf/2204.02557.pdf) 相关[代码](https://github.com/PaddlePaddle/PaddleClas/pull/1820/files)
- [more](./docs/zh_CN/others/update_history.md)
## 特性
PaddleClas发布了[PP-HGNet](docs/zh_CN/models/PP-HGNet.md)[PP-LCNetv2](docs/zh_CN/models/PP-LCNetV2.md)[PP-LCNet](docs/zh_CN/models/PP-LCNet.md)[SSLD半监督知识蒸馏方案](docs/zh_CN/advanced_tutorials/ssld.md)等算法,
并支持多种图像分类、识别相关算法,在此基础上打造[PULC超轻量图像分类方案](docs/zh_CN/PULC/PULC_quickstart.md)[PP-ShiTu图像识别系统](./docs/zh_CN/quick_start/quick_start_recognition.md)
![](https://user-images.githubusercontent.com/19523330/173273046-239a42da-c88d-4c2c-94b1-2134557afa49.png)
![](https://user-images.githubusercontent.com/11568925/188055076-7b0e3492-55cb-420f-b61a-160d4dd0bb0d.png)
## 欢迎加入技术交流群
* 您可以扫描下面的微信/QQ二维码(添加小助手微信并回复“C”),加入PaddleClas微信交流群,获得更高效的问题答疑,与各行各业开发者充分交流,期待您的加入。
* 欢迎加入PaddleClas 微信用户群(扫码填写问卷即可入群)
<div align="center">
<img src="https://user-images.githubusercontent.com/48054808/160531099-9811bbe6-cfbb-47d5-8bdb-c2b40684d7dd.png" width="200"/>
<img src="https://user-images.githubusercontent.com/80816848/164383225-e375eb86-716e-41b4-a9e0-4b8a3976c1aa.jpg" width="200"/>
<img src="https://user-images.githubusercontent.com/45199522/173483779-2332f990-4941-4f8d-baee-69b62035fc31.png" width = "200" height = "200"/>
</div>
## 快速体验
PULC超轻量图像分类方案快速体验:[点击这里](docs/zh_CN/PULC/PULC_quickstart.md)
PP-ShiTu图像识别快速体验:[点击这里](./docs/zh_CN/quick_start/quick_start_recognition.md)
<div align="center">
<img src="./docs/images/quick_start/android_demo/PPShiTu_qrcode.png" width = "400" />
<p>PP-ShiTuV2 Android Demo</p>
</div>
## 文档教程
- [环境准备](docs/zh_CN/installation/install_paddleclas.md)
- [PP-ShiTuV2图像识别系统介绍](./docs/zh_CN/PPShiTu/PPShiTuV2_introduction.md)
- [图像识别快速体验](docs/zh_CN/quick_start/quick_start_recognition.md)
- 模块介绍
- [主体检测](./docs/zh_CN/image_recognition_pipeline/mainbody_detection.md)
- [特征提取模型](./docs/zh_CN/image_recognition_pipeline/feature_extraction.md)
- [向量检索](./docs/zh_CN/image_recognition_pipeline/vector_search.md)
- [哈希编码](docs/zh_CN/image_recognition_pipeline/)
- [模型训练](docs/zh_CN/models_training/recognition.md)
- 推理部署
- [基于python预测引擎推理](docs/zh_CN/inference_deployment/python_deploy.md#2)
- [基于C++预测引擎推理](deploy/cpp_shitu/readme.md)
- [服务化部署](docs/zh_CN/inference_deployment/recognition_serving_deploy.md)
- [端侧部署](docs/zh_CN/inference_deployment/lite_shitu.md)
- [库管理工具](docs/zh_CN/inference_deployment/shitu_gallery_manager.md)
- [20+应用场景库](docs/zh_CN/introduction/ppshitu_application_scenarios.md)
- [PULC超轻量图像分类实用方案](docs/zh_CN/PULC/PULC_train.md)
- [超轻量图像分类快速体验](docs/zh_CN/PULC/PULC_quickstart.md)
- [超轻量图像分类模型库](docs/zh_CN/PULC/PULC_model_list.md)
......@@ -82,19 +93,6 @@ PP-ShiTu图像识别快速体验:[点击这里](./docs/zh_CN/quick_start/quick
- [端侧部署](docs/zh_CN/inference_deployment/paddle_lite_deploy.md)
- [Paddle2ONNX模型转化与预测](deploy/paddle2onnx/readme.md)
- [模型压缩](deploy/slim/README.md)
- [PP-ShiTu图像识别系统介绍](#图像识别系统介绍)
- [图像识别快速体验](docs/zh_CN/quick_start/quick_start_recognition.md)
- 模块介绍
- [主体检测](./docs/zh_CN/image_recognition_pipeline/mainbody_detection.md)
- [特征提取模型](./docs/zh_CN/image_recognition_pipeline/feature_extraction.md)
- [向量检索](./docs/zh_CN/image_recognition_pipeline/vector_search.md)
- [哈希编码](docs/zh_CN/image_recognition_pipeline/)
- [模型训练](docs/zh_CN/models_training/recognition.md)
- 推理部署
- [基于python预测引擎推理](docs/zh_CN/inference_deployment/python_deploy.md#2)
- [基于C++预测引擎推理](deploy/cpp_shitu/readme.md)
- [服务化部署](docs/zh_CN/inference_deployment/recognition_serving_deploy.md)
- [端侧部署](deploy/lite_shitu/README.md)
- PP系列骨干网络模型
- [PP-HGNet](docs/zh_CN/models/PP-HGNet.md)
- [PP-LCNetv2](docs/zh_CN/models/PP-LCNetV2.md)
......@@ -117,59 +115,75 @@ PP-ShiTu图像识别快速体验:[点击这里](./docs/zh_CN/quick_start/quick
- [许可证书](#许可证书)
- [贡献代码](#贡献代码)
<a name="PULC超轻量图像分类方案"></a>
## PULC超轻量图像分类方案
<div align="center">
<img src="https://user-images.githubusercontent.com/19523330/173011854-b10fcd7a-b799-4dfd-a1cf-9504952a3c44.png" width = "800" />
</div>
PULC融合了骨干网络、数据增广、蒸馏等多种前沿算法,可以自动训练得到轻量且高精度的图像分类模型。
PaddleClas提供了覆盖人、车、OCR场景九大常见任务的分类模型,CPU推理3ms,精度比肩SwinTransformer。
<a name="图像识别系统介绍"></a>
## PP-ShiTu图像识别系统
## PP-ShiTuV2图像识别系统
<div align="center">
<img src="./docs/images/structure.jpg" width = "800" />
</div>
PP-ShiTu是一个实用的轻量级通用图像识别系统,主要由主体检测、特征学习和向量检索三个模块组成。该系统从骨干网络选择和调整、损失函数的选择、数据增强、学习率变换策略、正则化参数选择、预训练模型使用以及模型裁剪量化8个方面,采用多种策略,对各个模块的模型进行优化,最终得到在CPU上仅0.2s即可完成10w+库的图像识别的系统。更多细节请参考[PP-ShiTu技术方案](https://arxiv.org/pdf/2111.00775.pdf)
<a name="分类效果展示"></a>
## PULC实用图像分类模型效果展示
<div align="center">
<img src="docs/images/classification.gif">
</div>
PP-ShiTuV2是一个实用的轻量级通用图像识别系统,主要由主体检测、特征学习和向量检索三个模块组成。该系统从骨干网络选择和调整、损失函数的选择、数据增强、学习率变换策略、正则化参数选择、预训练模型使用以及模型裁剪量化多个方面,采用多种策略,对各个模块的模型进行优化,PP-ShiTuV2相比V1而已,Recall1提升10+个点。更多细节请参考[PP-ShiTuV2详细介绍](./docs/zh_CN/PPShiTu/PPShiTuV2_introduction.md)
<a name="识别效果展示"></a>
## PP-ShiTu图像识别系统效果展示
## PP-ShiTuV2图像识别系统效果展示
- 瓶装饮料识别
<div align="center">
<img src="docs/images/drink_demo.gif">
</div>
- 商品识别
<div align="center">
<img src="https://user-images.githubusercontent.com/18028216/122769644-51604f80-d2d7-11eb-8290-c53b12a5c1f6.gif" width = "400" />
</div>
- 动漫人物识别
<div align="center">
<img src="https://user-images.githubusercontent.com/18028216/122769746-6b019700-d2d7-11eb-86df-f1d710999ba6.gif" width = "400" />
</div>
- logo识别
<div align="center">
<img src="https://user-images.githubusercontent.com/18028216/122769837-7fde2a80-d2d7-11eb-9b69-04140e9d785f.gif" width = "400" />
</div>
- 车辆识别
<div align="center">
<img src="https://user-images.githubusercontent.com/18028216/122769916-8ec4dd00-d2d7-11eb-8c60-42d89e25030c.gif" width = "400" />
</div>
<a name="PULC超轻量图像分类方案"></a>
## PULC超轻量图像分类方案
<div align="center">
<img src="https://user-images.githubusercontent.com/19523330/173011854-b10fcd7a-b799-4dfd-a1cf-9504952a3c44.png" width = "800" />
</div>
PULC融合了骨干网络、数据增广、蒸馏等多种前沿算法,可以自动训练得到轻量且高精度的图像分类模型。
PaddleClas提供了覆盖人、车、OCR场景九大常见任务的分类模型,CPU推理3ms,精度比肩SwinTransformer。
<a name="分类效果展示"></a>
## PULC实用图像分类模型效果展示
<div align="center">
<img src="docs/images/classification.gif">
</div>
<a name="许可证书"></a>
## 许可证书
......
......@@ -22,7 +22,7 @@ PreProcess:
scale: 0.00392157
mean: [0.485, 0.456, 0.406]
std: [0.229, 0.224, 0.225]
order: ''
order: ""
channel_num: 3
- ToCHWImage:
......
......@@ -6,7 +6,7 @@ Global:
threshold: 0.2
max_det_results: 1
label_list:
- foreground
- foreground
# inference engine config
use_gpu: True
......@@ -30,5 +30,5 @@ DetPreProcess:
mean: [0.485, 0.456, 0.406]
std: [0.229, 0.224, 0.225]
- DetPermute: {}
DetPostProcess: {}
\ No newline at end of file
DetPostProcess: {}
Global:
infer_imgs: "./drink_dataset_v1.0/test_images/hongniu_1.jpg"
infer_imgs: "./drink_dataset_v2.0/test_images/100.jpeg"
det_inference_model_dir: "./models/picodet_PPLCNet_x2_5_mainbody_lite_v1.0_infer"
rec_inference_model_dir: "./models/general_PPLCNet_x2_5_lite_v1.0_infer"
rec_inference_model_dir: "./models/general_PPLCNetV2_base_pretrained_v1.0_infer"
rec_nms_thresold: 0.05
batch_size: 1
......@@ -9,7 +9,7 @@ Global:
threshold: 0.2
max_det_results: 5
label_list:
- foreground
- foreground
use_gpu: True
enable_mkldnn: False
......@@ -43,7 +43,7 @@ RecPreProcess:
scale: 0.00392157
mean: [0.485, 0.456, 0.406]
std: [0.229, 0.224, 0.225]
order: ''
order: ""
- ToCHWImage:
RecPostProcess: null
......@@ -51,13 +51,13 @@ RecPostProcess: null
# indexing engine config
IndexProcess:
index_method: "HNSW32" # supported: HNSW32, IVF, Flat
image_root: "./drink_dataset_v1.0/gallery"
index_dir: "./drink_dataset_v1.0/index"
data_file: "./drink_dataset_v1.0/gallery/drink_label.txt"
image_root: "./drink_dataset_v2.0/gallery"
index_dir: "./drink_dataset_v2.0/index"
data_file: "./drink_dataset_v2.0/gallery/drink_label.txt"
index_operation: "new" # suported: "append", "remove", "new"
delimiter: "\t"
dist_type: "IP"
embedding_size: 512
batch_size: 32
return_k: 5
score_thres: 0.4
\ No newline at end of file
score_thres: 0.4
Global:
infer_imgs: "./drink_dataset_v1.0/test_images/nongfu_spring.jpeg"
infer_imgs: "./drink_dataset_v2.0/test_images/100.jpeg"
det_inference_model_dir: "./models/picodet_PPLCNet_x2_5_mainbody_lite_v1.0_infer"
rec_inference_model_dir: "./models/general_PPLCNet_x2_5_lite_v1.0_infer"
rec_inference_model_dir: "./models/general_PPLCNetV2_base_pretrained_v1.0_infer"
rec_nms_thresold: 0.05
batch_size: 1
......@@ -9,7 +9,7 @@ Global:
threshold: 0.2
max_det_results: 5
label_list:
- foreground
- foreground
use_gpu: True
enable_mkldnn: True
......@@ -38,12 +38,15 @@ DetPostProcess: {}
RecPreProcess:
transform_ops:
- ResizeImage:
size: 224
size: [224, 224]
return_numpy: False
interpolation: bilinear
backend: cv2
- NormalizeImage:
scale: 0.00392157
scale: 1.0/255.0
mean: [0.485, 0.456, 0.406]
std: [0.229, 0.224, 0.225]
order: ''
order: hwc
- ToCHWImage:
RecPostProcess: null
......@@ -51,13 +54,13 @@ RecPostProcess: null
# indexing engine config
IndexProcess:
index_method: "HNSW32" # supported: HNSW32, IVF, Flat
image_root: "./drink_dataset_v1.0/gallery/"
index_dir: "./drink_dataset_v1.0/index"
data_file: "./drink_dataset_v1.0/gallery/drink_label.txt"
image_root: "./drink_dataset_v2.0/gallery/"
index_dir: "./drink_dataset_v2.0/index"
data_file: "./drink_dataset_v2.0/gallery/drink_label.txt"
index_operation: "new" # suported: "append", "remove", "new"
delimiter: "\t"
dist_type: "IP"
embedding_size: 512
batch_size: 32
return_k: 5
score_thres: 0.5
\ No newline at end of file
score_thres: 0.5
Global:
infer_imgs: "./images/wangzai.jpg"
rec_inference_model_dir: "./models/product_ResNet50_vd_aliproduct_v1.0_infer"
rec_inference_model_dir: "./models/general_PPLCNetV2_base_pretrained_v1.0_infer"
batch_size: 1
use_gpu: False
enable_mkldnn: True
......@@ -22,7 +22,7 @@ RecPreProcess:
scale: 0.00392157
mean: [0.485, 0.456, 0.406]
std: [0.229, 0.224, 0.225]
order: ''
order: ""
- ToCHWImage:
RecPostProcess: null
\ No newline at end of file
RecPostProcess: null
# 服务器端C++预测
本教程将介绍在服务器端部署PP-ShiTU的详细步骤。
本教程将介绍在服务器端部署PP-ShiTu的详细步骤。
## 目录
......@@ -30,39 +30,39 @@
- 下载最新版本cmake
```shell
# 当前版本最新为3.22.0,根据实际情况自行下载,建议最新版本
wget https://github.com/Kitware/CMake/releases/download/v3.22.0/cmake-3.22.0.tar.gz
tar xf cmake-3.22.0.tar.gz
```
```shell
# 当前版本最新为3.22.0,根据实际情况自行下载,建议最新版本
wget https://github.com/Kitware/CMake/releases/download/v3.22.0/cmake-3.22.0.tar.gz
tar -xf cmake-3.22.0.tar.gz
```
最终可以在当前目录下看到`cmake-3.22.0/`的文件夹。
最终可以在当前目录下看到`cmake-3.22.0/`的文件夹。
- 编译cmake,首先设置came源码路径(`root_path`)以及安装路径(`install_path`),`root_path`为下载的came源码路径,`install_path`为came的安装路径。在本例中,源码路径即为当前目录下的`cmake-3.22.0/`
- 编译cmake,首先设置cmake源码路径(`root_path`)以及安装路径(`install_path`),`root_path`为下载的cmake源码路径,`install_path`为cmake的安装路径。在本例中,源码路径即为当前目录下的`cmake-3.22.0/`
```shell
cd ./cmake-3.22.0
export root_path=$PWD
export install_path=${root_path}/cmake
```
```shell
cd ./cmake-3.22.0
export root_path=$PWD
export install_path=${root_path}/cmake
```
- 然后在cmake源码路径下,按照下面的方式进行编译
- 然后在cmake源码路径下,执行以下命令进行编译
```shell
./bootstrap --prefix=${install_path}
make -j
make install
```
```shell
./bootstrap --prefix=${install_path}
make -j
make install
```
- 设置环境变量
- 编译安装cmake完成后,设置cmake的环境变量供后续程序使用
```shell
export PATH=${install_path}/bin:$PATH
#检查是否正常使用
cmake --version
```
```shell
export PATH=${install_path}/bin:$PATH
#检查是否正常使用
cmake --version
```
此时,cmake就可以使用了
此时cmake就可以正常使用了
<a name="1.2"></a>
......@@ -70,61 +70,66 @@ cmake --version
* 首先需要从opencv官网上下载在Linux环境下源码编译的包,以3.4.7版本为例,下载及解压缩命令如下:
```
wget https://github.com/opencv/opencv/archive/3.4.7.tar.gz
tar -xvf 3.4.7.tar.gz
```
```shell
wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/test/opencv-3.4.7.tar.gz
tar -xvf 3.4.7.tar.gz
```
最终可以在当前目录下看到`opencv-3.4.7/`的文件夹。
最终可以在当前目录下看到`opencv-3.4.7/`的文件夹。
* 编译opencv,首先设置opencv源码路径(`root_path`)以及安装路径(`install_path`),`root_path`为下载的opencv源码路径,`install_path`为opencv的安装路径。在本例中,源码路径即为当前目录下的`opencv-3.4.7/`
```shell
cd ./opencv-3.4.7
export root_path=$PWD
export install_path=${root_path}/opencv3
```
* 然后在opencv源码路径下,按照下面的方式进行编译。
```shell
# 进入deploy/cpp_shitu目录
cd deploy/cpp_shitu
```shell
rm -rf build
mkdir build
cd build
# 安装opencv
cd ./opencv-3.4.7
export root_path=$PWD
export install_path=${root_path}/opencv3
```
cmake .. \
-DCMAKE_INSTALL_PREFIX=${install_path} \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=OFF \
-DWITH_IPP=OFF \
-DBUILD_IPP_IW=OFF \
-DWITH_LAPACK=OFF \
-DWITH_EIGEN=OFF \
-DCMAKE_INSTALL_LIBDIR=lib64 \
-DWITH_ZLIB=ON \
-DBUILD_ZLIB=ON \
-DWITH_JPEG=ON \
-DBUILD_JPEG=ON \
-DWITH_PNG=ON \
-DBUILD_PNG=ON \
-DWITH_TIFF=ON \
-DBUILD_TIFF=ON
* 然后在opencv源码路径下,按照下面的方式进行编译。
make -j
make install
```
```shell
rm -rf build
mkdir build
cd build
cmake .. \
-DCMAKE_INSTALL_PREFIX=${install_path} \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=OFF \
-DWITH_IPP=OFF \
-DBUILD_IPP_IW=OFF \
-DWITH_LAPACK=OFF \
-DWITH_EIGEN=OFF \
-DCMAKE_INSTALL_LIBDIR=lib64 \
-DWITH_ZLIB=ON \
-DBUILD_ZLIB=ON \
-DWITH_JPEG=ON \
-DBUILD_JPEG=ON \
-DWITH_PNG=ON \
-DBUILD_PNG=ON \
-DWITH_TIFF=ON \
-DBUILD_TIFF=ON
make -j
make install
```
* `make install`完成之后,会在该文件夹下生成opencv头文件和库文件,用于后面的PaddleClas代码编译。
以opencv3.4.7版本为例,最终在安装路径下的文件结构如下所示。**注意**:不同的opencv版本,下述的文件结构可能不同。
以opencv3.4.7版本为例,最终在安装路径下的文件结构如下所示。**注意**:不同的opencv版本,下述的文件结构可能不同。
```
opencv3/
|-- bin
|-- include
|-- lib64
|-- share
```
```log
opencv3/
├── bin
├── include
├── lib
├── lib64
└── share
```
<a name="1.3"></a>
......@@ -139,44 +144,48 @@ opencv3/
* 如果希望获取最新预测库特性,可以从Paddle github上克隆最新代码,源码编译预测库。
* 可以参考[Paddle预测库官网](https://www.paddlepaddle.org.cn/documentation/docs/zh/develop/guides/05_inference_deployment/inference/build_and_install_lib_cn.html#id16)的说明,从github上获取Paddle代码,然后进行编译,生成最新的预测库。使用git获取代码方法如下。
```shell
git clone https://github.com/PaddlePaddle/Paddle.git
```
```shell
# 进入deploy/cpp_shitu目录
cd deploy/cpp_shitu
* 进入Paddle目录后,使用如下方法编译。
git clone https://github.com/PaddlePaddle/Paddle.git
```
```shell
rm -rf build
mkdir build
cd build
* 进入Paddle目录后,使用如下方法编译。
cmake .. \
-DWITH_CONTRIB=OFF \
-DWITH_MKL=ON \
-DWITH_MKLDNN=ON \
-DWITH_TESTING=OFF \
-DCMAKE_BUILD_TYPE=Release \
-DWITH_INFERENCE_API_TEST=OFF \
-DON_INFER=ON \
-DWITH_PYTHON=ON
make -j
make inference_lib_dist
```
```shell
rm -rf build
mkdir build
cd build
cmake .. \
-DWITH_CONTRIB=OFF \
-DWITH_MKL=ON \
-DWITH_MKLDNN=ON \
-DWITH_TESTING=OFF \
-DCMAKE_BUILD_TYPE=Release \
-DWITH_INFERENCE_API_TEST=OFF \
-DON_INFER=ON \
-DWITH_PYTHON=ON
make -j
make inference_lib_dist
```
更多编译参数选项可以参考[Paddle C++预测库官网](https://www.paddlepaddle.org.cn/documentation/docs/zh/develop/guides/05_inference_deployment/inference/build_and_install_lib_cn.html#id16)
更多编译参数选项可以参考[Paddle C++预测库官网](https://www.paddlepaddle.org.cn/documentation/docs/zh/develop/guides/05_inference_deployment/inference/build_and_install_lib_cn.html#id16)
* 编译完成之后,可以在`build/paddle_inference_install_dir/`文件下看到生成了以下文件及文件夹。
```
build/paddle_inference_install_dir/
|-- CMakeCache.txt
|-- paddle
|-- third_party
|-- version.txt
```
```log
build/paddle_inference_install_dir/
├── CMakeCache.txt
├── paddle
├── third_party
└── version.txt
```
其中`paddle`就是之后进行C++预测时所需的Paddle库,`version.txt`中包含当前预测库的版本信息。
其中`paddle`就是之后进行C++预测时所需的Paddle库,`version.txt`中包含当前预测库的版本信息。
<a name="1.3.2"></a>
......@@ -187,33 +196,41 @@ build/paddle_inference_install_dir/
`https://paddle-inference-lib.bj.bcebos.com/2.1.1-gpu-cuda10.2-cudnn8.1-mkl-gcc8.2/paddle_inference.tgz``develop`版本为例,使用下述命令下载并解压:
```shell
wget https://paddle-inference-lib.bj.bcebos.com/2.1.1-gpu-cuda10.2-cudnn8.1-mkl-gcc8.2/paddle_inference.tgz
```shell
# 进入deploy/cpp_shitu目录
cd deploy/cpp_shitu
tar -xvf paddle_inference.tgz
```
wget https://paddle-inference-lib.bj.bcebos.com/2.1.1-gpu-cuda10.2-cudnn8.1-mkl-gcc8.2/paddle_inference.tgz
tar -xvf paddle_inference.tgz
```
最终会在当前的文件夹中生成`paddle_inference/`的子文件夹。
最终会在当前的文件夹中生成`paddle_inference/`的子文件夹。
<a name="1.4"></a>
### 1.4 安装faiss库
在安装`faiss`前,请安装`openblas``ubuntu`系统中安装命令如下:
```shell
# 下载 faiss
git clone https://github.com/facebookresearch/faiss.git
cd faiss
export faiss_install_path=$PWD/faiss_install
cmake -B build . -DFAISS_ENABLE_PYTHON=OFF -DCMAKE_INSTALL_PREFIX=${faiss_install_path}
make -C build -j faiss
make -C build install
apt-get install libopenblas-dev
```
在安装`faiss`前,请安装`openblas``ubuntu`系统中安装命令如下:
然后按照以下命令编译并安装faiss
```shell
apt-get install libopenblas-dev
# 进入deploy/cpp_shitu目录
cd deploy/cpp_shitu
# 下载 faiss
git clone https://github.com/facebookresearch/faiss.git
cd faiss
export faiss_install_path=$PWD/faiss_install
cmake -B build . -DFAISS_ENABLE_PYTHON=OFF -DCMAKE_INSTALL_PREFIX=${faiss_install_path}
make -C build -j faiss
make -C build install
```
注意本教程以安装faiss cpu版本为例,安装时请参考[faiss](https://github.com/facebookresearch/faiss)官网文档,根据需求自行安装。
......@@ -224,12 +241,14 @@ apt-get install libopenblas-dev
编译命令如下,其中Paddle C++预测库、opencv等其他依赖库的地址需要换成自己机器上的实际地址。同时,编译过程中需要下载编译`yaml-cpp`等C++库,请保持联网环境。
```shell
# 进入deploy/cpp_shitu目录
cd deploy/cpp_shitu
sh tools/build.sh
```
具体地,`tools/build.sh`中内容如下,请根据具体路径修改。
具体地,`tools/build.sh`中内容如下,请根据具体路径和配置情况进行修改。
```shell
OPENCV_DIR=${opencv_install_dir}
......@@ -261,14 +280,13 @@ cd ..
上述命令中,
* `OPENCV_DIR`为opencv编译安装的地址(本例中为`opencv-3.4.7/opencv3`文件夹的路径);
* `LIB_DIR`为下载的Paddle预测库(`paddle_inference`文件夹),或编译生成的Paddle预测库(`build/paddle_inference_install_dir`文件夹)的路径;
* `CUDA_LIB_DIR`为cuda库文件地址,在docker中为`/usr/local/cuda/lib64`
* `CUDNN_LIB_DIR`为cudnn库文件地址,在docker中为`/usr/lib/x86_64-linux-gnu/`
* `TENSORRT_DIR`是tensorrt库文件地址,在dokcer中为`/usr/local/TensorRT6-cuda10.0-cudnn7/`,TensorRT需要结合GPU使用。
* `FAISS_DIR`是faiss的安装地址
* `FAISS_WITH_MKL`是指在编译faiss的过程中,是否使用了mkldnn,本文档中编译faiss,没有使用,而使用了openblas,故设置为`OFF`,若使用了mkldnn,则为`ON`.
* `OPENCV_DIR`:opencv编译安装的地址(本例中为`opencv-3.4.7/opencv3`文件夹的路径);
* `LIB_DIR`:下载的Paddle预测库(`paddle_inference`文件夹),或编译生成的Paddle预测库(`build/paddle_inference_install_dir`文件夹)的路径;
* `CUDA_LIB_DIR`:cuda库文件地址,在docker中为`/usr/local/cuda/lib64`
* `CUDNN_LIB_DIR`:cudnn库文件地址,在docker中为`/usr/lib/x86_64-linux-gnu/`
* `TENSORRT_DIR`:tensorrt库文件地址,在dokcer中为`/usr/local/TensorRT6-cuda10.0-cudnn7/`,TensorRT需要结合GPU使用。
* `FAISS_DIR`:faiss的安装地址
* `FAISS_WITH_MKL`:指在编译faiss的过程中是否使用mkldnn,本文档中编译faiss没有使用,而使用了openblas,故设置为`OFF`,若使用了mkldnn则为`ON`.
在执行上述命令,编译完成之后,会在当前路径下生成`build`文件夹,其中生成一个名为`pp_shitu`的可执行文件。
......@@ -276,60 +294,68 @@ cd ..
## 3. 运行demo
- 请参考[识别快速开始文档](../../docs/zh_CN/quick_start/quick_start_recognition.md),下载好相应的 轻量级通用主体检测模型、轻量级通用识别模型及瓶装饮料测试数据并解压。
- 按照如下命令下载好相应的轻量级通用主体检测模型、轻量级通用识别模型及瓶装饮料测试数据并解压。
```shell
# 进入deploy目录
cd deploy/
mkdir models
cd models
# 下载并解压主体检测模型
wget https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/rec/models/inference/picodet_PPLCNet_x2_5_mainbody_lite_v1.0_infer.tar
tar -xf picodet_PPLCNet_x2_5_mainbody_lite_v1.0_infer.tar
wget https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/rec/models/inference/general_PPLCNet_x2_5_lite_v1.0_infer.tar
tar -xf general_PPLCNet_x2_5_lite_v1.0_infer.tar
# 下载并解压特征提取模型
wget https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/rec/models/inference/PP-ShiTuV2/general_PPLCNetV2_base_pretrained_v1.0_infer.tar
tar -xf general_PPLCNetV2_base_pretrained_v1.0_infer.tar
cd ..
mkdir data
cd data
wget https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/rec/data/drink_dataset_v1.0.tar
tar -xf drink_dataset_v1.0.tar
wget https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/rec/data/drink_dataset_v2.0.tar
tar -xf drink_dataset_v2.0.tar
cd ..
```
- 将相应的yaml文件拷到当前文件夹下
```shell
cp ../configs/inference_drink.yaml .
cp ../configs/inference_drink.yaml ./
```
-`inference_drink.yaml`中的相对路径,改成基于本目录的路径或者绝对路径。涉及到的参数有
-`inference_drink.yaml`中的相对路径,改成基于 `deploy/cpp_shitu` 目录的相对路径或者绝对路径。涉及到的参数有
- Global.infer_imgs :此参数可以是具体的图像地址,也可以是图像集所在的目录
- Global.det_inference_model_dir : 检测模型存储目录
- Global.rec_inference_model_dir : 识别模型存储目录
- IndexProcess.index_dir : 检索库的存储目录,在示例中,检索库在下载的demo数据中。
- `Global.infer_imgs` :此参数可以是具体的图像地址,也可以是图像集所在的目录
- `Global.det_inference_model_dir` : 检测模型存储目录
- `Global.rec_inference_model_dir` : 识别模型存储目录
- `IndexProcess.index_dir` : 检索库的存储目录,在示例中,检索库在下载的demo数据中。
- 字典转换
- 标签文件转换
由于python的检索库的字典,使用`pickle`进行的序列化存储,导致C++不方便读取,因此进行转换
由于python的检索库的字典是使用`pickle`转换得到的序列化存储结果,导致C++不方便读取,因此需要先转换成普通的文本文件。
```shell
python tools/transform_id_map.py -c inference_drink.yaml
python3.7 tools/transform_id_map.py -c inference_drink.yaml
```
转换成功后,在`IndexProcess.index_dir`目录下生成`id_map.txt`方便c++ 读取。
转换成功后,在`IndexProcess.index_dir`目录下生成`id_map.txt`以便在C++推理时读取。
- 执行程序
```shell
./build/pp_shitu -c inference_drink.yaml
# or
./build/pp_shitu -config inference_drink.yaml
```
若对图像集进行检索,则可能得到,如下结果。注意,此结果只做展示,具体以实际运行结果为准。
`drink_dataset_v2.0/test_images/nongfu_spring.jpeg` 作为输入图像,则执行上述推理命令可以得到如下结果
同时,需注意的是,由于opencv 版本问题,会导致图像在预处理的过程中,resize产生细微差别,导致python 和c++结果,轻微不同,如bbox相差几个像素,检索结果小数点后3位diff等。但不会改变最终检索label。
```log
../../deploy/drink_dataset_v2.0/test_images/nongfu_spring.jpeg:
result0: bbox[0, 0, 729, 1094], score: 0.688691, label: 农夫山泉-饮用天然水
```
![](../../docs/images/quick_start/shitu_c++_result.png)
由于python和C++的opencv实现存在部分不同,可能导致python推理和C++推理结果有微小差异。但基本不影响最终的检索结果。
<a name="4"></a>
......
# PP-ShiTu在Paddle-Lite端侧部署
本教程将介绍基于[Paddle Lite](https://github.com/PaddlePaddle/Paddle-Lite) 在移动端部署PaddleClas PP-ShiTu模型的详细步骤。
Paddle Lite是飞桨轻量化推理引擎,为手机、IoT端提供高效推理能力,并广泛整合跨平台硬件,为端侧部署及应用落地问题提供轻量化的部署方案。
## 1. 准备环境
### 运行准备
- 电脑(编译Paddle Lite)
- 安卓手机(armv7或armv8)
### 1.1 准备交叉编译环境
交叉编译环境用于编译 Paddle Lite 和 PaddleClas 的PP-ShiTu Lite demo。
支持多种开发环境,不同开发环境的编译流程请参考对应文档,请确保安装完成Java jdk、Android NDK(R17以上)。
1. [Docker](https://paddle-lite.readthedocs.io/zh/latest/source_compile/compile_env.html#docker)
2. [Linux](https://paddle-lite.readthedocs.io/zh/latest/source_compile/compile_env.html#linux)
3. [MAC OS](https://paddle-lite.readthedocs.io/zh/latest/source_compile/compile_env.html#mac-os)
```shell
# 配置完成交叉编译环境后,更新环境变量
# for docker、Linux
source ~/.bashrc
# for Mac OS
source ~/.bash_profile
```
### 1.2 准备预测库
预测库有两种获取方式:
1. [**建议**]直接下载,预测库下载链接如下:
|平台| 架构 | 预测库下载链接|
|-|-|-|
|Android| arm7 | [inference_lite_lib](https://github.com/PaddlePaddle/Paddle-Lite/releases/download/v2.10-rc/inference_lite_lib.android.armv7.clang.c++_static.with_extra.with_cv.tar.gz) |
| Android | arm8 | [inference_lite_lib](https://github.com/PaddlePaddle/Paddle-Lite/releases/download/v2.10-rc/inference_lite_lib.android.armv8.clang.c++_static.with_extra.with_cv.tar.gz) |
| Android | arm8(FP16) | [inference_lite_lib](https://github.com/PaddlePaddle/Paddle-Lite/releases/download/v2.10-rc/inference_lite_lib.android.armv8_clang_c++_static_with_extra_with_cv_with_fp16.tiny_publish_427e46.zip) |
**注意**:1. 如果是从 Paddle-Lite [官方文档](https://paddle-lite.readthedocs.io/zh/latest/quick_start/release_lib.html#android-toolchain-gcc)下载的预测库,注意选择`with_extra=ON,with_cv=ON`的下载链接。2. 目前只提供Android端demo,IOS端demo可以参考[Paddle-Lite IOS demo](https://github.com/PaddlePaddle/Paddle-Lite-Demo/tree/master/PaddleLite-ios-demo)
2. 编译Paddle-Lite得到预测库,Paddle-Lite的编译方式如下:
```shell
git clone https://github.com/PaddlePaddle/Paddle-Lite.git
cd Paddle-Lite
# 如果使用编译方式,建议使用develop分支编译预测库
git checkout develop
# FP32
./lite/tools/build_android.sh --arch=armv8 --toolchain=clang --with_cv=ON --with_extra=ON
# FP16
./lite/tools/build_android.sh --arch=armv8 --toolchain=clang --with_cv=ON --with_extra=ON --with_arm82_fp16=ON
```
**注意**:编译Paddle-Lite获得预测库时,需要打开`--with_cv=ON --with_extra=ON`两个选项,`--arch`表示`arm`版本,这里指定为armv8,更多编译命令介绍请参考[链接](https://paddle-lite.readthedocs.io/zh/latest/source_compile/compile_andriod.html#id2)
直接下载预测库并解压后,可以得到`inference_lite_lib.android.armv8.clang.c++_static.with_extra.with_cv/`文件夹,通过编译Paddle-Lite得到的预测库位于`Paddle-Lite/build.lite.android.armv8.gcc/inference_lite_lib.android.armv8/`文件夹下。
预测库的文件目录如下:
```
inference_lite_lib.android.armv8/
|-- cxx C++ 预测库和头文件
| |-- include C++ 头文件
| | |-- paddle_api.h
| | |-- paddle_image_preprocess.h
| | |-- paddle_lite_factory_helper.h
| | |-- paddle_place.h
| | |-- paddle_use_kernels.h
| | |-- paddle_use_ops.h
| | `-- paddle_use_passes.h
| `-- lib C++预测库
| |-- libpaddle_api_light_bundled.a C++静态库
| `-- libpaddle_light_api_shared.so C++动态库
|-- java Java预测库
| |-- jar
| | `-- PaddlePredictor.jar
| |-- so
| | `-- libpaddle_lite_jni.so
| `-- src
|-- demo C++和Java示例代码
| |-- cxx C++ 预测库demo
| `-- java Java 预测库demo
```
## 2 模型准备
### 2.1 模型准备
PaddleClas 提供了转换并优化后的推理模型,可以直接参考下方 2.1.1 小节进行下载。如果需要使用其他模型,请参考后续 2.1.2 小节自行转换并优化模型。
#### 2.1.1 使用PaddleClas提供的推理模型
```shell
# 进入lite_ppshitu目录
cd $PaddleClas/deploy/lite_shitu
wget https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/lite/ppshitu_lite_models_v1.2.tar
tar -xf ppshitu_lite_models_v1.2.tar
rm -f ppshitu_lite_models_v1.2.tar
```
#### 2.1.2 使用其他模型
Paddle-Lite 提供了多种策略来自动优化原始的模型,其中包括量化、子图融合、混合调度、Kernel优选等方法,使用Paddle-Lite的`opt`工具可以自动对inference模型进行优化,目前支持两种优化方式,优化后的模型更轻量,模型运行速度更快。
**注意**:如果已经准备好了 `.nb` 结尾的模型文件,可以跳过此步骤。
##### 2.1.2.1 安装paddle_lite_opt工具
安装`paddle_lite_opt`工具有如下两种方法:
1. [**建议**]pip安装paddlelite并进行转换
```shell
pip install paddlelite==2.10rc
```
2. 源码编译Paddle-Lite生成`paddle_lite_opt`工具
模型优化需要Paddle-Lite的`opt`可执行文件,可以通过编译Paddle-Lite源码获得,编译步骤如下:
```shell
# 如果准备环境时已经clone了Paddle-Lite,则不用重新clone Paddle-Lite
git clone https://github.com/PaddlePaddle/Paddle-Lite.git
cd Paddle-Lite
git checkout develop
# 启动编译
./lite/tools/build.sh build_optimize_tool
```
编译完成后,`opt`文件位于`build.opt/lite/api/`下,可通过如下方式查看`opt`的运行选项和使用方式;
```shell
cd build.opt/lite/api/
./opt
```
`opt`的使用方式与参数与上面的`paddle_lite_opt`完全一致。
之后使用`paddle_lite_opt`工具可以进行inference模型的转换。`paddle_lite_opt`的部分参数如下:
|选项|说明|
|-|-|
|--model_file|待优化的PaddlePaddle模型(combined形式)的网络结构文件路径|
|--param_file|待优化的PaddlePaddle模型(combined形式)的权重文件路径|
|--optimize_out_type|输出模型类型,目前支持两种类型:protobuf和naive_buffer,其中naive_buffer是一种更轻量级的序列化/反序列化实现,默认为naive_buffer|
|--optimize_out|优化模型的输出路径|
|--valid_targets|指定模型可执行的backend,默认为arm。目前可支持x86、arm、opencl、npu、xpu,可以同时指定多个backend(以空格分隔),Model Optimize Tool将会自动选择最佳方式。如果需要支持华为NPU(Kirin 810/990 Soc搭载的达芬奇架构NPU),应当设置为npu, arm|
更详细的`paddle_lite_opt`工具使用说明请参考[使用opt转化模型文档](https://paddle-lite.readthedocs.io/zh/latest/user_guides/opt/opt_bin.html)
`--model_file`表示inference模型的model文件地址,`--param_file`表示inference模型的param文件地址;`optimize_out`用于指定输出文件的名称(不需要添加`.nb`的后缀)。直接在命令行中运行`paddle_lite_opt`,也可以查看所有参数及其说明。
##### 2.1.2.2 转换示例
下面介绍使用`paddle_lite_opt`完成主体检测模型和识别模型的预训练模型,转成inference模型,最终转换成Paddle-Lite的优化模型的过程。
1. 转换主体检测模型
```shell
# 当前目录为 $PaddleClas/deploy/lite_shitu
# $code_path需替换成相应的运行目录,可以根据需要,将$code_path设置成需要的目录
export code_path=~
cd $code_path
git clone https://github.com/PaddlePaddle/PaddleDetection.git
# 进入PaddleDetection根目录
cd PaddleDetection
# 将预训练模型导出为inference模型
python tools/export_model.py -c configs/picodet/legacy_model/application/mainbody_detection/picodet_lcnet_x2_5_640_mainbody.yml -o weights=https://paddledet.bj.bcebos.com/models/picodet_lcnet_x2_5_640_mainbody.pdparams export_post_process=False --output_dir=inference
# 将inference模型转化为Paddle-Lite优化模型
paddle_lite_opt --model_file=inference/picodet_lcnet_x2_5_640_mainbody/model.pdmodel --param_file=inference/picodet_lcnet_x2_5_640_mainbody/model.pdiparams --optimize_out=inference/picodet_lcnet_x2_5_640_mainbody/mainbody_det
# 将转好的模型复制到lite_shitu目录下
cd $PaddleClas/deploy/lite_shitu
mkdir models
cp $code_path/PaddleDetection/inference/picodet_lcnet_x2_5_640_mainbody/mainbody_det.nb $PaddleClas/deploy/lite_shitu/models
```
2. 转换识别模型
```shell
# 识别模型下载
wget https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/rec/models/inference/general_PPLCNet_x2_5_lite_v1.0_infer.tar
# 解压模型
tar -xf general_PPLCNet_x2_5_lite_v1.0_infer.tar
# 转换为Paddle-Lite模型
paddle_lite_opt --model_file=general_PPLCNet_x2_5_lite_v1.0_infer/inference.pdmodel --param_file=general_PPLCNet_x2_5_lite_v1.0_infer/inference.pdiparams --optimize_out=general_PPLCNet_x2_5_lite_v1.0_infer/rec
# 将模型文件拷贝到lite_shitu下
cp general_PPLCNet_x2_5_lite_v1.0_infer/rec.nb deploy/lite_shitu/models/
```
**注意**`--optimize_out` 参数为优化后模型的保存路径,无需加后缀`.nb``--model_file` 参数为模型结构信息文件的路径,`--param_file` 参数为模型权重信息文件的路径,请注意文件名。
### 2.2 生成新的检索库
由于lite 版本的检索库用的是`faiss1.5.3`版本,与新版本不兼容,因此需要重新生成index库
#### 2.2.1 数据及环境配置
```shell
# 进入PaddleClas根目录
cd $PaddleClas
# 安装PaddleClas
python setup.py install
cd deploy
# 下载瓶装饮料数据集
wget https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/rec/data/drink_dataset_v1.0.tar && tar -xf drink_dataset_v1.0.tar
rm -rf drink_dataset_v1.0.tar
rm -rf drink_dataset_v1.0/index
# 安装1.5.3版本的faiss
pip install faiss-cpu==1.5.3
# 下载通用识别模型,可替换成自己的inference model
wget https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/rec/models/inference/general_PPLCNet_x2_5_lite_v1.0_infer.tar
tar -xf general_PPLCNet_x2_5_lite_v1.0_infer.tar
rm -rf general_PPLCNet_x2_5_lite_v1.0_infer.tar
```
#### 2.2.2 生成新的index文件
```shell
# 生成新的index库,注意指定好识别模型的路径,同时将index_mothod修改成Flat,HNSW32和IVF在此版本中可能存在bug,请慎重使用。
# 如果使用自己的识别模型,对应的修改inference model的目录
python python/build_gallery.py -c configs/inference_drink.yaml -o Global.rec_inference_model_dir=general_PPLCNet_x2_5_lite_v1.0_infer -o IndexProcess.index_method=Flat
# 进入到lite_shitu目录
cd lite_shitu
mv ../drink_dataset_v1.0 .
```
### 2.3 将yaml文件转换成json文件
```shell
# 如果测试单张图像
python generate_json_config.py --det_model_path ppshitu_lite_models_v1.2/mainbody_PPLCNet_x2_5_640_v1.2_lite.nb --rec_model_path ppshitu_lite_models_v1.2/general_PPLCNet_x2_5_lite_v1.2_infer.nb --img_path images/demo.jpeg
# or
# 如果测试多张图像
python generate_json_config.py --det_model_path ppshitu_lite_models_v1.2/mainbody_PPLCNet_x2_5_640_v1.2_lite.nb --rec_model_path ppshitu_lite_models_v1.2/general_PPLCNet_x2_5_lite_v1.2_infer.nb --img_dir images
# 执行完成后,会在lit_shitu下生成shitu_config.json配置文件
```
### 2.4 index字典转换
由于python的检索库字典,使用`pickle`进行的序列化存储,导致C++不方便读取,因此需要进行转换
```shell
# 转化id_map.pkl为id_map.txt
python transform_id_map.py -c ../configs/inference_drink.yaml
```
转换成功后,会在`IndexProcess.index_dir`目录下生成`id_map.txt`
### 2.5 与手机联调
首先需要进行一些准备工作。
1. 准备一台arm8的安卓手机,如果编译的预测库是armv7,则需要arm7的手机,并修改Makefile中`ARM_ABI=arm7`
2. 电脑上安装ADB工具,用于调试。 ADB安装方式如下:
2.1. MAC电脑安装ADB:
```shell
brew cask install android-platform-tools
```
2.2. Linux安装ADB
```shell
sudo apt update
sudo apt install -y wget adb
```
2.3. Window安装ADB
win上安装需要去谷歌的安卓平台下载ADB软件包进行安装:[链接](https://developer.android.com/studio)
3. 手机连接电脑后,开启手机`USB调试`选项,选择`文件传输`模式,在电脑终端中输入:
```shell
adb devices
```
如果有device输出,则表示安装成功,如下所示:
```
List of devices attached
744be294 device
```
4. 编译lite部署代码生成移动端可执行文件
```shell
cd $PaddleClas/deploy/lite_shitu
# ${lite prediction library path}下载的Paddle-Lite库路径
inference_lite_path=${lite prediction library path}/inference_lite_lib.android.armv8.gcc.c++_static.with_extra.with_cv/
mkdir $inference_lite_path/demo/cxx/ppshitu_lite
cp -r * $inference_lite_path/demo/cxx/ppshitu_lite
cd $inference_lite_path/demo/cxx/ppshitu_lite
# 执行编译,等待完成后得到可执行文件main
make ARM_ABI=arm8
#如果是arm7,则执行 make ARM_ABI = arm7 (或者在Makefile中修改该项)
```
5. 准备优化后的模型、预测库文件、测试图像。
```shell
mkdir deploy
mv ppshitu_lite_models_v1.1 deploy/
mv drink_dataset_v1.0 deploy/
mv images deploy/
mv shitu_config.json deploy/
cp pp_shitu deploy/
# 将C++预测动态库so文件复制到deploy文件夹中
cp ../../../cxx/lib/libpaddle_light_api_shared.so deploy/
```
执行完成后,deploy文件夹下将有如下文件格式:
```shell
deploy/
|-- ppshitu_lite_models_v1.1/
| |--mainbody_PPLCNet_x2_5_640_quant_v1.1_lite.nb 优化后的主体检测模型文件
| |--general_PPLCNet_x2_5_lite_v1.1_infer.nb 优化后的识别模型文件
|-- images/
| |--demo.jpg 图片文件
|-- drink_dataset_v1.0/ 瓶装饮料demo数据
| |--index 检索index目录
|-- pp_shitu 生成的移动端执行文件
|-- shitu_config.json 执行时参数配置文件
|-- libpaddle_light_api_shared.so Paddle-Lite库文件
```
**注意:**
* `shitu_config.json` 包含了目标检测的超参数,请按需进行修改
6. 启动调试,上述步骤完成后就可以使用ADB将文件夹 `deploy/` push到手机上运行,步骤如下:
```shell
# 将上述deploy文件夹push到手机上
adb push deploy /data/local/tmp/
adb shell
cd /data/local/tmp/deploy
export LD_LIBRARY_PATH=/data/local/tmp/deploy:$LD_LIBRARY_PATH
# 修改权限为可执行
chmod 777 pp_shitu
# 执行程序
./pp_shitu shitu_config.json
```
如果对代码做了修改,则需要重新编译并push到手机上。
运行效果如下:
```
images/demo.jpeg:
result0: bbox[344, 98, 527, 593], score: 0.811656, label: 红牛-强化型
result1: bbox[0, 0, 600, 600], score: 0.729664, label: 红牛-强化型
```
## FAQ
Q1:如果想更换模型怎么办,需要重新按照流程走一遍吗?
A1:如果已经走通了上述步骤,更换模型只需要替换 `.nb` 模型文件即可,同时要注意修改下配置文件中的 `.nb` 文件路径以及类别映射文件(如有必要)。
Q2:换一个图测试怎么做?
A2:替换 deploy 下的测试图像为你想要测试的图像,并重新生成json配置文件(或者直接修改图像路径),使用 ADB 再次 push 到手机上即可。
../../docs/zh_CN/inference_deployment/lite_shitu.md
\ No newline at end of file
......@@ -9,15 +9,15 @@
# 默认编译时的${PWD}=PaddleClas/deploy/paddleserving/
python_name=${1:-'python'}
export python_name=${1:-'python'}
apt-get update
apt install -y libcurl4-openssl-dev libbz2-dev
wget -nc https://paddle-serving.bj.bcebos.com/others/centos_ssl.tar
tar xf centos_ssl.tar
rm -rf centos_ssl.tar
mv libcrypto.so.1.0.2k /usr/lib/libcrypto.so.1.0.2k
mv libssl.so.1.0.2k /usr/lib/libssl.so.1.0.2k
\mv libcrypto.so.1.0.2k /usr/lib/libcrypto.so.1.0.2k
\mv libssl.so.1.0.2k /usr/lib/libssl.so.1.0.2k
ln -sf /usr/lib/libcrypto.so.1.0.2k /usr/lib/libcrypto.so.10
ln -sf /usr/lib/libssl.so.1.0.2k /usr/lib/libssl.so.10
ln -sf /usr/lib/libcrypto.so.10 /usr/lib/libcrypto.so
......
......@@ -16,9 +16,8 @@ op:
#当op配置没有server_endpoints时,从local_service_conf读取本地服务配置
local_service_conf:
#uci模型路径
model_config: ../../models/general_PPLCNet_x2_5_lite_v1.0_serving
model_config: ../../models/general_PPLCNetV2_base_pretrained_v1.0_serving
#计算硬件类型: 空缺时由devices决定(CPU/GPU),0=cpu, 1=gpu, 2=tensorRT, 3=arm cpu, 4=kunlun xpu
device_type: 1
......@@ -37,7 +36,7 @@ op:
local_service_conf:
client_type: local_predictor
device_type: 1
devices: '0'
devices: "0"
fetch_list:
- save_infer_model/scale_0.tmp_1
- save_infer_model/scale_0.tmp_1
model_config: ../../models/picodet_PPLCNet_x2_5_mainbody_lite_v1.0_serving/
import requests
import json
# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import base64
import json
import os
imgpath = "../../drink_dataset_v1.0/test_images/001.jpeg"
import requests
image_path = "../../drink_dataset_v2.0/test_images/100.jpeg"
def bytes_to_base64(image_bytes: bytes) -> bytes:
"""encode bytes using base64 algorithm
Args:
image_bytes (bytes): bytes object to be encoded
Returns:
bytes: base64 bytes
"""
return base64.b64encode(image_bytes).decode('utf8')
def cv2_to_base64(image):
return base64.b64encode(image).decode('utf8')
if __name__ == "__main__":
url = "http://127.0.0.1:18081/recognition/prediction"
with open(os.path.join(".", imgpath), 'rb') as file:
image_data1 = file.read()
image = cv2_to_base64(image_data1)
data = {"key": ["image"], "value": [image]}
with open(os.path.join(".", image_path), 'rb') as file:
image_bytes = file.read()
image_base64 = bytes_to_base64(image_bytes)
data = {"key": ["image"], "value": [image_base64]}
for i in range(1):
r = requests.post(url=url, data=json.dumps(data))
......
......@@ -15,20 +15,33 @@ try:
from paddle_serving_server_gpu.pipeline import PipelineClient
except ImportError:
from paddle_serving_server.pipeline import PipelineClient
import base64
import os
client = PipelineClient()
client.connect(['127.0.0.1:9994'])
imgpath = "../../drink_dataset_v1.0/test_images/001.jpeg"
image_path = "../../drink_dataset_v2.0/test_images/100.jpeg"
def bytes_to_base64(image_bytes: bytes) -> bytes:
"""encode bytes using base64 algorithm
Args:
image_bytes (bytes): bytes to be encoded
Returns:
bytes: base64 bytes
"""
return base64.b64encode(image_bytes).decode('utf8')
def cv2_to_base64(image):
return base64.b64encode(image).decode('utf8')
if __name__ == "__main__":
with open(imgpath, 'rb') as file:
image_data = file.read()
image = cv2_to_base64(image_data)
with open(os.path.join(".", image_path), 'rb') as file:
image_bytes = file.read()
image_base64 = bytes_to_base64(image_bytes)
for i in range(1):
ret = client.predict(feed_dict={"image": image}, fetch=["result"])
ret = client.predict(
feed_dict={"image": image_base64}, fetch=["result"])
print(ret)
......@@ -15,7 +15,7 @@ feed_var {
shape: 6
}
fetch_var {
name: "save_infer_model/scale_0.tmp_1"
name: "batch_norm_25.tmp_2"
alias_name: "features"
is_lod_tensor: false
fetch_type: 1
......
......@@ -15,7 +15,7 @@ feed_var {
shape: 6
}
fetch_var {
name: "save_infer_model/scale_0.tmp_1"
name: "batch_norm_25.tmp_2"
alias_name: "features"
is_lod_tensor: false
fetch_type: 1
......
......@@ -11,17 +11,24 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from paddle_serving_server.web_service import WebService, Op
import base64
import json
import logging
import numpy as np
import os
import pickle
import sys
import cv2
from paddle_serving_app.reader import *
import base64
import os
import faiss
import pickle
import json
import numpy as np
from paddle_serving_app.reader import BGR2RGB
from paddle_serving_app.reader import Div
from paddle_serving_app.reader import Normalize
from paddle_serving_app.reader import RCNNPostprocess
from paddle_serving_app.reader import Resize
from paddle_serving_app.reader import Sequential
from paddle_serving_app.reader import Transpose
from paddle_serving_server.web_service import Op, WebService
class DetOp(Op):
......@@ -101,11 +108,11 @@ class RecOp(Op):
def init_op(self):
self.seq = Sequential([
BGR2RGB(), Resize((224, 224)), Div(255),
Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225],
False), Transpose((2, 0, 1))
Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225], False),
Transpose((2, 0, 1))
])
index_dir = "../../drink_dataset_v1.0/index"
index_dir = "../../drink_dataset_v2.0/index"
assert os.path.exists(os.path.join(
index_dir, "vector.index")), "vector.index not found ..."
assert os.path.exists(os.path.join(
......@@ -136,7 +143,7 @@ class RecOp(Op):
})
self.det_boxes = boxes
#construct batch images for rec
# construct batch images for rec
imgs = []
for box in boxes:
box = [int(x) for x in box["bbox"]]
......@@ -192,7 +199,7 @@ class RecOp(Op):
pred["rec_scores"] = scores[i][0]
results.append(pred)
#do nms
# do NMS
results = self.nms_to_rec_results(results, self.rec_nms_thresold)
return {"result": str(results)}, None, ""
......
......@@ -3,12 +3,12 @@ gpu_id=$1
# PP-ShiTu CPP serving script
if [[ -n "${gpu_id}" ]]; then
nohup python3.7 -m paddle_serving_server.serve \
--model ../../models/picodet_PPLCNet_x2_5_mainbody_lite_v1.0_serving ../../models/general_PPLCNet_x2_5_lite_v1.0_serving \
--model ../../models/picodet_PPLCNet_x2_5_mainbody_lite_v1.0_serving ../../models/general_PPLCNetV2_base_pretrained_v1.0_serving \
--op GeneralPicodetOp GeneralFeatureExtractOp \
--port 9400 --gpu_id="${gpu_id}" > log_PPShiTu.txt 2>&1 &
else
nohup python3.7 -m paddle_serving_server.serve \
--model ../../models/picodet_PPLCNet_x2_5_mainbody_lite_v1.0_serving ../../models/general_PPLCNet_x2_5_lite_v1.0_serving \
--model ../../models/picodet_PPLCNet_x2_5_mainbody_lite_v1.0_serving ../../models/general_PPLCNetV2_base_pretrained_v1.0_serving \
--op GeneralPicodetOp GeneralFeatureExtractOp \
--port 9400 > log_PPShiTu.txt 2>&1 &
fi
......@@ -12,20 +12,19 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import numpy as np
import os
import pickle
from paddle_serving_client import Client
from paddle_serving_app.reader import *
import cv2
import faiss
import os
import pickle
import numpy as np
from paddle_serving_client import Client
rec_nms_thresold = 0.05
rec_score_thres = 0.5
feature_normalize = True
return_k = 1
index_dir = "../../drink_dataset_v1.0/index"
index_dir = "../../drink_dataset_v2.0/index"
def init_index(index_dir):
......@@ -41,7 +40,7 @@ def init_index(index_dir):
return searcher, id_map
#get box
# get box
def nms_to_rec_results(results, thresh=0.1):
filtered_results = []
......@@ -91,21 +90,21 @@ def postprocess(fetch_dict, feature_normalize, det_boxes, searcher, id_map,
pred["rec_scores"] = scores[i][0]
results.append(pred)
#do nms
# do NMS
results = nms_to_rec_results(results, rec_nms_thresold)
return results
#do client
# do client
if __name__ == "__main__":
client = Client()
client.load_client_config([
"../../models/picodet_PPLCNet_x2_5_mainbody_lite_v1.0_client",
"../../models/general_PPLCNet_x2_5_lite_v1.0_client"
"../../models/general_PPLCNetV2_base_pretrained_v1.0_client"
])
client.connect(['127.0.0.1:9400'])
im = cv2.imread("../../drink_dataset_v1.0/test_images/001.jpeg")
im = cv2.imread("../../drink_dataset_v2.0/test_images/100.jpeg")
im_shape = np.array(im.shape[:2]).reshape(-1)
fetch_map = client.predict(
feed={"image": im,
......@@ -113,7 +112,8 @@ if __name__ == "__main__":
fetch=["features", "boxes"],
batch=False)
#add retrieval procedure
# add retrieval procedure
print(fetch_map.keys())
det_boxes = fetch_map["boxes"]
searcher, id_map = init_index(index_dir)
results = postprocess(fetch_map, feature_normalize, det_boxes, searcher,
......
../../docs/zh_CN/inference_deployment/shitu_gallery_manager.md
\ No newline at end of file
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
import mod.mainwindow
from paddleclas.deploy.utils import config, logger
from paddleclas.deploy.python.predict_rec import RecPredictor
from fastapi import FastAPI
import uvicorn
import numpy as np
import faiss
from typing import List
import pickle
import cv2
import socket
import json
import operator
from multiprocessing import Process
"""
完整的index库如下:
root_path/ # 库存储目录
|-- image_list.txt # 图像列表,每行:image_path label。由前端生成及修改。后端只读
|-- features.pkl # 建库之后,保存的embedding向量,后端生成,前端无需操作
|-- images # 图像存储目录,由前端生成及增删查等操作。后端只读
| |-- md5.jpg
| |-- md5.jpg
| |-- ……
|-- index # 真正的生成的index库存储目录,后端生成及操作,前端无需操作。
| |-- vector.index # faiss生成的索引库
| |-- id_map.pkl # 索引文件
"""
class ShiTuIndexManager(object):
def __init__(self, config):
self.root_path = None
self.image_list_path = "image_list.txt"
self.image_dir = "images"
self.index_path = "index/vector.index"
self.id_map_path = "index/id_map.pkl"
self.features_path = "features.pkl"
self.index = None
self.id_map = None
self.features = None
self.config = config
self.predictor = RecPredictor(config)
def _load_pickle(self, path):
if os.path.exists(path):
return pickle.load(open(path, 'rb'))
else:
return None
def _save_pickle(self, path, data):
if not os.path.exists(os.path.dirname(path)):
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'wb') as fd:
pickle.dump(data, fd)
def _load_index(self):
self.index = faiss.read_index(
os.path.join(self.root_path, self.index_path))
self.id_map = self._load_pickle(
os.path.join(self.root_path, self.id_map_path))
self.features = self._load_pickle(
os.path.join(self.root_path, self.features_path))
def _save_index(self, index, id_map, features):
faiss.write_index(index, os.path.join(self.root_path, self.index_path))
self._save_pickle(os.path.join(self.root_path, self.id_map_path),
id_map)
self._save_pickle(os.path.join(self.root_path, self.features_path),
features)
def _update_path(self, root_path, image_list_path=None):
if root_path == self.root_path:
pass
else:
self.root_path = root_path
if not os.path.exists(os.path.join(root_path, "index")):
os.mkdir(os.path.join(root_path, "index"))
if image_list_path is not None:
self.image_list_path = image_list_path
def _cal_featrue(self, image_list):
batch_images = []
featrures = None
cnt = 0
for idx, image_path in enumerate(image_list):
image = cv2.imread(image_path)
if image is None:
return "{} is broken or not exist. Stop"
else:
image = image[:, :, ::-1]
batch_images.append(image)
cnt += 1
if cnt % self.config["Global"]["batch_size"] == 0 or (
idx + 1) == len(image_list):
if len(batch_images) == 0:
continue
batch_results = self.predictor.predict(batch_images)
featrures = batch_results if featrures is None else np.concatenate(
(featrures, batch_results), axis=0)
batch_images = []
return featrures
def _split_datafile(self, data_file, image_root):
'''
data_file: image path and info, which can be splitted by spacer
image_root: image path root
delimiter: delimiter
'''
gallery_images = []
gallery_docs = []
gallery_ids = []
with open(data_file, 'r', encoding='utf-8') as f:
lines = f.readlines()
for _, ori_line in enumerate(lines):
line = ori_line.strip().split()
text_num = len(line)
assert text_num >= 2, f"line({ori_line}) must be splitted into at least 2 parts, but got {text_num}"
image_file = os.path.join(image_root, line[0])
gallery_images.append(image_file)
gallery_docs.append(ori_line.strip())
gallery_ids.append(os.path.basename(line[0]).split(".")[0])
return gallery_images, gallery_docs, gallery_ids
def create_index(self,
image_list: str,
index_method: str = "HNSW32",
image_root: str = None):
if not os.path.exists(image_list):
return "{} is not exist".format(image_list)
if index_method.lower() not in ['hnsw32', 'ivf', 'flat']:
return "The index method Only support: HNSW32, IVF, Flat"
self._update_path(os.path.dirname(image_list), image_list)
# get image_paths
image_root = image_root if image_root is not None else self.root_path
gallery_images, gallery_docs, image_ids = self._split_datafile(
image_list, image_root)
# gernerate index
if index_method == "IVF":
index_method = index_method + str(
min(max(int(len(gallery_images) // 32), 2), 65536)) + ",Flat"
index = faiss.index_factory(
self.config["IndexProcess"]["embedding_size"], index_method,
faiss.METRIC_INNER_PRODUCT)
self.index = faiss.IndexIDMap2(index)
features = self._cal_featrue(gallery_images)
self.index.train(features)
index_ids = np.arange(0, len(gallery_images)).astype(np.int64)
self.index.add_with_ids(features, index_ids)
self.id_map = dict()
for i, d in zip(list(index_ids), gallery_docs):
self.id_map[i] = d
self.features = {
"features": features,
"index_method": index_method,
"image_ids": image_ids,
"index_ids": index_ids.tolist()
}
self._save_index(self.index, self.id_map, self.features)
def open_index(self, root_path: str, image_list_path: str) -> str:
self._update_path(root_path)
_, _, image_ids = self._split_datafile(image_list_path, root_path)
if os.path.exists(os.path.join(self.root_path, self.index_path)) and \
os.path.exists(os.path.join(self.root_path, self.id_map_path)) and \
os.path.exists(os.path.join(self.root_path, self.features_path)):
self._update_path(root_path)
self._load_index()
if operator.eq(set(image_ids), set(self.features['image_ids'])):
return ""
else:
return "The image list is different from index, Please update index"
else:
return "File not exist: features.pkl, vector.index, id_map.pkl"
def update_index(self, image_list: str, image_root: str = None) -> str:
if self.index and self.id_map and self.features:
image_paths, image_docs, image_ids = self._split_datafile(
image_list,
image_root if image_root is not None else self.root_path)
# for add image
add_ids = list(
set(image_ids).difference(set(self.features["image_ids"])))
add_indexes = [i for i, x in enumerate(image_ids) if x in add_ids]
add_image_paths = [image_paths[i] for i in add_indexes]
add_image_docs = [image_docs[i] for i in add_indexes]
add_image_ids = [image_ids[i] for i in add_indexes]
self._add_index(add_image_paths, add_image_docs, add_image_ids)
# delete images
delete_ids = list(
set(self.features["image_ids"]).difference(set(image_ids)))
self._delete_index(delete_ids)
self._save_index(self.index, self.id_map, self.features)
return ""
else:
return "Failed. Please create or open index first"
def _add_index(self, image_list: List, image_docs: List, image_ids: List):
if len(image_ids) == 0:
return
featrures = self._cal_featrue(image_list)
index_ids = (np.arange(0, len(image_list)) + max(self.id_map.keys()) +
1).astype(np.int64)
self.index.add_with_ids(featrures, index_ids)
for i, d in zip(index_ids, image_docs):
self.id_map[i] = d
self.features['features'] = np.concatenate(
[self.features['features'], featrures], axis=0)
self.features['image_ids'].extend(image_ids)
self.features['index_ids'].extend(index_ids.tolist())
def _delete_index(self, image_ids: List):
if len(image_ids) == 0:
return
indexes = [
i for i, x in enumerate(self.features['image_ids'])
if x in image_ids
]
self.features["features"] = np.delete(self.features["features"],
indexes,
axis=0)
self.features["image_ids"] = np.delete(np.asarray(
self.features["image_ids"]),
indexes,
axis=0).tolist()
index_ids = np.delete(np.asarray(self.features["index_ids"]),
indexes,
axis=0).tolist()
id_map_values = [self.id_map[i] for i in index_ids]
self.index.reset()
ids = np.arange(0, len(id_map_values)).astype(np.int64)
self.index.add_with_ids(self.features['features'], ids)
self.id_map.clear()
for i, d in zip(ids, id_map_values):
self.id_map[i] = d
self.features["index_ids"] = ids
app = FastAPI()
@app.get("/new_index")
def new_index(image_list_path: str,
index_method: str = "HNSW32",
index_root_path: str = None,
force: bool = False):
result = ""
try:
if index_root_path is not None:
image_list_path = os.path.join(index_root_path, image_list_path)
index_path = os.path.join(index_root_path, "index", "vector.index")
id_map_path = os.path.join(index_root_path, "index", "id_map.pkl")
if not (os.path.exists(index_path)
and os.path.exists(id_map_path)) or force:
manager.create_index(image_list_path, index_method, index_root_path)
else:
result = "There alrealy has index in {}".format(index_root_path)
except Exception as e:
result = e.__str__()
data = {"error_message": result}
return json.dumps(data).encode()
@app.get("/open_index")
def open_index(index_root_path: str, image_list_path: str):
result = ""
try:
image_list_path = os.path.join(index_root_path, image_list_path)
result = manager.open_index(index_root_path, image_list_path)
except Exception as e:
result = e.__str__()
data = {"error_message": result}
return json.dumps(data).encode()
@app.get("/update_index")
def update_index(image_list_path: str, index_root_path: str = None):
result = ""
try:
if index_root_path is not None:
image_list_path = os.path.join(index_root_path, image_list_path)
result = manager.update_index(image_list=image_list_path,
image_root=index_root_path)
except Exception as e:
result = e.__str__()
data = {"error_message": result}
return json.dumps(data).encode()
def FrontInterface(server_process=None):
front = QtWidgets.QApplication([])
main_window = mod.mainwindow.MainWindow(process=server_process)
main_window.showMaximized()
sys.exit(front.exec_())
def Server(args):
[app, host, port] = args
uvicorn.run(app, host=host, port=port)
if __name__ == '__main__':
args = config.parse_args()
model_config = config.get_config(args.config,
overrides=args.override,
show=True)
manager = ShiTuIndexManager(model_config)
try:
ip = socket.gethostbyname(socket.gethostname())
except:
ip = '127.0.0.1'
port = 8000
p_server = Process(target=Server, args=([app, ip, port],))
p_server.start()
# p_client = Process(target=FrontInterface, args=())
# p_client.start()
# p_client.join()
FrontInterface(p_server)
p_server.terminate()
sys.exit(0)
import os
from PyQt5 import QtCore, QtWidgets
from mod import image_list_manager as imglistmgr
from mod import utils
from mod import ui_addclassifydialog
from mod import ui_renameclassifydialog
class ClassifyUiContext(QtCore.QObject):
# 分类界面相关业务
selected = QtCore.pyqtSignal(str) # 选择分类信号
def __init__(self, ui: QtWidgets.QListView, parent: QtWidgets.QMainWindow,
image_list_mgr: imglistmgr.ImageListManager):
super(ClassifyUiContext, self).__init__()
self.__ui = ui
self.__parent = parent
self.__imageListMgr = image_list_mgr
self.__menu = QtWidgets.QMenu()
self.__initMenu()
self.__initUi()
self.__connectSignal()
@property
def ui(self):
return self.__ui
@property
def parent(self):
return self.__parent
@property
def imageListManager(self):
return self.__imageListMgr
@property
def menu(self):
return self.__menu
def __initUi(self):
"""初始化分类界面"""
self.__ui.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
def __connectSignal(self):
"""连接信号"""
self.__ui.clicked.connect(self.uiClicked)
self.__ui.doubleClicked.connect(self.uiDoubleClicked)
def __initMenu(self):
"""初始化分类界面菜单"""
utils.setMenu(self.__menu, "添加分类", self.addClassify)
utils.setMenu(self.__menu, "移除分类", self.removeClassify)
utils.setMenu(self.__menu, "重命名分类", self.renemeClassify)
self.__ui.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.__ui.customContextMenuRequested.connect(self.__showMenu)
def __showMenu(self, pos):
"""显示分类界面菜单"""
if len(self.__imageListMgr.filePath) > 0:
self.__menu.exec_(self.__ui.mapToGlobal(pos))
def setClassifyList(self, classify_list):
"""设置分类列表"""
list_model = QtCore.QStringListModel(classify_list)
self.__ui.setModel(list_model)
def uiClicked(self, index):
"""分类列表点击"""
if not self.__ui.currentIndex().isValid():
return
txt = index.data()
self.selected.emit(txt)
def uiDoubleClicked(self, index):
"""分类列表双击"""
if not self.__ui.currentIndex().isValid():
return
ole_name = index.data()
dlg = QtWidgets.QDialog(parent=self.parent)
ui = ui_renameclassifydialog.Ui_RenameClassifyDialog()
ui.setupUi(dlg)
ui.oldNameLineEdit.setText(ole_name)
result = dlg.exec_()
new_name = ui.newNameLineEdit.text()
if result == QtWidgets.QDialog.Accepted:
mgr_result = self.__imageListMgr.renameClassify(ole_name, new_name)
if not mgr_result:
QtWidgets.QMessageBox.warning(self.parent, "重命名分类", "重命名分类错误")
else:
self.setClassifyList(self.__imageListMgr.classifyList)
self.__imageListMgr.writeFile()
def addClassify(self):
"""添加分类"""
if not os.path.exists(self.__imageListMgr.filePath):
QtWidgets.QMessageBox.information(self.__parent, "提示",
"请先打开正确的图像库")
return
dlg = QtWidgets.QDialog(parent=self.parent)
ui = ui_addclassifydialog.Ui_AddClassifyDialog()
ui.setupUi(dlg)
result = dlg.exec_()
txt = ui.lineEdit.text()
if result == QtWidgets.QDialog.Accepted:
mgr_result = self.__imageListMgr.addClassify(txt)
if not mgr_result:
QtWidgets.QMessageBox.warning(self.parent, "添加分类", "添加分类错误")
else:
self.setClassifyList(self.__imageListMgr.classifyList)
def removeClassify(self):
"""移除分类"""
if not os.path.exists(self.__imageListMgr.filePath):
QtWidgets.QMessageBox.information(self.__parent, "提示",
"请先打开正确的图像库")
return
if not self.__ui.currentIndex().isValid():
return
classify = self.__ui.currentIndex().data()
result = QtWidgets.QMessageBox.information(
self.parent,
"移除分类",
"确定移除分类: {}".format(classify),
buttons=QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel,
defaultButton=QtWidgets.QMessageBox.Cancel)
if result == QtWidgets.QMessageBox.Ok:
if len(self.__imageListMgr.imageList(classify)) > 0:
QtWidgets.QMessageBox.warning(self.parent, "移除分类",
"分类下存在图片,请先移除图片")
else:
self.__imageListMgr.removeClassify(classify)
self.setClassifyList(self.__imageListMgr.classifyList())
def renemeClassify(self):
"""重命名分类"""
idx = self.__ui.currentIndex()
if idx.isValid():
self.uiDoubleClicked(idx)
def searchClassify(self, classify):
"""查找分类"""
self.setClassifyList(self.__imageListMgr.findLikeClassify(classify))
import os
class ImageListManager:
"""
图像列表文件管理器
"""
def __init__(self, file_path="", encoding="utf-8"):
self.__filePath = ""
self.__dirName = ""
self.__dataList = {}
self.__findLikeClassifyResult = []
if file_path != "":
self.readFile(file_path, encoding)
@property
def filePath(self):
return self.__filePath
@property
def dirName(self):
return self.__dirName
@dirName.setter
def dirName(self, value):
self.__dirName = value
@property
def dataList(self):
return self.__dataList
@property
def classifyList(self):
return self.__dataList.keys()
@property
def findLikeClassifyResult(self):
return self.__findLikeClassifyResult
def imageList(self, classify: str):
"""
获取分类下的图片列表
Args:
classify (str): 分类名称
Returns:
list: 图片列表
"""
return self.__dataList[classify]
def readFile(self, file_path: str, encoding="utf-8"):
"""
读取文件内容
Args:
file_path (str): 文件路径
encoding (str, optional): 文件编码. 默认 "utf-8".
Raises:
Exception: 文件不存在
"""
if not os.path.exists(file_path):
raise Exception("文件不存在:{}".format(file_path))
self.__filePath = file_path
self.__dirName = os.path.dirname(self.__filePath)
self.__readData(file_path, encoding)
def __readData(self, file_path: str, encoding="utf-8"):
"""
读取文件内容
Args:
file_path (str): 文件路径
encoding (str, optional): 文件编码. 默认 "utf-8".
"""
with open(file_path, "r", encoding=encoding) as f:
self.__dataList.clear()
for line in f:
line = line.rstrip("\n")
data = line.split("\t")
self.__appendData(data)
def __appendData(self, data: list):
"""
添加数据
Args:
data (list): 数据
"""
if data[1] not in self.__dataList:
self.__dataList[data[1]] = []
self.__dataList[data[1]].append(data[0])
def writeFile(self, file_path="", encoding="utf-8"):
"""
写入文件
Args:
file_path (str, optional): 文件路径. 默认 "".
encoding (str, optional): 文件编码. 默认 "utf-8".
"""
if file_path == "":
file_path = self.__filePath
if not os.path.exists(file_path):
return False
self.__dirName = os.path.dirname(self.__filePath)
lines = []
for classify in self.__dataList.keys():
for path in self.__dataList[classify]:
lines.append("{}\t{}\n".format(path, classify))
with open(file_path, "w", encoding=encoding) as f:
f.writelines(lines)
return True
def realPath(self, image_path: str):
"""
获取真实路径
Args:
image_path (str): 图片路径
"""
return os.path.join(self.__dirName, image_path)
def realPathList(self, classify: str):
"""
获取分类下的真实路径列表
Args:
classify (str): 分类名称
Returns:
list: 真实路径列表
"""
if classify not in self.classifyList:
return []
paths = self.__dataList[classify]
if len(paths) == 0:
return []
for i in range(len(paths)):
paths[i] = os.path.join(self.__dirName, paths[i])
return paths
def findLikeClassify(self, name: str):
"""
查找类似的分类名称
Args:
name (str): 分类名称
Returns:
list: 类似的分类名称列表
"""
self.__findLikeClassifyResult.clear()
for classify in self.__dataList.keys():
word = str(name)
if (word in classify):
self.__findLikeClassifyResult.append(classify)
return self.__findLikeClassifyResult
def addClassify(self, classify: str):
"""
添加分类
Args:
classify (str): 分类名称
Returns:
bool: 如果分类名称已经存在,返回False,否则添加分类并返回True
"""
if classify in self.__dataList:
return False
self.__dataList[classify] = []
return True
def removeClassify(self, classify: str):
"""
移除分类
Args:
classify (str): 分类名称
Returns:
bool: 如果分类名称不存在,返回False,否则移除分类并返回True
"""
if classify not in self.__dataList:
return False
self.__dataList.pop(classify)
return True
def renameClassify(self, old_classify: str, new_classify: str):
"""
重命名分类名称
Args:
old_classify (str): 原分类名称
new_classify (str): 新分类名称
Returns:
bool: 如果原分类名称不存在,或者新分类名称已经存在,返回False,否则重命名分类名称并返回True
"""
if old_classify not in self.__dataList:
return False
if new_classify in self.__dataList:
return False
self.__dataList[new_classify] = self.__dataList[old_classify]
self.__dataList.pop(old_classify)
return True
def allClassfiyNotEmpty(self):
"""
检查所有分类是否都有图片
Returns:
bool: 如果有一个分类没有图片,返回False,否则返回True
"""
for classify in self.__dataList.keys():
if len(self.__dataList[classify]) == 0:
return False
return True
def resetImageList(self, classify: str, image_list: list):
"""
重置图片列表
Args:
classify (str): 分类名称
image_list (list): 图片相对路径列表
Returns:
bool: 如果分类名称不存在,返回False,否则重置图片列表并返回True
"""
if classify not in self.__dataList:
return False
self.__dataList[classify] = image_list
return True
import os
from stat import filemode
from PyQt5 import QtCore, QtGui, QtWidgets
from mod import image_list_manager as imglistmgr
from mod import utils
from mod import ui_renameclassifydialog
from mod import imageeditclassifydialog
# 图像缩放基数
BASE_IMAGE_SIZE = 64
class ImageListUiContext(QtCore.QObject):
# 图片列表界面相关业务,style sheet 在 MainWindow.ui 相应的 ImageListWidget 中设置
listCount = QtCore.pyqtSignal(int) # 图像列表图像的数量
selectedCount = QtCore.pyqtSignal(int) # 图像列表选择图像的数量
def __init__(self, ui: QtWidgets.QListWidget,
parent: QtWidgets.QMainWindow,
image_list_mgr: imglistmgr.ImageListManager):
super(ImageListUiContext, self).__init__()
self.__ui = ui
self.__parent = parent
self.__imageListMgr = image_list_mgr
self.__initUi()
self.__menu = QtWidgets.QMenu()
self.__initMenu()
self.__connectSignal()
self.__selectedClassify = ""
self.__imageScale = 1
@property
def ui(self):
return self.__ui
@property
def parent(self):
return self.__parent
@property
def imageListManager(self):
return self.__imageListMgr
@property
def menu(self):
return self.__menu
def __initUi(self):
"""初始化图片列表样式"""
self.__ui.setViewMode(QtWidgets.QListView.IconMode)
self.__ui.setSpacing(15)
self.__ui.setMovement(QtWidgets.QListView.Static)
self.__ui.setSelectionMode(
QtWidgets.QAbstractItemView.ExtendedSelection)
def __initMenu(self):
"""初始化图片列表界面菜单"""
utils.setMenu(self.__menu, "添加图片", self.addImage)
utils.setMenu(self.__menu, "移除图片", self.removeImage)
utils.setMenu(self.__menu, "编辑图片分类", self.editImageClassify)
self.__menu.addSeparator()
utils.setMenu(self.__menu, "选择全部图片", self.selectAllImage)
utils.setMenu(self.__menu, "反向选择图片", self.reverseSelectImage)
utils.setMenu(self.__menu, "取消选择图片", self.cancelSelectImage)
self.__ui.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.__ui.customContextMenuRequested.connect(self.__showMenu)
def __showMenu(self, pos):
"""显示图片列表界面菜单"""
if len(self.__imageListMgr.filePath) > 0:
self.__menu.exec_(self.__ui.mapToGlobal(pos))
def __connectSignal(self):
"""连接信号与槽"""
self.__ui.itemSelectionChanged.connect(self.onSelectionChanged)
def setImageScale(self, scale: int):
"""设置图片大小"""
self.__imageScale = scale
size = QtCore.QSize(scale * BASE_IMAGE_SIZE, scale * BASE_IMAGE_SIZE)
self.__ui.setIconSize(size)
for i in range(self.__ui.count()):
item = self.__ui.item(i)
item.setSizeHint(size)
def setImageList(self, classify: str):
"""设置图片列表"""
size = QtCore.QSize(self.__imageScale * BASE_IMAGE_SIZE,
self.__imageScale * BASE_IMAGE_SIZE)
self.__selectedClassify = classify
image_list = self.__imageListMgr.imageList(classify)
self.__ui.clear()
count = 0
for i in image_list:
item = QtWidgets.QListWidgetItem(self.__ui)
item.setIcon(QtGui.QIcon(self.__imageListMgr.realPath(i)))
item.setData(QtCore.Qt.UserRole, i)
item.setSizeHint(size)
self.__ui.addItem(item)
count += 1
self.listCount.emit(count)
def clear(self):
"""清除图片列表"""
self.__ui.clear()
def addImage(self):
"""添加图片"""
if not os.path.exists(self.__imageListMgr.filePath):
QtWidgets.QMessageBox.information(self.__parent, "提示",
"请先打开正确的图像库")
return
filter = "图片 (*.png *.jpg *.jpeg *.PNG *.JPG *.JPEG);;所有文件(*.*)"
dlg = QtWidgets.QFileDialog(self.__parent)
dlg.setFileMode(QtWidgets.QFileDialog.ExistingFiles) # 多选文件
dlg.setViewMode(QtWidgets.QFileDialog.Detail) # 详细模式
file_paths = dlg.getOpenFileNames(filter=filter)[0]
if len(file_paths) == 0:
return
image_list_dir = self.__imageListMgr.dirName
file_list = []
for path in file_paths:
if not os.path.exists(path):
continue
new_file = self.__copyToImagesDir(path)
if new_file != "" and image_list_dir in new_file:
# 去掉 image_list_dir 的路径和斜杠
begin = len(image_list_dir) + 1
file_list.append(new_file[begin:])
if len(file_list) > 0:
if self.__selectedClassify == "":
QtWidgets.QMessageBox.warning(self.__parent, "提示", "请先选择分类")
return
new_list = self.__imageListMgr.imageList(
self.__selectedClassify) + file_list
self.__imageListMgr.resetImageList(self.__selectedClassify,
new_list)
self.setImageList(self.__selectedClassify)
self.__imageListMgr.writeFile()
def __copyToImagesDir(self, image_path: str):
md5 = utils.fileMD5(image_path)
file_ext = utils.fileExtension(image_path)
to_dir = os.path.join(self.__imageListMgr.dirName, "images")
new_path = os.path.join(to_dir, md5 + file_ext)
if os.path.exists(to_dir):
utils.copyFile(image_path, new_path)
return new_path
else:
return ""
def removeImage(self):
"""移除图片"""
if not os.path.exists(self.__imageListMgr.filePath):
QtWidgets.QMessageBox.information(self.__parent, "提示",
"请先打开正确的图像库")
return
path_list = []
image_list = self.__ui.selectedItems()
if len(image_list) == 0:
return
question = QtWidgets.QMessageBox.question(self.__parent, "移除图片",
"确定移除所选图片吗?")
if question == QtWidgets.QMessageBox.No:
return
for i in range(self.__ui.count()):
item = self.__ui.item(i)
img_path = item.data(QtCore.Qt.UserRole)
if not item.isSelected():
path_list.append(img_path)
else:
# 从磁盘上删除图片
utils.removeFile(
os.path.join(self.__imageListMgr.dirName, img_path))
self.__imageListMgr.resetImageList(self.__selectedClassify, path_list)
self.setImageList(self.__selectedClassify)
self.__imageListMgr.writeFile()
def editImageClassify(self):
"""编辑图片分类"""
old_classify = self.__selectedClassify
dlg = imageeditclassifydialog.ImageEditClassifyDialog(
parent=self.__parent,
old_classify=old_classify,
classify_list=self.__imageListMgr.classifyList)
result = dlg.exec_()
new_classify = dlg.newClassify
if result == QtWidgets.QDialog.Accepted \
and new_classify != old_classify \
and new_classify != "":
self.__moveImage(old_classify, new_classify)
self.__imageListMgr.writeFile()
def __moveImage(self, old_classify, new_classify):
"""移动图片"""
keep_list = []
is_selected = False
move_list = self.__imageListMgr.imageList(new_classify)
for i in range(self.__ui.count()):
item = self.__ui.item(i)
txt = item.data(QtCore.Qt.UserRole)
if item.isSelected():
move_list.append(txt)
is_selected = True
else:
keep_list.append(txt)
if is_selected:
self.__imageListMgr.resetImageList(new_classify, move_list)
self.__imageListMgr.resetImageList(old_classify, keep_list)
self.setImageList(old_classify)
def selectAllImage(self):
"""选择所有图片"""
self.__ui.selectAll()
def reverseSelectImage(self):
"""反向选择图片"""
for i in range(self.__ui.count()):
item = self.__ui.item(i)
item.setSelected(not item.isSelected())
def cancelSelectImage(self):
"""取消选择图片"""
self.__ui.clearSelection()
def onSelectionChanged(self):
"""选择图像该变,发送选择的数量信号"""
count = len(self.__ui.selectedItems())
self.selectedCount.emit(count)
import os
from PyQt5 import QtCore, QtGui, QtWidgets
from mod import image_list_manager
from mod import ui_imageeditclassifydialog
from mod import utils
class ImageEditClassifyDialog(QtWidgets.QDialog):
"""图像编辑分类对话框"""
def __init__(self, parent, old_classify, classify_list):
super(ImageEditClassifyDialog, self).__init__(parent)
self.ui = mod.ui_imageeditclassifydialog.Ui_Dialog()
self.ui.setupUi(self) # 初始化主窗口界面
self.__oldClassify = old_classify
self.__classifyList = classify_list
self.__newClassify = ""
self.__searchResult = []
self.__initUi()
self.__connectSignal()
@property
def newClassify(self):
return self.__newClassify
def __initUi(self):
self.ui.oldLineEdit.setText(self.__oldClassify)
self.__setClassifyList(self.__classifyList)
self.ui.classifyListView.setEditTriggers(
QtWidgets.QAbstractItemView.NoEditTriggers)
def __connectSignal(self):
self.ui.classifyListView.clicked.connect(self.selectedListView)
self.ui.searchButton.clicked.connect(self.searchClassify)
def __setClassifyList(self, classify_list):
list_model = QtCore.QStringListModel(classify_list)
self.ui.classifyListView.setModel(list_model)
def selectedListView(self, index):
if not self.ui.classifyListView.currentIndex().isValid():
return
txt = index.data()
self.ui.newLineEdit.setText(txt)
self.__newClassify = txt
def searchClassify(self):
txt = self.ui.searchWordLineEdit.text()
self.__searchResult.clear()
for classify in self.__classifyList:
if txt in classify:
self.__searchResult.append(classify)
self.__setClassifyList(self.__searchResult)
import json
import os
import urllib3
import urllib.parse
class IndexHttpClient():
"""索引库客户端,使用 urllib3 连接,使用 urllib.parse 进行 url 编码"""
def __init__(self, host: str, port: int):
self.__host = host
self.__port = port
self.__http = urllib3.PoolManager()
self.__headers = {"Content-type": "application/json"}
def url(self):
return "http://{}:{}".format(self.__host, self.__port)
def new_index(self,
image_list_path: str,
index_root_path: str,
index_method="HNSW32",
force=False):
"""新建 重建 库"""
if index_method not in ["HNSW32", "FLAT", "IVF"]:
raise Exception(
"index_method 必须是 HNSW32, FLAT, IVF,实际值为:{}".format(
index_method))
params = {"image_list_path":image_list_path, \
"index_root_path":index_root_path, \
"index_method":index_method, \
"force":force}
return self.__post(self.url() + "/new_index?", params)
def open_index(self, index_root_path: str, image_list_path: str):
"""打开库"""
params = {
"index_root_path": index_root_path,
"image_list_path": image_list_path
}
return self.__post(self.url() + "/open_index?", params)
def update_index(self, image_list_path: str, index_root_path: str):
"""更新索引库"""
params = {"image_list_path":image_list_path, \
"index_root_path":index_root_path}
return self.__post(self.url() + "/update_index?", params)
def __post(self, url: str, params: dict):
"""发送 url 并接收数据"""
http = self.__http
encode_params = urllib.parse.urlencode(params)
get_url = url + encode_params
req = http.request("GET", get_url, headers=self.__headers)
result = json.loads(req.data)
if isinstance(result, str):
result = eval(result)
msg = result["error_message"]
if msg != None and len(msg) == 0:
msg = None
return msg
此差异已折叠。
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'ui/AddClassifyDialog.ui'
#
# Created by: PyQt5 UI code generator 5.15.5
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_AddClassifyDialog(object):
def setupUi(self, AddClassifyDialog):
AddClassifyDialog.setObjectName("AddClassifyDialog")
AddClassifyDialog.resize(286, 127)
AddClassifyDialog.setModal(True)
self.verticalLayout = QtWidgets.QVBoxLayout(AddClassifyDialog)
self.verticalLayout.setObjectName("verticalLayout")
self.label = QtWidgets.QLabel(AddClassifyDialog)
self.label.setObjectName("label")
self.verticalLayout.addWidget(self.label)
self.lineEdit = QtWidgets.QLineEdit(AddClassifyDialog)
self.lineEdit.setObjectName("lineEdit")
self.verticalLayout.addWidget(self.lineEdit)
spacerItem = QtWidgets.QSpacerItem(20, 11,
QtWidgets.QSizePolicy.Minimum,
QtWidgets.QSizePolicy.Expanding)
self.verticalLayout.addItem(spacerItem)
self.buttonBox = QtWidgets.QDialogButtonBox(AddClassifyDialog)
self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel
| QtWidgets.QDialogButtonBox.Ok)
self.buttonBox.setObjectName("buttonBox")
self.verticalLayout.addWidget(self.buttonBox)
self.retranslateUi(AddClassifyDialog)
self.buttonBox.accepted.connect(AddClassifyDialog.accept)
self.buttonBox.rejected.connect(AddClassifyDialog.reject)
QtCore.QMetaObject.connectSlotsByName(AddClassifyDialog)
def retranslateUi(self, AddClassifyDialog):
_translate = QtCore.QCoreApplication.translate
AddClassifyDialog.setWindowTitle(
_translate("AddClassifyDialog", "添加分类"))
self.label.setText(_translate("AddClassifyDialog", "分类名称"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
AddClassifyDialog = QtWidgets.QDialog()
ui = Ui_AddClassifyDialog()
ui.setupUi(AddClassifyDialog)
AddClassifyDialog.show()
sys.exit(app.exec_())
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'ui/ImageEditClassifyDialog.ui'
#
# Created by: PyQt5 UI code generator 5.15.5
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_Dialog(object):
def setupUi(self, Dialog):
Dialog.setObjectName("Dialog")
Dialog.resize(414, 415)
Dialog.setMinimumSize(QtCore.QSize(0, 0))
self.verticalLayout = QtWidgets.QVBoxLayout(Dialog)
self.verticalLayout.setObjectName("verticalLayout")
self.label = QtWidgets.QLabel(Dialog)
self.label.setObjectName("label")
self.verticalLayout.addWidget(self.label)
self.oldLineEdit = QtWidgets.QLineEdit(Dialog)
self.oldLineEdit.setEnabled(False)
self.oldLineEdit.setObjectName("oldLineEdit")
self.verticalLayout.addWidget(self.oldLineEdit)
self.label_2 = QtWidgets.QLabel(Dialog)
self.label_2.setObjectName("label_2")
self.verticalLayout.addWidget(self.label_2)
self.newLineEdit = QtWidgets.QLineEdit(Dialog)
self.newLineEdit.setEnabled(False)
self.newLineEdit.setObjectName("newLineEdit")
self.verticalLayout.addWidget(self.newLineEdit)
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
self.searchWordLineEdit = QtWidgets.QLineEdit(Dialog)
self.searchWordLineEdit.setObjectName("searchWordLineEdit")
self.horizontalLayout.addWidget(self.searchWordLineEdit)
self.searchButton = QtWidgets.QPushButton(Dialog)
self.searchButton.setObjectName("searchButton")
self.horizontalLayout.addWidget(self.searchButton)
self.verticalLayout.addLayout(self.horizontalLayout)
self.classifyListView = QtWidgets.QListView(Dialog)
self.classifyListView.setEnabled(True)
self.classifyListView.setMinimumSize(QtCore.QSize(400, 200))
self.classifyListView.setObjectName("classifyListView")
self.verticalLayout.addWidget(self.classifyListView)
self.buttonBox = QtWidgets.QDialogButtonBox(Dialog)
self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel
| QtWidgets.QDialogButtonBox.Ok)
self.buttonBox.setObjectName("buttonBox")
self.verticalLayout.addWidget(self.buttonBox)
self.retranslateUi(Dialog)
self.buttonBox.accepted.connect(Dialog.accept)
self.buttonBox.rejected.connect(Dialog.reject)
QtCore.QMetaObject.connectSlotsByName(Dialog)
def retranslateUi(self, Dialog):
_translate = QtCore.QCoreApplication.translate
Dialog.setWindowTitle(_translate("Dialog", "编辑图像分类"))
self.label.setText(_translate("Dialog", "原分类"))
self.label_2.setText(_translate("Dialog", "新分类"))
self.searchButton.setText(_translate("Dialog", "查找"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
Dialog = QtWidgets.QDialog()
ui = Ui_Dialog()
ui.setupUi(Dialog)
Dialog.show()
sys.exit(app.exec_())
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'ui/MainWindow.ui'
#
# Created by: PyQt5 UI code generator 5.15.5
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(833, 538)
MainWindow.setMinimumSize(QtCore.QSize(0, 0))
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.centralwidget)
self.verticalLayout_3.setObjectName("verticalLayout_3")
self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
self.horizontalLayout_3.setObjectName("horizontalLayout_3")
self.appMenuBtn = QtWidgets.QToolButton(self.centralwidget)
self.appMenuBtn.setObjectName("appMenuBtn")
self.horizontalLayout_3.addWidget(self.appMenuBtn)
self.saveImageLibraryBtn = QtWidgets.QToolButton(self.centralwidget)
self.saveImageLibraryBtn.setObjectName("saveImageLibraryBtn")
self.horizontalLayout_3.addWidget(self.saveImageLibraryBtn)
self.addClassifyBtn = QtWidgets.QToolButton(self.centralwidget)
self.addClassifyBtn.setObjectName("addClassifyBtn")
self.horizontalLayout_3.addWidget(self.addClassifyBtn)
self.removeClassifyBtn = QtWidgets.QToolButton(self.centralwidget)
self.removeClassifyBtn.setObjectName("removeClassifyBtn")
self.horizontalLayout_3.addWidget(self.removeClassifyBtn)
spacerItem = QtWidgets.QSpacerItem(40, 20,
QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_3.addItem(spacerItem)
self.imageScaleSlider = QtWidgets.QSlider(self.centralwidget)
self.imageScaleSlider.setMaximumSize(QtCore.QSize(400, 16777215))
self.imageScaleSlider.setMinimum(1)
self.imageScaleSlider.setMaximum(8)
self.imageScaleSlider.setPageStep(2)
self.imageScaleSlider.setOrientation(QtCore.Qt.Horizontal)
self.imageScaleSlider.setObjectName("imageScaleSlider")
self.horizontalLayout_3.addWidget(self.imageScaleSlider)
self.verticalLayout_3.addLayout(self.horizontalLayout_3)
self.splitter = QtWidgets.QSplitter(self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.splitter.sizePolicy().hasHeightForWidth())
self.splitter.setSizePolicy(sizePolicy)
self.splitter.setOrientation(QtCore.Qt.Horizontal)
self.splitter.setObjectName("splitter")
self.widget = QtWidgets.QWidget(self.splitter)
self.widget.setObjectName("widget")
self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.widget)
self.verticalLayout_2.setContentsMargins(0, 0, 0, 0)
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
self.searchClassifyHistoryCmb = QtWidgets.QComboBox(self.widget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.searchClassifyHistoryCmb.sizePolicy().hasHeightForWidth())
self.searchClassifyHistoryCmb.setSizePolicy(sizePolicy)
self.searchClassifyHistoryCmb.setEditable(True)
self.searchClassifyHistoryCmb.setObjectName("searchClassifyHistoryCmb")
self.horizontalLayout.addWidget(self.searchClassifyHistoryCmb)
self.searchClassifyBtn = QtWidgets.QToolButton(self.widget)
self.searchClassifyBtn.setObjectName("searchClassifyBtn")
self.horizontalLayout.addWidget(self.searchClassifyBtn)
self.verticalLayout_2.addLayout(self.horizontalLayout)
self.classifyListView = QtWidgets.QListView(self.widget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.classifyListView.sizePolicy().hasHeightForWidth())
self.classifyListView.setSizePolicy(sizePolicy)
self.classifyListView.setMinimumSize(QtCore.QSize(200, 0))
self.classifyListView.setEditTriggers(
QtWidgets.QAbstractItemView.NoEditTriggers)
self.classifyListView.setObjectName("classifyListView")
self.verticalLayout_2.addWidget(self.classifyListView)
self.widget1 = QtWidgets.QWidget(self.splitter)
self.widget1.setObjectName("widget1")
self.verticalLayout = QtWidgets.QVBoxLayout(self.widget1)
self.verticalLayout.setContentsMargins(0, 0, 0, 0)
self.verticalLayout.setObjectName("verticalLayout")
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
self.addImageBtn = QtWidgets.QToolButton(self.widget1)
self.addImageBtn.setObjectName("addImageBtn")
self.horizontalLayout_2.addWidget(self.addImageBtn)
self.removeImageBtn = QtWidgets.QToolButton(self.widget1)
self.removeImageBtn.setObjectName("removeImageBtn")
self.horizontalLayout_2.addWidget(self.removeImageBtn)
spacerItem1 = QtWidgets.QSpacerItem(40, 20,
QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_2.addItem(spacerItem1)
self.verticalLayout.addLayout(self.horizontalLayout_2)
self.imageListWidget = QtWidgets.QListWidget(self.widget1)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.imageListWidget.sizePolicy().hasHeightForWidth())
self.imageListWidget.setSizePolicy(sizePolicy)
self.imageListWidget.setMinimumSize(QtCore.QSize(200, 0))
self.imageListWidget.setStyleSheet(
"QListWidget::Item:hover{background:skyblue;padding-top:0px; padding-bottom:0px;}\n"
"QListWidget::item:selected{background:rgb(245, 121, 0); color:red;}"
)
self.imageListWidget.setObjectName("imageListWidget")
self.verticalLayout.addWidget(self.imageListWidget)
self.verticalLayout_3.addWidget(self.splitter)
MainWindow.setCentralWidget(self.centralwidget)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "识图图像库管理"))
self.appMenuBtn.setText(_translate("MainWindow", "..."))
self.saveImageLibraryBtn.setText(_translate("MainWindow", "..."))
self.addClassifyBtn.setText(_translate("MainWindow", "..."))
self.removeClassifyBtn.setText(_translate("MainWindow", "..."))
self.searchClassifyBtn.setText(_translate("MainWindow", "..."))
self.addImageBtn.setText(_translate("MainWindow", "..."))
self.removeImageBtn.setText(_translate("MainWindow", "..."))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
MainWindow = QtWidgets.QMainWindow()
ui = Ui_MainWindow()
ui.setupUi(MainWindow)
MainWindow.show()
sys.exit(app.exec_())
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'ui/NewlibraryDialog.ui'
#
# Created by: PyQt5 UI code generator 5.15.5
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_NewlibraryDialog(object):
def setupUi(self, NewlibraryDialog):
NewlibraryDialog.setObjectName("NewlibraryDialog")
NewlibraryDialog.resize(414, 230)
self.verticalLayout = QtWidgets.QVBoxLayout(NewlibraryDialog)
self.verticalLayout.setObjectName("verticalLayout")
self.label = QtWidgets.QLabel(NewlibraryDialog)
self.label.setObjectName("label")
self.verticalLayout.addWidget(self.label)
self.indexMethodCmb = QtWidgets.QComboBox(NewlibraryDialog)
self.indexMethodCmb.setEnabled(True)
self.indexMethodCmb.setObjectName("indexMethodCmb")
self.indexMethodCmb.addItem("")
self.indexMethodCmb.addItem("")
self.indexMethodCmb.addItem("")
self.verticalLayout.addWidget(self.indexMethodCmb)
self.resetCheckBox = QtWidgets.QCheckBox(NewlibraryDialog)
self.resetCheckBox.setObjectName("resetCheckBox")
self.verticalLayout.addWidget(self.resetCheckBox)
spacerItem = QtWidgets.QSpacerItem(20, 80,
QtWidgets.QSizePolicy.Minimum,
QtWidgets.QSizePolicy.Expanding)
self.verticalLayout.addItem(spacerItem)
self.buttonBox = QtWidgets.QDialogButtonBox(NewlibraryDialog)
self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel
| QtWidgets.QDialogButtonBox.Ok)
self.buttonBox.setObjectName("buttonBox")
self.verticalLayout.addWidget(self.buttonBox)
self.retranslateUi(NewlibraryDialog)
self.indexMethodCmb.setCurrentIndex(0)
self.buttonBox.accepted.connect(NewlibraryDialog.accept)
self.buttonBox.rejected.connect(NewlibraryDialog.reject)
QtCore.QMetaObject.connectSlotsByName(NewlibraryDialog)
def retranslateUi(self, NewlibraryDialog):
_translate = QtCore.QCoreApplication.translate
NewlibraryDialog.setWindowTitle(
_translate("NewlibraryDialog", "新建/重建 索引"))
self.label.setText(_translate("NewlibraryDialog", "索引方式"))
self.indexMethodCmb.setItemText(
0, _translate("NewlibraryDialog", "HNSW32"))
self.indexMethodCmb.setItemText(1,
_translate("NewlibraryDialog", "FLAT"))
self.indexMethodCmb.setItemText(2, _translate("NewlibraryDialog",
"IVF"))
self.resetCheckBox.setText(
_translate("NewlibraryDialog", "重建索引,警告:会覆盖原索引"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
NewlibraryDialog = QtWidgets.QDialog()
ui = Ui_NewlibraryDialog()
ui.setupUi(NewlibraryDialog)
NewlibraryDialog.show()
sys.exit(app.exec_())
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'ui/RenameClassifyDialog.ui'
#
# Created by: PyQt5 UI code generator 5.15.5
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_RenameClassifyDialog(object):
def setupUi(self, RenameClassifyDialog):
RenameClassifyDialog.setObjectName("RenameClassifyDialog")
RenameClassifyDialog.resize(342, 194)
self.verticalLayout = QtWidgets.QVBoxLayout(RenameClassifyDialog)
self.verticalLayout.setObjectName("verticalLayout")
self.oldlabel = QtWidgets.QLabel(RenameClassifyDialog)
self.oldlabel.setObjectName("oldlabel")
self.verticalLayout.addWidget(self.oldlabel)
self.oldNameLineEdit = QtWidgets.QLineEdit(RenameClassifyDialog)
self.oldNameLineEdit.setEnabled(False)
self.oldNameLineEdit.setObjectName("oldNameLineEdit")
self.verticalLayout.addWidget(self.oldNameLineEdit)
self.newlabel = QtWidgets.QLabel(RenameClassifyDialog)
self.newlabel.setObjectName("newlabel")
self.verticalLayout.addWidget(self.newlabel)
self.newNameLineEdit = QtWidgets.QLineEdit(RenameClassifyDialog)
self.newNameLineEdit.setObjectName("newNameLineEdit")
self.verticalLayout.addWidget(self.newNameLineEdit)
spacerItem = QtWidgets.QSpacerItem(20, 14,
QtWidgets.QSizePolicy.Minimum,
QtWidgets.QSizePolicy.Expanding)
self.verticalLayout.addItem(spacerItem)
self.buttonBox = QtWidgets.QDialogButtonBox(RenameClassifyDialog)
self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel
| QtWidgets.QDialogButtonBox.Ok)
self.buttonBox.setObjectName("buttonBox")
self.verticalLayout.addWidget(self.buttonBox)
self.retranslateUi(RenameClassifyDialog)
self.buttonBox.accepted.connect(RenameClassifyDialog.accept)
self.buttonBox.rejected.connect(RenameClassifyDialog.reject)
QtCore.QMetaObject.connectSlotsByName(RenameClassifyDialog)
def retranslateUi(self, RenameClassifyDialog):
_translate = QtCore.QCoreApplication.translate
RenameClassifyDialog.setWindowTitle(
_translate("RenameClassifyDialog", "重命名分类"))
self.oldlabel.setText(_translate("RenameClassifyDialog", "原名称"))
self.newlabel.setText(_translate("RenameClassifyDialog", "新名称"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
RenameClassifyDialog = QtWidgets.QDialog()
ui = Ui_RenameClassifyDialog()
ui.setupUi(RenameClassifyDialog)
RenameClassifyDialog.show()
sys.exit(app.exec_())
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'ui/WaitDialog.ui'
#
# Created by: PyQt5 UI code generator 5.15.5
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_WaitDialog(object):
def setupUi(self, WaitDialog):
WaitDialog.setObjectName("WaitDialog")
WaitDialog.setWindowModality(QtCore.Qt.NonModal)
WaitDialog.resize(324, 78)
self.verticalLayout = QtWidgets.QVBoxLayout(WaitDialog)
self.verticalLayout.setObjectName("verticalLayout")
self.msgLabel = QtWidgets.QLabel(WaitDialog)
self.msgLabel.setObjectName("msgLabel")
self.verticalLayout.addWidget(self.msgLabel)
self.progressBar = QtWidgets.QProgressBar(WaitDialog)
self.progressBar.setMaximum(0)
self.progressBar.setProperty("value", -1)
self.progressBar.setObjectName("progressBar")
self.verticalLayout.addWidget(self.progressBar)
spacerItem = QtWidgets.QSpacerItem(20, 1,
QtWidgets.QSizePolicy.Minimum,
QtWidgets.QSizePolicy.Expanding)
self.verticalLayout.addItem(spacerItem)
self.retranslateUi(WaitDialog)
QtCore.QMetaObject.connectSlotsByName(WaitDialog)
def retranslateUi(self, WaitDialog):
_translate = QtCore.QCoreApplication.translate
WaitDialog.setWindowTitle(_translate("WaitDialog", "请等待"))
self.msgLabel.setText(_translate("WaitDialog", "正在更新索引库,请等待。。。"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
WaitDialog = QtWidgets.QDialog()
ui = Ui_WaitDialog()
ui.setupUi(WaitDialog)
WaitDialog.show()
sys.exit(app.exec_())
import os
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
import hashlib
import shutil
from mod import image_list_manager
def setMenu(menu: QtWidgets.QMenu, text: str, triggered):
"""设置菜单"""
action = menu.addAction(text)
action.triggered.connect(triggered)
def fileMD5(file_path: str):
"""计算文件的MD5值"""
md5 = hashlib.md5()
with open(file_path, 'rb') as f:
md5.update(f.read())
return md5.hexdigest().lower()
def copyFile(from_path: str, to_path: str):
"""复制文件"""
shutil.copyfile(from_path, to_path)
return os.path.exists(to_path)
def removeFile(file_path: str):
"""删除文件"""
if os.path.exists(file_path):
os.remove(file_path)
return not os.path.exists(file_path)
def fileExtension(file_path: str):
"""获取文件的扩展名"""
return os.path.splitext(file_path)[1]
def copyImageToDir(self, from_image_path: str, to_dir_path: str):
"""复制图像文件到目标目录"""
if not os.path.exists(from_image_path) and not os.path.exists(to_dir_path):
return None
md5 = fileMD5(from_image_path)
file_ext = fileExtension(from_image_path)
new_path = os.path.join(to_dir_path, md5 + file_ext)
copyFile(from_image_path, new_path)
return new_path
def oneKeyImportFromFile(from_path: str, to_path: str):
"""从其它图像库 from_path {image_list.txt} 导入到图像库 to_path {image_list.txt}"""
if not os.path.exists(from_path) or not os.path.exists(to_path):
return None
if from_path == to_path:
return None
from_mgr = image_list_manager.ImageListManager(file_path=from_path)
to_mgr = image_list_manager.ImageListManager(file_path=to_path)
return oneKeyImport(from_mgr=from_mgr, to_mgr=to_mgr)
def oneKeyImportFromDirs(from_dir: str, to_image_list_path: str):
"""从其它图像库 from_dir 搜索子目录 导入到图像库 to_image_list_path"""
if not os.path.exists(from_dir) or not os.path.exists(to_image_list_path):
return None
if from_dir == os.path.dirname(to_image_list_path):
return None
from_mgr = image_list_manager.ImageListManager()
to_mgr = image_list_manager.ImageListManager(
file_path=to_image_list_path)
from_mgr.dirName = from_dir
sub_dir_list = os.listdir(from_dir)
for sub_dir in sub_dir_list:
real_sub_dir = os.path.join(from_dir, sub_dir)
if not os.path.isdir(real_sub_dir):
continue
img_list = os.listdir(real_sub_dir)
img_path = []
for img in img_list:
real_img = os.path.join(real_sub_dir, img)
if not os.path.isfile(real_img):
continue
img_path.append("{}/{}".format(sub_dir, img))
if len(img_path) == 0:
continue
from_mgr.addClassify(sub_dir)
from_mgr.resetImageList(sub_dir, img_path)
return oneKeyImport(from_mgr=from_mgr, to_mgr=to_mgr)
def oneKeyImport(from_mgr: image_list_manager.ImageListManager,
to_mgr: image_list_manager.ImageListManager):
"""一键导入"""
count = 0
for classify in from_mgr.classifyList:
img_list = from_mgr.realPathList(classify)
to_mgr.addClassify(classify)
to_img_list = to_mgr.imageList(classify)
new_img_list = []
for img in img_list:
from_image_path = img
to_dir_path = os.path.join(to_mgr.dirName, "images")
md5 = fileMD5(from_image_path)
file_ext = fileExtension(from_image_path)
new_path = os.path.join(to_dir_path, md5 + file_ext)
if os.path.exists(new_path):
# 如果新文件 MD5 重复跳过后面的复制文件操作
continue
copyFile(from_image_path, new_path)
new_img_list.append("images/" + md5 + file_ext)
count += 1
to_img_list += new_img_list
to_mgr.resetImageList(classify, to_img_list)
to_mgr.writeFile()
return count
def newFile(file_path: str):
"""创建文件"""
if os.path.exists(file_path):
return False
else:
with open(file_path, 'w') as f:
pass
return True
def isEmptyDir(dir_path: str):
"""判断目录是否为空"""
return not os.listdir(dir_path)
def initLibrary(dir_path: str):
"""初始化库"""
images_dir = os.path.join(dir_path, "images")
if not os.path.exists(images_dir):
os.makedirs(images_dir)
image_list_path = os.path.join(dir_path, "image_list.txt")
newFile(image_list_path)
return os.path.exists(dir_path)
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AddClassifyDialog</class>
<widget class="QDialog" name="AddClassifyDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>286</width>
<height>127</height>
</rect>
</property>
<property name="windowTitle">
<string>添加分类</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>分类名称</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit"/>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>11</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>AddClassifyDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>AddClassifyDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>414</width>
<height>415</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="windowTitle">
<string>编辑图像分类</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>原分类</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="oldLineEdit">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>新分类</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="newLineEdit">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="searchWordLineEdit"/>
</item>
<item>
<widget class="QPushButton" name="searchButton">
<property name="text">
<string>查找</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QListView" name="classifyListView">
<property name="enabled">
<bool>true</bool>
</property>
<property name="minimumSize">
<size>
<width>400</width>
<height>200</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>833</width>
<height>538</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="windowTitle">
<string>识图图像库管理</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QToolButton" name="appMenuBtn">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="saveImageLibraryBtn">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="addClassifyBtn">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="removeClassifyBtn">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QSlider" name="imageScaleSlider">
<property name="maximumSize">
<size>
<width>400</width>
<height>16777215</height>
</size>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>8</number>
</property>
<property name="pageStep">
<number>2</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QSplitter" name="splitter">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QWidget" name="">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QComboBox" name="searchClassifyHistoryCmb">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="searchClassifyBtn">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QListView" name="classifyListView">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QToolButton" name="addImageBtn">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="removeImageBtn">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QListWidget" name="imageListWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">QListWidget::Item:hover{background:skyblue;padding-top:0px; padding-bottom:0px;}
QListWidget::item:selected{background:rgb(245, 121, 0); color:red;}</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<resources/>
<connections/>
</ui>
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>NewlibraryDialog</class>
<widget class="QDialog" name="NewlibraryDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>414</width>
<height>230</height>
</rect>
</property>
<property name="windowTitle">
<string>新建/重建 索引</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>索引方式</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="indexMethodCmb">
<property name="enabled">
<bool>true</bool>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<item>
<property name="text">
<string>HNSW32</string>
</property>
</item>
<item>
<property name="text">
<string>FLAT</string>
</property>
</item>
<item>
<property name="text">
<string>IVF</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QCheckBox" name="resetCheckBox">
<property name="text">
<string>重建索引,警告:会覆盖原索引</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>80</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>NewlibraryDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>NewlibraryDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>RenameClassifyDialog</class>
<widget class="QDialog" name="RenameClassifyDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>342</width>
<height>194</height>
</rect>
</property>
<property name="windowTitle">
<string>重命名分类</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="oldlabel">
<property name="text">
<string>原名称</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="oldNameLineEdit">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="newlabel">
<property name="text">
<string>新名称</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="newNameLineEdit"/>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>14</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>RenameClassifyDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>RenameClassifyDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>WaitDialog</class>
<widget class="QDialog" name="WaitDialog">
<property name="windowModality">
<enum>Qt::NonModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>324</width>
<height>78</height>
</rect>
</property>
<property name="windowTitle">
<string>请等待</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="msgLabel">
<property name="text">
<string>正在更新索引库,请等待。。。</string>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="maximum">
<number>0</number>
</property>
<property name="value">
<number>-1</number>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>1</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
docs/images/structure.jpg

98.7 KB | W: | H:

docs/images/structure.jpg

1.8 MB | W: | H:

docs/images/structure.jpg
docs/images/structure.jpg
docs/images/structure.jpg
docs/images/structure.jpg
  • 2-up
  • Swipe
  • Onion skin
## PP-ShiTuV2图像识别系统
## 目录
- [PP-ShiTuV2简介](#pp-shituv2简介)
- [数据集介绍](#数据集介绍)
- [模型训练](#模型训练)
- [模型评估](#模型评估)
- [模型推理](#模型推理)
- [模型部署](#模型部署)
- [模块介绍](#模块介绍)
- [主体检测模型](#主体检测模型)
- [特征提取模型](#特征提取模型)
- [训练数据集优化与扩充](#训练数据集优化与扩充)
- [骨干网络优化](#骨干网络优化)
- [网络结构优化](#网络结构优化)
- [数据增强优化](#数据增强优化)
- [参考文献](#参考文献)
## PP-ShiTuV2简介
PP-ShiTuV2 是基于 PP-ShiTuV1 改进的一个实用轻量级通用图像识别系统,相比 PP-ShiTuV1 具有更高的识别精度、更强的泛化能力以及相近的推理速度<sup>*</sup>。该系统主要针对**训练数据集**、特征提取两个部分进行优化,使用了更优的骨干网络、损失函数与训练策略。使得 PP-ShiTuV2 在多个实际应用场景上的检索性能有显著提升。
<div align="center">
<img src="../../images/structure.jpg" />
</div>
### 数据集介绍
我们将训练数据进行了合理扩充与优化,更多细节请参考 [PP-ShiTuV2 数据集](../image_recognition_pipeline/feature_extraction.md#4-实验部分)
下面以 [PP-ShiTuV2](../image_recognition_pipeline/feature_extraction.md#4-实验部分) 的数据集为例,介绍 PP-ShiTuV2 模型的训练、评估、推理流程。
### 模型训练
首先下载好 [PP-ShiTuV2 数据集](../image_recognition_pipeline/feature_extraction.md#4-实验部分) 中的16个数据集并手动进行合并、生成标注文本文件 `train_reg_all_data_v2.txt`,最后放置到 `dataset` 目录下。
合并后的文件夹结构如下所示:
```python
dataset/
├── Aliproduct/ # Aliproduct数据集文件夹
├── SOP/ # SOPt数据集文件夹
├── ...
├── Products-10k/ # Products-10k数据集文件夹
├── ...
└── train_reg_all_data_v2.txt # 标注文本文件
```
生成的 `train_reg_all_data_v2.txt` 内容如下所示:
```log
...
Aliproduct/train/50029/1766228.jpg 50029
Aliproduct/train/50029/1764348.jpg 50029
...
Products-10k/train/88823.jpg 186440
Products-10k/train/88824.jpg 186440
...
```
然后在终端运行以下命令进行训练:
```shell
# 使用0号GPU进行单卡训练
export CUDA_VISIBLE_DEVICES=0
python3.7 tools/train.py \
-c ./ppcls/configs/GeneralRecognitionV2/GeneralRecognitionV2_PPLCNetV2_base.yaml
# 使用0,1,2,3,4,5,6,7号GPU进行8卡分布式训练
export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7
python3.7 -m paddle.distributed.launch tools/train.py \
-c ./ppcls/configs/GeneralRecognitionV2/GeneralRecognitionV2_PPLCNetV2_base.yaml
```
**注:** 在训练时默认会开启`eval_during_train`,每训练完 `eval_interval` 个epoch就会在配置文件中 `Eval` 指定的数据集上(默认为 Aliproduct )进行模型评估并计算得到参考指标。
### 模型评估
参考 [模型评估](../image_recognition_pipeline/feature_extraction.md#53-模型评估)
### 模型推理
参考 [Python模型推理](../quick_start/quick_start_recognition.md#22-图像识别体验)[C++ 模型推理](../../../deploy/cpp_shitu/readme.md)
### 模型部署
参考 [模型部署](../inference_deployment/recognition_serving_deploy.md#3-图像识别服务部署)
## 模块介绍
### 主体检测模型
主体检测模型使用 `PicoDet-LCNet_x2_5`,详细信息参考:[picodet_lcnet_x2_5_640_mainbody](../image_recognition_pipeline/mainbody_detection.md)
### 特征提取模型
#### 训练数据集优化与扩充
在 PP-ShiTuV1 所用训练数据集的基础上,我们去掉了使用范围较小的 iCartoonFace 数据集,同时加入了更多常见、使用范围更广的数据集,如 bird400、Cars、Products-10k、fruits-262。
#### 骨干网络优化
我们将骨干网络从 `PPLCNet_x2_5` 替换成了 [`PPLCNetV2_base`](../models/PP-LCNetV2.md),相比 `PPLCNet_x2_5``PPLCNetV2_base` 基本保持了较高的分类精度,并减少了40%的推理时间<sup>*</sup>
**注:** <sup>*</sup>推理环境基于 Intel(R) Xeon(R) Gold 6271C CPU @ 2.60GHz 硬件平台,OpenVINO 推理平台。
#### 网络结构优化
我们对 `PPLCNetV2_base` 结构做了微调,并加入了在行人重检测、地标检索、人脸识别等任务上较为通用有效的优化调整。主要包括以下几点:
1. `PPLCNetV2_base` 结构微调:实验发现网络末尾的 [`ReLU`](../../../ppcls/arch/backbone/legendary_models/pp_lcnet_v2.py#L322) 对检索性能有较大影响, [`FC`](../../../ppcls/arch/backbone/legendary_models/pp_lcnet_v2.py#L325) 也会导致检索性能轻微掉点,因此我们去掉了 BackBone 末尾的 `ReLU``FC`
2. `last stride=1`:只将最后一个 stage 的 stride 改为1,即不进行下采样,以此增加最后输出的特征图的语义信息,同时不对推理速度产生太大影响。
3. `BN Neck`:在全局池化层后加入一个 `BatchNorm1D` 结构,对特征向量的每个维度进行标准化,使得模型更快地收敛。
| 模型 | training data | recall@1%(mAP%) |
| :----------------------------------------------------------------- | :---------------- | :-------------- |
| PP-ShiTuV1 | PP-ShiTuV1 数据集 | 63.0(51.5) |
| PP-ShiTuV1+`PPLCNetV2_base`+`last_stride=1`+`BNNeck`+`TripletLoss` | PP-ShiTuV1 数据集 | 72.3(60.5) |
4. `TripletAngularMarginLoss`:我们基于原始的 `TripletLoss` (困难三元组损失)进行了改进,将优化目标从 L2 欧几里得空间更换成余弦空间,并额外加入了 anchor 与 positive/negtive 之间的硬性距离约束,让训练与测试的目标更加接近,提升模型的泛化能力。
| 模型 | training data | recall@1%(mAP%) |
| :------------------------------------------------------------------------------ | :---------------- | :-------------- |
| PP-ShiTuV1+`PPLCNetV2_base`+`last_stride=1`+`BNNeck`+`TripletLoss` | PP-ShiTuV2 数据集 | 71.9(60.2) |
| PP-ShiTuV1+`PPLCNetV2_base`+`last_stride=1`+`BNNeck`+`TripletAngularMarginLoss` | PP-ShiTuV2 数据集 | 73.7(61.0) |
#### 数据增强优化
我们考虑到实际相机拍摄时目标主体可能出现一定的旋转而不一定能保持正立状态,因此我们在数据增强中加入了适当的 [随机旋转增强](../../../ppcls/configs/GeneralRecognitionV2/GeneralRecognitionV2_PPLCNetV2_base.yaml#L117),以提升模型在真实场景中的检索能力。
结合以上3个优化点,最终在多个数据集的实验结果如下:
| 模型 | product<sup>*</sup> |
| :--------- | :------------------ |
| - | recall@1%(mAP%) |
| PP-ShiTuV1 | 63.0(51.5) |
| PP-ShiTuV2 | 73.7(61.0) |
| 模型 | Aliproduct | VeRI-Wild | LogoDet-3k | iCartoonFace | SOP | Inshop |
| :--------- | :-------------- | :-------------- | :-------------- | :-------------- | :-------------- | :-------------- |
| - | recall@1%(mAP%) | recall@1%(mAP%) | recall@1%(mAP%) | recall@1%(mAP%) | recall@1%(mAP%) | recall@1%(mAP%) |
| PP-ShiTuV1 | 83.9(83.2) | 88.7(60.1) | 86.1(73.6) | 84.1(72.3) | 79.7(58.6) | 89.1(69.4) |
| PP-ShiTuV2 | 84.2(83.3) | 87.8(68.8) | 88.0(63.2) | 53.6(27.5) | 77.6(55.3) | 90.8(74.3) |
| 模型 | gldv2 | imdb_face | iNat | instre | sketch | sop<sup>*</sup> |
| :--------- | :-------------- | :-------------- | :-------------- | :-------------- | :-------------- | :-------------- |
| - | recall@1%(mAP%) | recall@1%(mAP%) | recall@1%(mAP%) | recall@1%(mAP%) | recall@1%(mAP%) | recall@1%(mAP%) |
| PP-ShiTuV1 | 98.2(91.6) | 28.8(8.42) | 12.6(6.1) | 72.0(50.4) | 27.9(9.5) | 97.6(90.3) |
| PP-ShiTuV2 | 98.1(90.5) | 35.9(11.2) | 38.6(23.9) | 87.7(71.4) | 39.3(15.6) | 98.3(90.9) |
**注:** product数据集是为了验证PP-ShiTu的泛化性能而制作的数据集,所有的数据都没有在训练和测试集中出现。该数据包含8个大类(人脸、化妆品、地标、红酒、手表、车、运动鞋、饮料),299个小类。测试时,使用299个小类的标签进行测试;sop数据集来自[GPR1200: A Benchmark for General-Purpose Content-Based Image Retrieval](https://arxiv.org/abs/2111.13122),可视为“SOP”数据集的子集。
## 参考文献
1. Schall, Konstantin, et al. "GPR1200: A Benchmark for General-Purpose Content-Based Image Retrieval." International Conference on Multimedia Modeling. Springer, Cham, 2022.
2. Luo, Hao, et al. "A strong baseline and batch normalization neck for deep person re-identification." IEEE Transactions on Multimedia 22.10 (2019): 2597-2609.
......@@ -344,7 +344,7 @@ PaddleClas 提供了基于 Paddle2ONNX 来完成 inference 模型转换 ONNX 模
#### 5.1 方法总结与对比
上述算法能快速地迁移至多数的ReID模型中,能进一步提升ReID模型的性能。
上述算法能快速地迁移至多数的ReID模型中(参考 [PP-ShiTuV2](../PPShiTu/PPShiTuV2_introduction.md) ),能进一步提升ReID模型的性能,
#### 5.2 使用建议/FAQ
......
此差异已折叠。
......@@ -18,7 +18,7 @@ LeViT 是一种快速推理的、用于图像分类任务的混合神经网络
| Models | Top1 | Top5 | Reference<br>top1 | Reference<br>top5 | FLOPS<br>(M) | Params<br>(M) |
|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
| LeViT-128S | 0.7598 | 0.9269 | 0.766 | 0.929 | 305 | 7.8 |
| LeViT-128 | 0.7810 | 0.9371 | 0.786 | 0.940 | 406 | 9.2 |
| LeViT-128 | 0.7810 | 0.9372 | 0.786 | 0.940 | 406 | 9.2 |
| LeViT-192 | 0.7934 | 0.9446 | 0.800 | 0.947 | 658 | 11 |
| LeViT-256 | 0.8085 | 0.9497 | 0.816 | 0.954 | 1120 | 19 |
| LeViT-384 | 0.8191 | 0.9551 | 0.826 | 0.960 | 2353 | 39 |
......
......@@ -57,7 +57,7 @@ PP-LCNetV2 模型的网络整体结构如上图所示。PP-LCNetV2 模型是在
### 1.2.1 Rep 策略
卷积核的大小决定了卷积层感受野的大小,通过组合使用不同大小的卷积核,能够获取不同尺度的特征,因此 PPLCNetV2 在 Stage3、Stage4 中,在同一层组合使用 kernel size 分别为 5、3、1 的 DW 卷积,同时为了避免对模型效率的影响,使用重参数化(Re parameterization,Rep)策略对同层的 DW 卷积进行融合,如下图所示。
卷积核的大小决定了卷积层感受野的大小,通过组合使用不同大小的卷积核,能够获取不同尺度的特征,因此 PPLCNetV2 在 Stage4、Stage5 中,在同一层组合使用 kernel size 分别为 5、3、1 的 DW 卷积,同时为了避免对模型效率的影响,使用重参数化(Re parameterization,Rep)策略对同层的 DW 卷积进行融合,如下图所示。
![](../../images/PP-LCNetV2/rep.png)
......@@ -65,7 +65,7 @@ PP-LCNetV2 模型的网络整体结构如上图所示。PP-LCNetV2 模型是在
### 1.2.2 PW 卷积
深度可分离卷积通常由一层 DW 卷积和一层 PW 卷积组成,用以替换标准卷积,为了使深度可分离卷积具有更强的拟合能力,我们尝试使用两层 PW 卷积,同时为了控制模型效率不受影响,两层 PW 卷积设置为:第一个在通道维度对特征图压缩,第二个再通过放大还原特征图通道,如下图所示。通过实验发现,该策略能够显著提高模型性能,同时为了平衡对模型效率带来的影响,PPLCNetV2 仅在 Stage4、Stage5 中使用了该策略。
深度可分离卷积通常由一层 DW 卷积和一层 PW 卷积组成,用以替换标准卷积,为了使深度可分离卷积具有更强的拟合能力,我们尝试使用两层 PW 卷积,同时为了控制模型效率不受影响,两层 PW 卷积设置为:第一个在通道维度对特征图压缩,第二个再通过放大还原特征图通道,如下图所示。通过实验发现,该策略能够显著提高模型性能,同时为了平衡对模型效率带来的影响,PPLCNetV2 仅在 Stage4 中使用了该策略。
![](../../images/PP-LCNetV2/split_pw.png)
......@@ -73,7 +73,7 @@ PP-LCNetV2 模型的网络整体结构如上图所示。PP-LCNetV2 模型是在
### 1.2.3 Shortcut
残差结构(residual)自提出以来,被诸多模型广泛使用,但在轻量级卷积神经网络中,由于残差结构所带来的元素级(element-wise)加法操作,会对模型的速度造成影响,我们在 PP-LCNetV2 中,以 Stage 为单位实验了 残差结构对模型的影响,发现残差结构的使用并非一定会带来性能的提高,因此 PPLCNetV2 仅在最后一个 Stage 中的使用了残差结构:在 Block 中增加 Shortcut,如下图所示。
残差结构(residual)自提出以来,被诸多模型广泛使用,但在轻量级卷积神经网络中,由于残差结构所带来的元素级(element-wise)加法操作,会对模型的速度造成影响,我们在 PP-LCNetV2 中,以 Stage 为单位实验了残差结构对模型的影响,发现残差结构的使用并非一定会带来性能的提高,因此 PPLCNetV2 仅在最后一个 Stage 中的使用了残差结构:在 Block 中增加 Shortcut,如下图所示。
![](../../images/PP-LCNetV2/shortcut.png)
......@@ -87,7 +87,7 @@ PP-LCNetV2 模型的网络整体结构如上图所示。PP-LCNetV2 模型是在
### 1.2.5 SE 模块
虽然 SE 模块能够显著提高模型性能,但其对模型速度的影响同样不可忽视,在 PP-LCNetV1 中,我们发现在模型中后部使用 SE 模块能够获得最大化的收益。在 PP-LCNetV2 的优化过程中,我们以 Stage 为单位对 SE 模块的位置做了进一步实验,并发现在 Stage3 中使用能够取得更好的平衡。
虽然 SE 模块能够显著提高模型性能,但其对模型速度的影响同样不可忽视,在 PP-LCNetV1 中,我们发现在模型中后部使用 SE 模块能够获得最大化的收益。在 PP-LCNetV2 的优化过程中,我们以 Stage 为单位对 SE 模块的位置做了进一步实验,并发现在 Stage4 中使用能够取得更好的平衡。
<a name="1.3"></a>
......
......@@ -18,10 +18,10 @@ PVTV2 是 VisionTransformer 系列模型,该模型基于 PVT(Pyramid Vision
| Models | Top1 | Top5 | Reference<br>top1 | Reference<br>top5 | FLOPS<br>(G) | Params<br>(M) |
|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
| PVT_V2_B0 | 0.705 | 0.902 | 0.705 | - | 0.53 | 3.7 |
| PVT_V2_B1 | 0.787 | 0.945 | 0.787 | - | 2.0 | 14.0 |
| PVT_V2_B2 | 0.821 | 0.960 | 0.820 | - | 3.9 | 25.4 |
| PVT_V2_B3 | 0.831 | 0.965 | 0.831 | - | 6.7 | 45.2 |
| PVT_V2_B4 | 0.836 | 0.967 | 0.836 | - | 9.8 | 62.6 |
| PVT_V2_B5 | 0.837 | 0.966 | 0.838 | - | 11.4 | 82.0 |
| PVT_V2_B2_Linear | 0.821 | 0.961 | 0.821 | - | 3.8 | 22.6 |
| PVT_V2_B0 | 0.7052 | 0.9016 | 0.705 | - | 0.53 | 3.7 |
| PVT_V2_B1 | 0.7869 | 0.9450 | 0.787 | - | 2.0 | 14.0 |
| PVT_V2_B2 | 0.8206 | 0.9599 | 0.820 | - | 3.9 | 25.4 |
| PVT_V2_B3 | 0.8310 | 0.9648 | 0.831 | - | 6.7 | 45.2 |
| PVT_V2_B4 | 0.8361 | 0.9666 | 0.836 | - | 9.8 | 62.6 |
| PVT_V2_B5 | 0.8374 | 0.9662 | 0.838 | - | 11.4 | 82.0 |
| PVT_V2_B2_Linear | 0.8205 | 0.9605 | 0.820 | - | 3.8 | 22.6 |
......@@ -17,14 +17,12 @@ Twins 网络包括 Twins-PCPVT 和 Twins-SVT,其重点对空间注意力机制
| Models | Top1 | Top5 | Reference<br>top1 | Reference<br>top5 | FLOPs<br>(G) | Params<br>(M) |
|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
| pcpvt_small | 0.8082 | 0.9552 | 0.812 | - | 3.7 | 24.1 |
| pcpvt_base | 0.8242 | 0.9619 | 0.827 | - | 6.4 | 43.8 |
| pcpvt_large | 0.8273 | 0.9650 | 0.831 | - | 9.5 | 60.9 |
| alt_gvt_small | 0.8140 | 0.9546 | 0.817 | - | 2.8 | 24 |
| alt_gvt_base | 0.8294 | 0.9621 | 0.832 | - | 8.3 | 56 |
| alt_gvt_large | 0.8331 | 0.9642 | 0.837 | - | 14.8 | 99.2 |
**注**:与 Reference 的精度差异源于数据预处理不同。
| pcpvt_small | 0.8115 | 0.9567 | 0.812 | - | 3.7 | 24.1 |
| pcpvt_base | 0.8268 | 0.9627 | 0.827 | - | 6.4 | 43.8 |
| pcpvt_large | 0.8306 | 0.9659 | 0.831 | - | 9.5 | 60.9 |
| alt_gvt_small | 0.8177 | 0.9557 | 0.817 | - | 2.8 | 24 |
| alt_gvt_base | 0.8315 | 0.9629 | 0.832 | - | 8.3 | 56 |
| alt_gvt_large | 0.8364 | 0.9651 | 0.837 | - | 14.8 | 99.2 |
<a name='3'></a>
......
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册