Merge branch 'pre_release' into dev
This commit is contained in:
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,9 +1,14 @@
|
|||||||
|
**/.DS_Store
|
||||||
**/__pycache__
|
**/__pycache__
|
||||||
**/.vscode
|
**/.vscode
|
||||||
|
|
||||||
**/train_result
|
**/train_result
|
||||||
|
**/ckpt
|
||||||
|
**/*cache
|
||||||
|
**/.cache
|
||||||
|
**/data
|
||||||
|
|
||||||
**/logs
|
**/logs
|
||||||
**/.cache
|
|
||||||
**/tmp*
|
**/tmp*
|
||||||
**/data
|
**/data
|
||||||
**/*cache
|
**/*cache
|
||||||
|
|||||||
206
README.md
Normal file
206
README.md
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
📄 English | <a href="./assets/README_zh.md">中文</a>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<h1>
|
||||||
|
<img src="./assets/fire.svg" width=30, height=30>
|
||||||
|
𝚃𝚎𝚡𝚃𝚎𝚕𝚕𝚎𝚛
|
||||||
|
<img src="./assets/fire.svg" width=30, height=30>
|
||||||
|
</h1>
|
||||||
|
<p align="center">
|
||||||
|
🤗 <a href="https://huggingface.co/OleehyO/TexTeller"> Hugging Face</a>
|
||||||
|
</p>
|
||||||
|
<!-- <p align="center">
|
||||||
|
<img src="./assets/web_demo.gif" alt="TexTeller_demo" width=800>
|
||||||
|
</p> -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
https://github.com/OleehyO/TexTeller/assets/56267907/b23b2b2e-a663-4abb-b013-bd47238d513b
|
||||||
|
|
||||||
|
TexTeller is an end-to-end formula recognition model based on ViT, capable of converting images into corresponding LaTeX formulas.
|
||||||
|
|
||||||
|
TexTeller was trained with ~~550K~~7.5M image-formula pairs (dataset available [here](https://huggingface.co/datasets/OleehyO/latex-formulas)), compared to [LaTeX-OCR](https://github.com/lukas-blecher/LaTeX-OCR) which used a 100K dataset, TexTeller has **stronger generalization abilities** and **higher accuracy**, covering most use cases (**except for scanned images and handwritten formulas**).
|
||||||
|
|
||||||
|
> ~~We will soon release a TexTeller checkpoint trained on a 7.5M dataset~~
|
||||||
|
|
||||||
|
## 🔄 Change Log
|
||||||
|
|
||||||
|
* 📮[2024-03-25] TexTeller 2.0 released! The training data for TexTeller 2.0 has been increased to 7.5M (about **15 times more** than TexTeller 1.0 and also improved in data quality). The trained TexTeller 2.0 demonstrated **superior performance** in the test set, especially in recognizing rare symbols, complex multi-line formulas, and matrices.
|
||||||
|
> [There](./assets/test.pdf) are more test images here and a horizontal comparison of recognition models from different companies.
|
||||||
|
|
||||||
|
* 📮[2024-04-12] Trained a **formula detection model**, thereby enhancing the capability to detect and recognize formulas in entire documents (whole-image inference)!
|
||||||
|
|
||||||
|
|
||||||
|
## 🔑 Prerequisites
|
||||||
|
|
||||||
|
python=3.10
|
||||||
|
|
||||||
|
[pytorch](https://pytorch.org/get-started/locally/)
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> Only CUDA versions >= 12.0 have been fully tested, so it is recommended to use CUDA version >= 12.0
|
||||||
|
|
||||||
|
## 🚀 Getting Started
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/OleehyO/TexTeller
|
||||||
|
```
|
||||||
|
|
||||||
|
2. [Installing pytorch](https://pytorch.org/get-started/locally/#start-locally)
|
||||||
|
|
||||||
|
3. Install the project's dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Enter the `TexTeller/src` directory and run the following command in the terminal to start inference:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python inference.py -img "/path/to/image.{jpg,png}"
|
||||||
|
# use --inference-mode option to enable GPU(cuda or mps) inference
|
||||||
|
#+e.g. python inference.py -img "./img.jpg" --inference-mode cuda
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> The first time you run it, the required checkpoints will be downloaded from Hugging Face
|
||||||
|
|
||||||
|
## 🌐 Web Demo
|
||||||
|
|
||||||
|
Go to the `TexTeller/src` directory and run the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./start_web.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Enter `http://localhost:8501` in a browser to view the web demo.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> If you are Windows user, please run the `start_web.bat` file instead.
|
||||||
|
|
||||||
|
## 🧠 Full Image Inference
|
||||||
|
|
||||||
|
TexTeller also supports **formula detection and recognition** on full images, allowing for the detection of formulas throughout the image, followed by batch recognition of the formulas.
|
||||||
|
|
||||||
|
### Download Weights
|
||||||
|
|
||||||
|
English documentation formula detection [[link](https://huggingface.co/TonyLee1256/texteller_det/resolve/main/rtdetr_r50vd_6x_coco_trained_on_IBEM_en_papers.onnx?download=true)]: Trained on 8272 images from the [IBEM dataset](https://zenodo.org/records/4757865).
|
||||||
|
|
||||||
|
Chinese documentation formula detection [[link](https://huggingface.co/TonyLee1256/texteller_det/blob/main/rtdetr_r50vd_6x_coco_trained_on_cn_textbook.onnx)]: Trained on 2560 Chinese textbook images (100+ layouts).
|
||||||
|
|
||||||
|
### Formula Detection
|
||||||
|
|
||||||
|
Run the following command in the `TexTeller/src` directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python infer_det.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Detects all formulas in the full image, and the results are saved in `TexTeller/src/subimages`.
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="./assets/det_rec.png" width=400>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### Batch Formula Recognition
|
||||||
|
|
||||||
|
After **formula detection**, run the following command in the `TexTeller/src` directory:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
python rec_infer_from_crop_imgs.py
|
||||||
|
```
|
||||||
|
|
||||||
|
This will use the results of the previous formula detection to perform batch recognition on all cropped formulas, saving the recognition results as txt files in `TexTeller/src/results`.
|
||||||
|
|
||||||
|
## 📡 API Usage
|
||||||
|
|
||||||
|
We use [ray serve](https://github.com/ray-project/ray) to provide an API interface for TexTeller, allowing you to integrate TexTeller into your own projects. To start the server, you first need to enter the `TexTeller/src` directory and then run the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python server.py # default settings
|
||||||
|
```
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
| --- | --- |
|
||||||
|
| `-ckpt` | The path to the weights file, *default is TexTeller's pretrained weights*.|
|
||||||
|
| `-tknz` | The path to the tokenizer, *default is TexTeller's tokenizer*.|
|
||||||
|
| `-port` | The server's service port, *default is 8000*. |
|
||||||
|
| `--inference-mode` | Whether to use GPU(cuda or mps) for inference, *default is CPU*. |
|
||||||
|
| `--num_beams` | The number of beams for beam search, *default is 1*. |
|
||||||
|
| `--num_replicas` | The number of service replicas to run on the server, *default is 1 replica*. You can use more replicas to achieve greater throughput.|
|
||||||
|
| `--ncpu_per_replica` | The number of CPU cores used per service replica, *default is 1*. |
|
||||||
|
| `--ngpu_per_replica` | The number of GPUs used per service replica, *default is 1*. You can set this value between 0 and 1 to run multiple service replicas on one GPU to share the GPU, thereby improving GPU utilization. (Note, if --num_replicas is 2, --ngpu_per_replica is 0.7, then 2 GPUs must be available) |
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> A client demo can be found at `TexTeller/client/demo.py`, you can refer to `demo.py` to send requests to the server
|
||||||
|
|
||||||
|
## 🏋️♂️ Training
|
||||||
|
|
||||||
|
### Dataset
|
||||||
|
|
||||||
|
We provide an example dataset in the `TexTeller/src/models/ocr_model/train/dataset` directory, you can place your own images in the `images` directory and annotate each image with its corresponding formula in `formulas.jsonl`.
|
||||||
|
|
||||||
|
After preparing your dataset, you need to **change the `DIR_URL` variable to your own dataset's path** in `.../dataset/loader.py`
|
||||||
|
|
||||||
|
### Retraining the Tokenizer
|
||||||
|
|
||||||
|
If you are using a different dataset, you might need to retrain the tokenizer to obtain a different dictionary. After configuring your dataset, you can train your own tokenizer with the following command:
|
||||||
|
|
||||||
|
1. In `TexTeller/src/models/tokenizer/train.py`, change `new_tokenizer.save_pretrained('./your_dir_name')` to your custom output directory
|
||||||
|
> If you want to use a different dictionary size (default is 10k tokens), you need to change the `VOCAB_SIZE` variable in `TexTeller/src/models/globals.py`
|
||||||
|
|
||||||
|
2. **In the `TexTeller/src` directory**, run the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m models.tokenizer.train
|
||||||
|
```
|
||||||
|
|
||||||
|
### Training the Model
|
||||||
|
|
||||||
|
To train the model, you need to run the following command in the `TexTeller/src` directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m models.ocr_model.train.train
|
||||||
|
```
|
||||||
|
|
||||||
|
You can set your own tokenizer and checkpoint paths in `TexTeller/src/models/ocr_model/train/train.py` (refer to `train.py` for more information). If you are using the same architecture and dictionary as TexTeller, you can also fine-tune TexTeller's default weights with your own dataset.
|
||||||
|
|
||||||
|
In `TexTeller/src/globals.py` and `TexTeller/src/models/ocr_model/train/train_args.py`, you can change the model's architecture and training hyperparameters.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Our training scripts use the [Hugging Face Transformers](https://github.com/huggingface/transformers) library, so you can refer to their [documentation](https://huggingface.co/docs/transformers/v4.32.1/main_classes/trainer#transformers.TrainingArguments) for more details and configurations on training parameters.
|
||||||
|
|
||||||
|
## 🚧 Limitations
|
||||||
|
|
||||||
|
* Does not support scanned images and PDF document recognition
|
||||||
|
|
||||||
|
* Does not support handwritten formulas
|
||||||
|
|
||||||
|
## 📅 Plans
|
||||||
|
|
||||||
|
- [x] ~~Train the model with a larger dataset (7.5M samples, coming soon)~~
|
||||||
|
|
||||||
|
- [ ] Recognition of scanned images
|
||||||
|
|
||||||
|
- [ ] PDF document recognition + Support for English and Chinese scenarios
|
||||||
|
|
||||||
|
- [ ] Inference acceleration
|
||||||
|
|
||||||
|
- [ ] ...
|
||||||
|
|
||||||
|
## ⭐️ Stargazers over time
|
||||||
|
|
||||||
|
[](https://starchart.cc/OleehyO/TexTeller)
|
||||||
|
|
||||||
|
## 💖 Acknowledgments
|
||||||
|
|
||||||
|
Thanks to [LaTeX-OCR](https://github.com/lukas-blecher/LaTeX-OCR) which has brought me a lot of inspiration, and [im2latex-100K](https://zenodo.org/records/56198#.V2px0jXT6eA) which enriches our dataset.
|
||||||
|
|
||||||
|
## 👥 Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/OleehyO/TexTeller/graphs/contributors">
|
||||||
|
<a href="https://github.com/OleehyO/TexTeller/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=OleehyO/TexTeller" />
|
||||||
|
</a>
|
||||||
|
</a>
|
||||||
233
assets/README_zh.md
Normal file
233
assets/README_zh.md
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
📄 <a href="../README.md">English</a> | 中文
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<h1>
|
||||||
|
<img src="./fire.svg" width=30, height=30>
|
||||||
|
𝚃𝚎𝚡𝚃𝚎𝚕𝚕𝚎𝚛
|
||||||
|
<img src="./fire.svg" width=30, height=30>
|
||||||
|
</h1>
|
||||||
|
<p align="center">
|
||||||
|
🤗 <a href="https://huggingface.co/OleehyO/TexTeller">Hugging Face</a>
|
||||||
|
</p>
|
||||||
|
<!-- <p align="center">
|
||||||
|
<img src="./web_demo.gif" alt="TexTeller_demo" width=800>
|
||||||
|
</p> -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
https://github.com/OleehyO/TexTeller/assets/56267907/fb17af43-f2a5-47ce-ad1d-101db5fd7fbb
|
||||||
|
|
||||||
|
TexTeller是一个基于ViT的端到端公式识别模型,可以把图片转换为对应的latex公式
|
||||||
|
|
||||||
|
TexTeller用了~~550K~~7.5M的图片-公式对进行训练(数据集可以在[这里](https://huggingface.co/datasets/OleehyO/latex-formulas)获取),相比于[LaTeX-OCR](https://github.com/lukas-blecher/LaTeX-OCR)(使用了一个100K的数据集),TexTeller具有**更强的泛化能力**以及**更高的准确率**,可以覆盖大部分的使用场景(**扫描图片,手写公式除外**)。
|
||||||
|
|
||||||
|
> ~~我们马上就会发布一个使用7.5M数据集进行训练的TexTeller checkpoint~~
|
||||||
|
|
||||||
|
## 🔄 变更信息
|
||||||
|
|
||||||
|
* 📮[2024-03-25] TexTeller2.0发布!TexTeller2.0的训练数据增大到了7.5M(相较于TexTeller1.0**增加了~15倍**并且数据质量也有所改善)。训练后的TexTeller2.0在测试集中展现出了**更加优越的性能**,尤其在生僻符号、复杂多行、矩阵的识别场景中。
|
||||||
|
|
||||||
|
> 在[这里](./test.pdf)有更多的测试图片以及各家识别模型的横向对比。
|
||||||
|
>
|
||||||
|
* 📮[2024-04-12] 训练了**公式检测模型**,从而增加了对整个文档进行公式检测+公式识别(整图推理)的功能!
|
||||||
|
|
||||||
|
## 🔑 前置条件
|
||||||
|
|
||||||
|
python=3.10
|
||||||
|
|
||||||
|
[pytorch](https://pytorch.org/get-started/locally/)
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> 只有CUDA版本>= 12.0被完全测试过,所以最好使用>= 12.0的CUDA版本
|
||||||
|
|
||||||
|
## 🚀 开搞
|
||||||
|
|
||||||
|
1. 克隆本仓库:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/OleehyO/TexTeller
|
||||||
|
```
|
||||||
|
|
||||||
|
2. [安装pytorch](https://pytorch.org/get-started/locally/#start-locally)
|
||||||
|
3. 安装本项目的依赖包:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
4. 进入 `TexTeller/src`目录,在终端运行以下命令进行推理:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python inference.py -img "/path/to/image.{jpg,png}"
|
||||||
|
# use --inference-mode option to enable GPU(cuda or mps) inference
|
||||||
|
#+e.g. python inference.py -img "./img.jpg" --inference-mode cuda
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> 第一次运行时会在hugging face上下载所需要的checkpoints
|
||||||
|
|
||||||
|
## ❓ 常见问题:无法连接到Hugging Face
|
||||||
|
|
||||||
|
默认情况下,会在Hugging Face中下载模型权重,**如果你的远端服务器无法连接到Hugging Face**,你可以通过以下命令进行加载:
|
||||||
|
|
||||||
|
1. 安装huggingface hub包
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -U "huggingface_hub[cli]"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 在能连接Hugging Face的机器上下载模型权重:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
huggingface-cli download OleehyO/TexTeller --include "*.json" "*.bin" "*.txt" --repo-type model --local-dir "your/dir/path"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 把包含权重的目录上传远端服务器,然后把 `TexTeller/src/models/ocr_model/model/TexTeller.py`中的 `REPO_NAME = 'OleehyO/TexTeller'`修改为 `REPO_NAME = 'your/dir/path'`
|
||||||
|
|
||||||
|
如果你还想在训练模型时开启evaluate,你需要提前下载metric脚本并上传远端服务器:
|
||||||
|
|
||||||
|
1. 在能连接Hugging Face的机器上下载metric脚本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
huggingface-cli download evaluate-metric/google_bleu --repo-type space --local-dir "your/dir/path"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 把这个目录上传远端服务器,并在 `TexTeller/src/models/ocr_model/utils/metrics.py`中把 `evaluate.load('google_bleu')`改为 `evaluate.load('your/dir/path/google_bleu.py')`
|
||||||
|
|
||||||
|
## 🌐 网页演示
|
||||||
|
|
||||||
|
进入 `TexTeller/src` 目录,运行以下命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./start_web.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
在浏览器里输入 `http://localhost:8501`就可以看到web demo
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> 对于Windows用户, 请运行 `start_web.bat`文件.
|
||||||
|
|
||||||
|
## 🧠 整图推理
|
||||||
|
|
||||||
|
TexTeller还支持对整张图片进行**公式检测+公式识别**,从而对整图公式进行检测,然后进行批公式识别。
|
||||||
|
|
||||||
|
### 下载权重
|
||||||
|
|
||||||
|
英文文档公式检测 [[link](https://huggingface.co/TonyLee1256/texteller_det/resolve/main/rtdetr_r50vd_6x_coco_trained_on_IBEM_en_papers.onnx?download=true)]:在8272张[IBEM数据集](https://zenodo.org/records/4757865)上训练得到
|
||||||
|
|
||||||
|
中文文档公式检测 [[link](https://huggingface.co/TonyLee1256/texteller_det/blob/main/rtdetr_r50vd_6x_coco_trained_on_cn_textbook.onnx)]:在2560张中文教材数据(100+版式)上训练得到
|
||||||
|
|
||||||
|
### 公式检测
|
||||||
|
|
||||||
|
`TexTeller/src`目录下运行以下命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python infer_det.py
|
||||||
|
```
|
||||||
|
|
||||||
|
对整张图中的所有公式进行检测,结果保存在 `TexTeller/src/subimages`
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="det_rec.png" width=400>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### 公式批识别
|
||||||
|
|
||||||
|
在进行**公式检测后**, `TexTeller/src`目录下运行以下命令
|
||||||
|
|
||||||
|
```shell
|
||||||
|
python rec_infer_from_crop_imgs.py
|
||||||
|
```
|
||||||
|
|
||||||
|
会基于上一步公式检测的结果,对裁剪出的所有公式进行批量识别,将识别结果在 `TexTeller/src/results`中保存为txt文件。
|
||||||
|
|
||||||
|
## 📡 API调用
|
||||||
|
|
||||||
|
我们使用[ray serve](https://github.com/ray-project/ray)来对外提供一个TexTeller的API接口,通过使用这个接口,你可以把TexTeller整合到自己的项目里。要想启动server,你需要先进入 `TexTeller/src`目录然后运行以下命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
| 参数 | 描述 |
|
||||||
|
| - | - |
|
||||||
|
| `-ckpt` | 权重文件的路径,*默认为TexTeller的预训练权重*。 |
|
||||||
|
| `-tknz` | 分词器的路径,*默认为TexTeller的分词器*。 |
|
||||||
|
| `-port` | 服务器的服务端口,*默认是8000*。 |
|
||||||
|
| `--inference-mode`| 是否使用GPU(cuda或mps)推理,*默认为CPU*。 |
|
||||||
|
| `--num_beams` | beam search的beam数量,*默认是1*。 |
|
||||||
|
| `--num_replicas`| 在服务器上运行的服务副本数量,*默认1个副本*。你可以使用更多的副本来获取更大的吞吐量。 |
|
||||||
|
| `--ncpu_per_replica` | 每个服务副本所用的CPU核心数,*默认为1*。 |
|
||||||
|
| `--ngpu_per_replica` | 每个服务副本所用的GPU数量,*默认为1*。你可以把这个值设置成 0~1之间的数,这样会在一个GPU上运行多个服务副本来共享GPU,从而提高GPU的利用率。(注意,如果 --num_replicas 2, --ngpu_per_replica 0.7, 那么就必须要有2个GPU可用) |
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> 一个客户端demo可以在 `TexTeller/client/demo.py`找到,你可以参考 `demo.py`来给server发送请求
|
||||||
|
|
||||||
|
## 🏋️♂️ 训练
|
||||||
|
|
||||||
|
### 数据集
|
||||||
|
|
||||||
|
我们在 `TexTeller/src/models/ocr_model/train/dataset`目录中提供了一个数据集的例子,你可以把自己的图片放在 `images`目录然后在 `formulas.jsonl`中为每张图片标注对应的公式。
|
||||||
|
|
||||||
|
准备好数据集后,你需要在 `.../dataset/loader.py`中把 **`DIR_URL`变量改成你自己数据集的路径**
|
||||||
|
|
||||||
|
### 重新训练分词器
|
||||||
|
|
||||||
|
如果你使用了不一样的数据集,你可能需要重新训练tokenizer来得到一个不一样的字典。配置好数据集后,可以通过以下命令来训练自己的tokenizer:
|
||||||
|
|
||||||
|
1. 在 `TexTeller/src/models/tokenizer/train.py`中,修改 `new_tokenizer.save_pretrained('./your_dir_name')`为你自定义的输出目录
|
||||||
|
|
||||||
|
> 注意:如果要用一个不一样大小的字典(默认1W个token),你需要在 `TexTeller/src/models/globals.py`中修改 `VOCAB_SIZE`变量
|
||||||
|
>
|
||||||
|
2. **在 `TexTeller/src` 目录下**运行以下命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m models.tokenizer.train
|
||||||
|
```
|
||||||
|
|
||||||
|
### 训练模型
|
||||||
|
|
||||||
|
要想训练模型, 你需要在 `TexTeller/src`目录下运行以下命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m models.ocr_model.train.train
|
||||||
|
```
|
||||||
|
|
||||||
|
你可以在 `TexTeller/src/models/ocr_model/train/train.py`中设置自己的tokenizer和checkpoint路径(请参考 `train.py`)。如果你使用了与TexTeller一样的架构和相同的字典,你还可以用自己的数据集来微调TexTeller的默认权重。
|
||||||
|
|
||||||
|
在 `TexTeller/src/globals.py`和 `TexTeller/src/models/ocr_model/train/train_args.py`中,你可以改变模型的架构以及训练的超参数。
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> 我们的训练脚本使用了[Hugging Face Transformers](https://github.com/huggingface/transformers)库, 所以你可以参考他们提供的[文档](https://huggingface.co/docs/transformers/v4.32.1/main_classes/trainer#transformers.TrainingArguments)来获取更多训练参数的细节以及配置。
|
||||||
|
|
||||||
|
## 🚧 不足
|
||||||
|
|
||||||
|
* 不支持扫描图片以及PDF文档识别
|
||||||
|
* 不支持手写体公式
|
||||||
|
|
||||||
|
## 📅 计划
|
||||||
|
|
||||||
|
- [X] ~~使用更大的数据集来训练模型(7.5M样本,即将发布)~~
|
||||||
|
|
||||||
|
- [ ] 扫描图片识别
|
||||||
|
|
||||||
|
- [ ] PDF文档识别 + 中英文场景支持
|
||||||
|
|
||||||
|
- [ ] 推理加速
|
||||||
|
|
||||||
|
- [ ] ...
|
||||||
|
|
||||||
|
## ⭐️ 观星曲线
|
||||||
|
|
||||||
|
[](https://starchart.cc/OleehyO/TexTeller)
|
||||||
|
|
||||||
|
## 💖 感谢
|
||||||
|
|
||||||
|
Thanks to [LaTeX-OCR](https://github.com/lukas-blecher/LaTeX-OCR) which has brought me a lot of inspiration, and [im2latex-100K](https://zenodo.org/records/56198#.V2px0jXT6eA) which enriches our dataset.
|
||||||
|
|
||||||
|
## 👥 贡献者
|
||||||
|
|
||||||
|
<a href="https://github.com/OleehyO/TexTeller/graphs/contributors">
|
||||||
|
<a href="https://github.com/OleehyO/TexTeller/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=OleehyO/TexTeller" />
|
||||||
|
</a>
|
||||||
|
</a>
|
||||||
157
assets/css.css
157
assets/css.css
@@ -1,157 +0,0 @@
|
|||||||
html {
|
|
||||||
font-family: Inter;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1.5;
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
background: #fff;
|
|
||||||
color: #323232;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--space: 1;
|
|
||||||
--vspace: calc(var(--space) * 1rem);
|
|
||||||
--vspace-0: calc(3 * var(--space) * 1rem);
|
|
||||||
--vspace-1: calc(2 * var(--space) * 1rem);
|
|
||||||
--vspace-2: calc(1.5 * var(--space) * 1rem);
|
|
||||||
--vspace-3: calc(0.5 * var(--space) * 1rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.app {
|
|
||||||
max-width: 748px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose p {
|
|
||||||
margin: var(--vspace) 0;
|
|
||||||
line-height: var(--vspace * 2);
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
font-family: "inconsolata", sans-serif;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1,
|
|
||||||
h1 code {
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: calc(2.5 / var(--space) * var(--vspace));
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 code {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
padding-bottom: 5px;
|
|
||||||
position: relative;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin: var(--vspace-1) 0 var(--vspace-2) 0;
|
|
||||||
line-height: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3,
|
|
||||||
h3 code {
|
|
||||||
margin: var(--vspace-1) 0 var(--vspace-2) 0;
|
|
||||||
line-height: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h4,
|
|
||||||
h5,
|
|
||||||
h6 {
|
|
||||||
margin: var(--vspace-3) 0 var(--vspace-3) 0;
|
|
||||||
line-height: var(--vspace);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bigtitle,
|
|
||||||
h1,
|
|
||||||
h1 code {
|
|
||||||
font-size: calc(8px * 4.5);
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title,
|
|
||||||
h2,
|
|
||||||
h2 code {
|
|
||||||
font-size: calc(8px * 3.375);
|
|
||||||
font-weight: lighter;
|
|
||||||
word-break: break-word;
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subheading1,
|
|
||||||
h3,
|
|
||||||
h3 code {
|
|
||||||
font-size: calc(8px * 1.8);
|
|
||||||
font-weight: 600;
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 code {
|
|
||||||
padding: 0;
|
|
||||||
position: relative;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote {
|
|
||||||
font-size: calc(8px * 1.1667);
|
|
||||||
font-style: italic;
|
|
||||||
line-height: calc(1.1667 * var(--vspace));
|
|
||||||
margin: var(--vspace-2) var(--vspace-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.subheading2,
|
|
||||||
h4 {
|
|
||||||
font-size: calc(8px * 1.4292);
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subheading3,
|
|
||||||
h5 {
|
|
||||||
font-size: calc(8px * 1.2917);
|
|
||||||
line-height: calc(1.2917 * var(--vspace));
|
|
||||||
|
|
||||||
font-weight: lighter;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.15em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h6 {
|
|
||||||
font-size: calc(8px * 1.1667);
|
|
||||||
font-size: 1.1667em;
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: italic;
|
|
||||||
font-family: "le-monde-livre-classic-byol", serif !important;
|
|
||||||
letter-spacing: 0px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#start .md > *:first-child {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 + h3 {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.md hr {
|
|
||||||
border: none;
|
|
||||||
border-top: 1px solid var(--block-border-color);
|
|
||||||
margin: var(--vspace-2) 0 var(--vspace-2) 0;
|
|
||||||
}
|
|
||||||
.prose ul {
|
|
||||||
margin: var(--vspace-2) 0 var(--vspace-1) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gap {
|
|
||||||
gap: 0;
|
|
||||||
}
|
|
||||||
BIN
assets/det_rec.png
Normal file
BIN
assets/det_rec.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 919 KiB |
460
assets/fire.svg
Normal file
460
assets/fire.svg
Normal file
@@ -0,0 +1,460 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="" width="200px" height="100px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
|
||||||
|
<defs>
|
||||||
|
<filter id="ldio-ekpf7uvh2aq-filter" filterUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
|
||||||
|
<feGaussianBlur in="SourceGraphic" stdDeviation="3"></feGaussianBlur>
|
||||||
|
<feComponentTransfer result="cutoff">
|
||||||
|
<feFuncA type="linear" slope="10" intercept="-5"></feFuncA>
|
||||||
|
</feComponentTransfer>
|
||||||
|
</filter>
|
||||||
|
</defs><g filter="url(#ldio-ekpf7uvh2aq-filter)"><circle cx="45" cy="154.67770829199992" r="42" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="154.67770829199992;-27.568110790210763" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.7914508173328552s"></animate>
|
||||||
|
<animate attributeName="r" values="42;0;0" keyTimes="0;0.6593879177915443;1" dur="1s" repeatCount="indefinite" begin="-0.7914508173328552s"></animate>
|
||||||
|
</circle><circle cx="53" cy="156.51873756667007" r="43" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="156.51873756667007;-28.593472199379597" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.8990601299952956s"></animate>
|
||||||
|
<animate attributeName="r" values="43;0;0" keyTimes="0;0.9199190750649376;1" dur="1s" repeatCount="indefinite" begin="-0.8990601299952956s"></animate>
|
||||||
|
</circle><circle cx="22" cy="118.4676277511406" r="6" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="118.4676277511406;-1.812134766063739" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.2574158626531723s"></animate>
|
||||||
|
<animate attributeName="r" values="6;0;0" keyTimes="0;0.7424894336620584;1" dur="1s" repeatCount="indefinite" begin="-0.2574158626531723s"></animate>
|
||||||
|
</circle><circle cx="56" cy="143.3980016480395" r="34" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="143.3980016480395;-23.264651741765398" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.5292591072219247s"></animate>
|
||||||
|
<animate attributeName="r" values="34;0;0" keyTimes="0;0.8257208789488842;1" dur="1s" repeatCount="indefinite" begin="-0.5292591072219247s"></animate>
|
||||||
|
</circle><circle cx="43" cy="154.61226210156264" r="43" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="154.61226210156264;-39.72257238426019" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.9349241678635103s"></animate>
|
||||||
|
<animate attributeName="r" values="43;0;0" keyTimes="0;0.6655411648349204;1" dur="1s" repeatCount="indefinite" begin="-0.9349241678635103s"></animate>
|
||||||
|
</circle><circle cx="36" cy="141.18233539125538" r="23" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="141.18233539125538;-11.919782601799477" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.9661184430026497s"></animate>
|
||||||
|
<animate attributeName="r" values="23;0;0" keyTimes="0;0.7340510315067473;1" dur="1s" repeatCount="indefinite" begin="-0.9661184430026497s"></animate>
|
||||||
|
</circle><circle cx="55" cy="137.61381349909033" r="35" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="137.61381349909033;-27.023105799592948" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.7882390392923937s"></animate>
|
||||||
|
<animate attributeName="r" values="35;0;0" keyTimes="0;0.5596286394923506;1" dur="1s" repeatCount="indefinite" begin="-0.7882390392923937s"></animate>
|
||||||
|
</circle><circle cx="81" cy="116.42482869722863" r="6" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="116.42482869722863;2.642571962973477" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.6838551001109257s"></animate>
|
||||||
|
<animate attributeName="r" values="6;0;0" keyTimes="0;0.8530428185299654;1" dur="1s" repeatCount="indefinite" begin="-0.6838551001109257s"></animate>
|
||||||
|
</circle><circle cx="51" cy="144.1337397120671" r="41" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="144.1337397120671;-35.62888188299487" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.8931867510460544s"></animate>
|
||||||
|
<animate attributeName="r" values="41;0;0" keyTimes="0;0.9351064787950636;1" dur="1s" repeatCount="indefinite" begin="-0.8931867510460544s"></animate>
|
||||||
|
</circle><circle cx="22" cy="127.94124738258117" r="20" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="127.94124738258117;-4.588101238414598" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.9129507531699166s"></animate>
|
||||||
|
<animate attributeName="r" values="20;0;0" keyTimes="0;0.9626971761152365;1" dur="1s" repeatCount="indefinite" begin="-0.9129507531699166s"></animate>
|
||||||
|
</circle><circle cx="51" cy="130.13871763314205" r="21" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="130.13871763314205;-2.771870373434613" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.16276671760313832s"></animate>
|
||||||
|
<animate attributeName="r" values="21;0;0" keyTimes="0;0.6367210977937845;1" dur="1s" repeatCount="indefinite" begin="-0.16276671760313832s"></animate>
|
||||||
|
</circle><circle cx="28" cy="130.94671647108635" r="26" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="130.94671647108635;-20.54470862263146" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.010777607623041363s"></animate>
|
||||||
|
<animate attributeName="r" values="26;0;0" keyTimes="0;0.5986827903483527;1" dur="1s" repeatCount="indefinite" begin="-0.010777607623041363s"></animate>
|
||||||
|
</circle><circle cx="32" cy="133.57559887485095" r="18" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="133.57559887485095;-13.998747273650661" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.6849903294560423s"></animate>
|
||||||
|
<animate attributeName="r" values="18;0;0" keyTimes="0;0.9272684317035897;1" dur="1s" repeatCount="indefinite" begin="-0.6849903294560423s"></animate>
|
||||||
|
</circle><circle cx="50" cy="129.2368025879272" r="29" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="129.2368025879272;-21.38222818211007" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.2570532837614655s"></animate>
|
||||||
|
<animate attributeName="r" values="29;0;0" keyTimes="0;0.5349692982819836;1" dur="1s" repeatCount="indefinite" begin="-0.2570532837614655s"></animate>
|
||||||
|
</circle><circle cx="54" cy="147.67203918209864" r="32" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="147.67203918209864;-23.292000640460095" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.8840781999829185s"></animate>
|
||||||
|
<animate attributeName="r" values="32;0;0" keyTimes="0;0.9905440228534627;1" dur="1s" repeatCount="indefinite" begin="-0.8840781999829185s"></animate>
|
||||||
|
</circle><circle cx="49" cy="156.33097983975816" r="43" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="156.33097983975816;-30.688836209655307" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.6363282840605137s"></animate>
|
||||||
|
<animate attributeName="r" values="43;0;0" keyTimes="0;0.578321371334853;1" dur="1s" repeatCount="indefinite" begin="-0.6363282840605137s"></animate>
|
||||||
|
</circle><circle cx="53" cy="150.73132612778645" r="38" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="150.73132612778645;-24.243875812169208" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.6889884148164682s"></animate>
|
||||||
|
<animate attributeName="r" values="38;0;0" keyTimes="0;0.9820908894527897;1" dur="1s" repeatCount="indefinite" begin="-0.6889884148164682s"></animate>
|
||||||
|
</circle><circle cx="58" cy="136.92364235316566" r="30" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="136.92364235316566;-14.514104757207221" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.3274028295945308s"></animate>
|
||||||
|
<animate attributeName="r" values="30;0;0" keyTimes="0;0.9109990458833535;1" dur="1s" repeatCount="indefinite" begin="-0.3274028295945308s"></animate>
|
||||||
|
</circle><circle cx="21" cy="125.47085228007643" r="18" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="125.47085228007643;-8.232426956653288" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.11103461733078768s"></animate>
|
||||||
|
<animate attributeName="r" values="18;0;0" keyTimes="0;0.7718042613876622;1" dur="1s" repeatCount="indefinite" begin="-0.11103461733078768s"></animate>
|
||||||
|
</circle><circle cx="57" cy="154.13251799723747" r="37" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="154.13251799723747;-18.665203993986026" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.8263441768461145s"></animate>
|
||||||
|
<animate attributeName="r" values="37;0;0" keyTimes="0;0.7148325280461965;1" dur="1s" repeatCount="indefinite" begin="-0.8263441768461145s"></animate>
|
||||||
|
</circle><circle cx="52" cy="163.55969451733722" r="47" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="163.55969451733722;-45.32343944696123" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.08605155305311041s"></animate>
|
||||||
|
<animate attributeName="r" values="47;0;0" keyTimes="0;0.8554524873372089;1" dur="1s" repeatCount="indefinite" begin="-0.08605155305311041s"></animate>
|
||||||
|
</circle><circle cx="43" cy="150.72861891310126" r="42" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="150.72861891310126;-23.942286768617272" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.8013052401764136s"></animate>
|
||||||
|
<animate attributeName="r" values="42;0;0" keyTimes="0;0.6681090498432822;1" dur="1s" repeatCount="indefinite" begin="-0.8013052401764136s"></animate>
|
||||||
|
</circle><circle cx="62" cy="109.2607457626771" r="2" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="109.2607457626771;3.194634855160243" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.7901767326521292s"></animate>
|
||||||
|
<animate attributeName="r" values="2;0;0" keyTimes="0;0.7018579919397697;1" dur="1s" repeatCount="indefinite" begin="-0.7901767326521292s"></animate>
|
||||||
|
</circle><circle cx="29" cy="132.04950518708117" r="26" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="132.04950518708117;-24.268419710129816" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.9729317633977274s"></animate>
|
||||||
|
<animate attributeName="r" values="26;0;0" keyTimes="0;0.8277305604086497;1" dur="1s" repeatCount="indefinite" begin="-0.9729317633977274s"></animate>
|
||||||
|
</circle><circle cx="54" cy="150.69697127653222" r="41" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="150.69697127653222;-27.168516505190766" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.5902016146688314s"></animate>
|
||||||
|
<animate attributeName="r" values="41;0;0" keyTimes="0;0.8175867220161461;1" dur="1s" repeatCount="indefinite" begin="-0.5902016146688314s"></animate>
|
||||||
|
</circle><circle cx="50" cy="115.01352405454155" r="7" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="115.01352405454155;-4.5076288690789195" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.5091907734741129s"></animate>
|
||||||
|
<animate attributeName="r" values="7;0;0" keyTimes="0;0.6751846924914742;1" dur="1s" repeatCount="indefinite" begin="-0.5091907734741129s"></animate>
|
||||||
|
</circle><circle cx="65" cy="137.6419430633514" r="34" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="137.6419430633514;-17.00344965868893" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.34747192063247945s"></animate>
|
||||||
|
<animate attributeName="r" values="34;0;0" keyTimes="0;0.5212737600536792;1" dur="1s" repeatCount="indefinite" begin="-0.34747192063247945s"></animate>
|
||||||
|
</circle><circle cx="34" cy="127.0455079544209" r="14" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="127.0455079544209;-3.6990759299641454" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.4890615261218786s"></animate>
|
||||||
|
<animate attributeName="r" values="14;0;0" keyTimes="0;0.6183470012170013;1" dur="1s" repeatCount="indefinite" begin="-0.4890615261218786s"></animate>
|
||||||
|
</circle><circle cx="12" cy="120.43345098845494" r="3" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="120.43345098845494;9.74374931913883" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.3026505339978601s"></animate>
|
||||||
|
<animate attributeName="r" values="3;0;0" keyTimes="0;0.5414300978949788;1" dur="1s" repeatCount="indefinite" begin="-0.3026505339978601s"></animate>
|
||||||
|
</circle><circle cx="49" cy="161.35205628493102" r="43" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="161.35205628493102;-37.872089939512506" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.38741962448531564s"></animate>
|
||||||
|
<animate attributeName="r" values="43;0;0" keyTimes="0;0.5096615889177538;1" dur="1s" repeatCount="indefinite" begin="-0.38741962448531564s"></animate>
|
||||||
|
</circle><circle cx="54" cy="146.5769009919314" r="44" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="146.5769009919314;-38.33530354334875" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.34335748774106034s"></animate>
|
||||||
|
<animate attributeName="r" values="44;0;0" keyTimes="0;0.743420827137904;1" dur="1s" repeatCount="indefinite" begin="-0.34335748774106034s"></animate>
|
||||||
|
</circle><circle cx="20" cy="111.24659457696168" r="7" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="111.24659457696168;10.851798254886354" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.6282307990647713s"></animate>
|
||||||
|
<animate attributeName="r" values="7;0;0" keyTimes="0;0.8297799829349941;1" dur="1s" repeatCount="indefinite" begin="-0.6282307990647713s"></animate>
|
||||||
|
</circle><circle cx="50" cy="164.0676485495781" r="45" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="164.0676485495781;-31.499414285176986" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.7760446285439819s"></animate>
|
||||||
|
<animate attributeName="r" values="45;0;0" keyTimes="0;0.5740694195049653;1" dur="1s" repeatCount="indefinite" begin="-0.7760446285439819s"></animate>
|
||||||
|
</circle><circle cx="63" cy="121.15583070803987" r="16" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="121.15583070803987;-2.1042758907266066" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.2305276534763374s"></animate>
|
||||||
|
<animate attributeName="r" values="16;0;0" keyTimes="0;0.5205278426126575;1" dur="1s" repeatCount="indefinite" begin="-0.2305276534763374s"></animate>
|
||||||
|
</circle><circle cx="70" cy="143.94247592516618" r="29" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="143.94247592516618;-23.62297573618442" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.5284797120514513s"></animate>
|
||||||
|
<animate attributeName="r" values="29;0;0" keyTimes="0;0.9336811516026573;1" dur="1s" repeatCount="indefinite" begin="-0.5284797120514513s"></animate>
|
||||||
|
</circle><circle cx="21" cy="122.79868387744153" r="20" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="122.79868387744153;-13.104461771681535" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.8845782118773111s"></animate>
|
||||||
|
<animate attributeName="r" values="20;0;0" keyTimes="0;0.904216846935756;1" dur="1s" repeatCount="indefinite" begin="-0.8845782118773111s"></animate>
|
||||||
|
</circle><circle cx="46" cy="143.70707265719267" r="24" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="143.70707265719267;-20.28891701845349" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.23245576862802375s"></animate>
|
||||||
|
<animate attributeName="r" values="24;0;0" keyTimes="0;0.6586288079548765;1" dur="1s" repeatCount="indefinite" begin="-0.23245576862802375s"></animate>
|
||||||
|
</circle><circle cx="65" cy="140.13731645312657" r="22" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="140.13731645312657;-5.338876455584764" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.7182419259629308s"></animate>
|
||||||
|
<animate attributeName="r" values="22;0;0" keyTimes="0;0.8813907372203135;1" dur="1s" repeatCount="indefinite" begin="-0.7182419259629308s"></animate>
|
||||||
|
</circle><circle cx="37" cy="139.00958710472267" r="35" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="139.00958710472267;-25.68265144780311" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.7030100698848409s"></animate>
|
||||||
|
<animate attributeName="r" values="35;0;0" keyTimes="0;0.7320613459176248;1" dur="1s" repeatCount="indefinite" begin="-0.7030100698848409s"></animate>
|
||||||
|
</circle><circle cx="45" cy="146.6744507961619" r="44" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="146.6744507961619;-38.087338695486295" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.8319540053556033s"></animate>
|
||||||
|
<animate attributeName="r" values="44;0;0" keyTimes="0;0.5904241586083279;1" dur="1s" repeatCount="indefinite" begin="-0.8319540053556033s"></animate>
|
||||||
|
</circle><circle cx="53" cy="116.16529146873187" r="15" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="116.16529146873187;-3.17669223153381" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.7864341362651808s"></animate>
|
||||||
|
<animate attributeName="r" values="15;0;0" keyTimes="0;0.589186107816807;1" dur="1s" repeatCount="indefinite" begin="-0.7864341362651808s"></animate>
|
||||||
|
</circle><circle cx="29" cy="141.6902909599232" r="23" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="141.6902909599232;-16.250272669063218" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.18084365714200346s"></animate>
|
||||||
|
<animate attributeName="r" values="23;0;0" keyTimes="0;0.8116571311237253;1" dur="1s" repeatCount="indefinite" begin="-0.18084365714200346s"></animate>
|
||||||
|
</circle><circle cx="65" cy="143.73302386926983" r="32" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="143.73302386926983;-24.229369251904558" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.5786484558188305s"></animate>
|
||||||
|
<animate attributeName="r" values="32;0;0" keyTimes="0;0.8515606125902615;1" dur="1s" repeatCount="indefinite" begin="-0.5786484558188305s"></animate>
|
||||||
|
</circle><circle cx="39" cy="143.3951504366216" r="33" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="143.3951504366216;-27.75171362166084" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.1481578769905092s"></animate>
|
||||||
|
<animate attributeName="r" values="33;0;0" keyTimes="0;0.797255218191478;1" dur="1s" repeatCount="indefinite" begin="-0.1481578769905092s"></animate>
|
||||||
|
</circle><circle cx="59" cy="129.28605384114482" r="27" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="129.28605384114482;-12.095864862844131" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.23581997562886903s"></animate>
|
||||||
|
<animate attributeName="r" values="27;0;0" keyTimes="0;0.8271538616610963;1" dur="1s" repeatCount="indefinite" begin="-0.23581997562886903s"></animate>
|
||||||
|
</circle><circle cx="70" cy="144.09835508207823" r="28" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="144.09835508207823;-13.162793363728145" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.23606519556482253s"></animate>
|
||||||
|
<animate attributeName="r" values="28;0;0" keyTimes="0;0.73085815703799;1" dur="1s" repeatCount="indefinite" begin="-0.23606519556482253s"></animate>
|
||||||
|
</circle><circle cx="48" cy="145.01565757702042" r="44" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="145.01565757702042;-32.30510020024561" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.8615348704203486s"></animate>
|
||||||
|
<animate attributeName="r" values="44;0;0" keyTimes="0;0.9694373671371078;1" dur="1s" repeatCount="indefinite" begin="-0.8615348704203486s"></animate>
|
||||||
|
</circle><circle cx="95" cy="113.78554320990165" r="4" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="113.78554320990165;-1.2652564238335904" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.21370544900580335s"></animate>
|
||||||
|
<animate attributeName="r" values="4;0;0" keyTimes="0;0.5334621383741172;1" dur="1s" repeatCount="indefinite" begin="-0.21370544900580335s"></animate>
|
||||||
|
</circle><circle cx="57" cy="136.06708935936715" r="34" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="136.06708935936715;-19.758990054858902" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.7755376997281404s"></animate>
|
||||||
|
<animate attributeName="r" values="34;0;0" keyTimes="0;0.9943252777203475;1" dur="1s" repeatCount="indefinite" begin="-0.7755376997281404s"></animate>
|
||||||
|
</circle><circle cx="72" cy="123.8422572942333" r="19" fill="#e15b64">
|
||||||
|
<animate attributeName="cy" values="123.8422572942333;-1.0000700639794928" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.9670461872772004s"></animate>
|
||||||
|
<animate attributeName="r" values="19;0;0" keyTimes="0;0.7801926792335607;1" dur="1s" repeatCount="indefinite" begin="-0.9670461872772004s"></animate>
|
||||||
|
</circle></g><g filter="url(#ldio-ekpf7uvh2aq-filter)"><circle cx="27" cy="136.75172282051147" r="17" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="136.75172282051147;-5.48853662281188" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.4403846891955857s"></animate>
|
||||||
|
<animate attributeName="r" values="17;0;0" keyTimes="0;0.7894732341719188;1" dur="1s" repeatCount="indefinite" begin="-0.4403846891955857s"></animate>
|
||||||
|
</circle><circle cx="34" cy="132.08290473906044" r="28" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="132.08290473906044;-16.339029232048958" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.7882134883361418s"></animate>
|
||||||
|
<animate attributeName="r" values="28;0;0" keyTimes="0;0.5035175026787356;1" dur="1s" repeatCount="indefinite" begin="-0.7882134883361418s"></animate>
|
||||||
|
</circle><circle cx="66" cy="127.45606892584162" r="23" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="127.45606892584162;-11.56763185745981" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.23537267190332678s"></animate>
|
||||||
|
<animate attributeName="r" values="23;0;0" keyTimes="0;0.7818578332234903;1" dur="1s" repeatCount="indefinite" begin="-0.23537267190332678s"></animate>
|
||||||
|
</circle><circle cx="29" cy="124.28337961013858" r="15" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="124.28337961013858;0.8461921465181206" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.30918442080681285s"></animate>
|
||||||
|
<animate attributeName="r" values="15;0;0" keyTimes="0;0.9741475377259025;1" dur="1s" repeatCount="indefinite" begin="-0.30918442080681285s"></animate>
|
||||||
|
</circle><circle cx="61" cy="147.91603256008383" r="31" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="147.91603256008383;-14.754981670358578" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.0033816756583812113s"></animate>
|
||||||
|
<animate attributeName="r" values="31;0;0" keyTimes="0;0.6463193577485268;1" dur="1s" repeatCount="indefinite" begin="-0.0033816756583812113s"></animate>
|
||||||
|
</circle><circle cx="25" cy="120.64483537229628" r="9" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="120.64483537229628;-7.193123212298179" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.6891092543031828s"></animate>
|
||||||
|
<animate attributeName="r" values="9;0;0" keyTimes="0;0.8637808572418493;1" dur="1s" repeatCount="indefinite" begin="-0.6891092543031828s"></animate>
|
||||||
|
</circle><circle cx="12" cy="121.18727231753691" r="4" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="121.18727231753691;15.883181236637633" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.24454851002004097s"></animate>
|
||||||
|
<animate attributeName="r" values="4;0;0" keyTimes="0;0.8215012014926046;1" dur="1s" repeatCount="indefinite" begin="-0.24454851002004097s"></animate>
|
||||||
|
</circle><circle cx="58" cy="136.64954415018815" r="19" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="136.64954415018815;-13.637628862199563" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.7672442553828805s"></animate>
|
||||||
|
<animate attributeName="r" values="19;0;0" keyTimes="0;0.7534841891330046;1" dur="1s" repeatCount="indefinite" begin="-0.7672442553828805s"></animate>
|
||||||
|
</circle><circle cx="69" cy="120.72538023727738" r="10" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="120.72538023727738;-5.651458016294906" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.6587915764098667s"></animate>
|
||||||
|
<animate attributeName="r" values="10;0;0" keyTimes="0;0.5977129956186352;1" dur="1s" repeatCount="indefinite" begin="-0.6587915764098667s"></animate>
|
||||||
|
</circle><circle cx="46" cy="122.63158963579554" r="20" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="122.63158963579554;-8.99196405151625" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.3698350873089088s"></animate>
|
||||||
|
<animate attributeName="r" values="20;0;0" keyTimes="0;0.5563937567659611;1" dur="1s" repeatCount="indefinite" begin="-0.3698350873089088s"></animate>
|
||||||
|
</circle><circle cx="7" cy="121.15700947168602" r="2" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="121.15700947168602;0.605011189845321" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.514133243834255s"></animate>
|
||||||
|
<animate attributeName="r" values="2;0;0" keyTimes="0;0.7510335363256938;1" dur="1s" repeatCount="indefinite" begin="-0.514133243834255s"></animate>
|
||||||
|
</circle><circle cx="19" cy="117.69071117783832" r="7" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="117.69071117783832;-2.4512162536532234" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.4163222368875168s"></animate>
|
||||||
|
<animate attributeName="r" values="7;0;0" keyTimes="0;0.9697983093212361;1" dur="1s" repeatCount="indefinite" begin="-0.4163222368875168s"></animate>
|
||||||
|
</circle><circle cx="34" cy="122.22172344680293" r="22" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="122.22172344680293;-14.875000336072436" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.8346904488502503s"></animate>
|
||||||
|
<animate attributeName="r" values="22;0;0" keyTimes="0;0.9284864899458874;1" dur="1s" repeatCount="indefinite" begin="-0.8346904488502503s"></animate>
|
||||||
|
</circle><circle cx="48" cy="118.34245443793573" r="12" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="118.34245443793573;6.1569446890589035" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.7372012265846987s"></animate>
|
||||||
|
<animate attributeName="r" values="12;0;0" keyTimes="0;0.9146509122657862;1" dur="1s" repeatCount="indefinite" begin="-0.7372012265846987s"></animate>
|
||||||
|
</circle><circle cx="38" cy="108.37260349538107" r="4" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="108.37260349538107;-3.9166184571860483" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.6955752887050161s"></animate>
|
||||||
|
<animate attributeName="r" values="4;0;0" keyTimes="0;0.9793871272170744;1" dur="1s" repeatCount="indefinite" begin="-0.6955752887050161s"></animate>
|
||||||
|
</circle><circle cx="50" cy="120.05611377372627" r="20" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="120.05611377372627;-19.59128463520709" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.8198691615147322s"></animate>
|
||||||
|
<animate attributeName="r" values="20;0;0" keyTimes="0;0.6017320767396992;1" dur="1s" repeatCount="indefinite" begin="-0.8198691615147322s"></animate>
|
||||||
|
</circle><circle cx="69" cy="133.11553485199934" r="21" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="133.11553485199934;-7.230262198733577" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.6502042470386947s"></animate>
|
||||||
|
<animate attributeName="r" values="21;0;0" keyTimes="0;0.9802383350633911;1" dur="1s" repeatCount="indefinite" begin="-0.6502042470386947s"></animate>
|
||||||
|
</circle><circle cx="60" cy="138.10205797824347" r="31" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="138.10205797824347;-21.149182634283513" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.8527464543018912s"></animate>
|
||||||
|
<animate attributeName="r" values="31;0;0" keyTimes="0;0.5593223005306734;1" dur="1s" repeatCount="indefinite" begin="-0.8527464543018912s"></animate>
|
||||||
|
</circle><circle cx="72" cy="121.45841247692351" r="16" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="121.45841247692351;-5.0851516529984195" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.4077549975882817s"></animate>
|
||||||
|
<animate attributeName="r" values="16;0;0" keyTimes="0;0.5763111141098053;1" dur="1s" repeatCount="indefinite" begin="-0.4077549975882817s"></animate>
|
||||||
|
</circle><circle cx="56" cy="118.12349945951125" r="10" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="118.12349945951125;-7.082779421666896" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.21747152423150562s"></animate>
|
||||||
|
<animate attributeName="r" values="10;0;0" keyTimes="0;0.6868094744383062;1" dur="1s" repeatCount="indefinite" begin="-0.21747152423150562s"></animate>
|
||||||
|
</circle><circle cx="77" cy="119.41951761904794" r="17" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="119.41951761904794;-9.114276721599797" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.48345793287516814s"></animate>
|
||||||
|
<animate attributeName="r" values="17;0;0" keyTimes="0;0.5135663211192452;1" dur="1s" repeatCount="indefinite" begin="-0.48345793287516814s"></animate>
|
||||||
|
</circle><circle cx="78" cy="125.60192795392818" r="11" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="125.60192795392818;-6.73068982191926" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.23667812050200931s"></animate>
|
||||||
|
<animate attributeName="r" values="11;0;0" keyTimes="0;0.9898092475181265;1" dur="1s" repeatCount="indefinite" begin="-0.23667812050200931s"></animate>
|
||||||
|
</circle><circle cx="51" cy="138.224179154187" r="24" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="138.224179154187;-8.55653503677315" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.5735700676741093s"></animate>
|
||||||
|
<animate attributeName="r" values="24;0;0" keyTimes="0;0.9566960986989479;1" dur="1s" repeatCount="indefinite" begin="-0.5735700676741093s"></animate>
|
||||||
|
</circle><circle cx="41" cy="131.14944604607328" r="21" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="131.14944604607328;-17.847508222350655" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.07696580759865079s"></animate>
|
||||||
|
<animate attributeName="r" values="21;0;0" keyTimes="0;0.6865631531399743;1" dur="1s" repeatCount="indefinite" begin="-0.07696580759865079s"></animate>
|
||||||
|
</circle><circle cx="49" cy="128.787268826053" r="17" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="128.787268826053;1.143259231969072" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.7890428937034474s"></animate>
|
||||||
|
<animate attributeName="r" values="17;0;0" keyTimes="0;0.5926722445396657;1" dur="1s" repeatCount="indefinite" begin="-0.7890428937034474s"></animate>
|
||||||
|
</circle><circle cx="17" cy="120.22416295842616" r="13" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="120.22416295842616;5.932998615440596" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.25642472915187764s"></animate>
|
||||||
|
<animate attributeName="r" values="13;0;0" keyTimes="0;0.5738477034101163;1" dur="1s" repeatCount="indefinite" begin="-0.25642472915187764s"></animate>
|
||||||
|
</circle><circle cx="73" cy="127.02191586426626" r="24" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="127.02191586426626;-19.34982189589097" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.9257599774553938s"></animate>
|
||||||
|
<animate attributeName="r" values="24;0;0" keyTimes="0;0.6060248140675957;1" dur="1s" repeatCount="indefinite" begin="-0.9257599774553938s"></animate>
|
||||||
|
</circle><circle cx="29" cy="122.37303701766326" r="22" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="122.37303701766326;-17.181874655618834" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.11979523584713825s"></animate>
|
||||||
|
<animate attributeName="r" values="22;0;0" keyTimes="0;0.5778892301319281;1" dur="1s" repeatCount="indefinite" begin="-0.11979523584713825s"></animate>
|
||||||
|
</circle><circle cx="30" cy="132.91741320840808" r="18" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="132.91741320840808;0.24294121648419775" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.6890213202603488s"></animate>
|
||||||
|
<animate attributeName="r" values="18;0;0" keyTimes="0;0.8587373770805918;1" dur="1s" repeatCount="indefinite" begin="-0.6890213202603488s"></animate>
|
||||||
|
</circle><circle cx="80" cy="116.72839679840811" r="14" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="116.72839679840811;4.82183707831593" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.08182847032405782s"></animate>
|
||||||
|
<animate attributeName="r" values="14;0;0" keyTimes="0;0.6809633164153448;1" dur="1s" repeatCount="indefinite" begin="-0.08182847032405782s"></animate>
|
||||||
|
</circle><circle cx="31" cy="125.20247260666616" r="13" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="125.20247260666616;2.008326413572634" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.8369662812852767s"></animate>
|
||||||
|
<animate attributeName="r" values="13;0;0" keyTimes="0;0.5845779670186058;1" dur="1s" repeatCount="indefinite" begin="-0.8369662812852767s"></animate>
|
||||||
|
</circle><circle cx="60" cy="125.0794549947879" r="16" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="125.0794549947879;0.7338248372355807" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.8948237868324189s"></animate>
|
||||||
|
<animate attributeName="r" values="16;0;0" keyTimes="0;0.9120596722058173;1" dur="1s" repeatCount="indefinite" begin="-0.8948237868324189s"></animate>
|
||||||
|
</circle><circle cx="25" cy="126.90612837175388" r="8" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="126.90612837175388;4.0472618983783715" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.39581604043317986s"></animate>
|
||||||
|
<animate attributeName="r" values="8;0;0" keyTimes="0;0.8074064845720312;1" dur="1s" repeatCount="indefinite" begin="-0.39581604043317986s"></animate>
|
||||||
|
</circle><circle cx="37" cy="131.42028038990128" r="25" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="131.42028038990128;-22.403977227715075" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.04301794169924622s"></animate>
|
||||||
|
<animate attributeName="r" values="25;0;0" keyTimes="0;0.524891315929541;1" dur="1s" repeatCount="indefinite" begin="-0.04301794169924622s"></animate>
|
||||||
|
</circle><circle cx="41" cy="149.05000141391616" r="31" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="149.05000141391616;-19.10046896539864" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.7213401886638007s"></animate>
|
||||||
|
<animate attributeName="r" values="31;0;0" keyTimes="0;0.6890520162965066;1" dur="1s" repeatCount="indefinite" begin="-0.7213401886638007s"></animate>
|
||||||
|
</circle><circle cx="36" cy="138.58798523568342" r="27" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="138.58798523568342;-15.572058043829461" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.40556498158772736s"></animate>
|
||||||
|
<animate attributeName="r" values="27;0;0" keyTimes="0;0.8506348676044777;1" dur="1s" repeatCount="indefinite" begin="-0.40556498158772736s"></animate>
|
||||||
|
</circle><circle cx="78" cy="137.9707233461312" r="20" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="137.9707233461312;-3.6945948738885512" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.8880631706610672s"></animate>
|
||||||
|
<animate attributeName="r" values="20;0;0" keyTimes="0;0.9304971995517395;1" dur="1s" repeatCount="indefinite" begin="-0.8880631706610672s"></animate>
|
||||||
|
</circle><circle cx="79" cy="134.71673525431498" r="18" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="134.71673525431498;-10.261412982322742" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.2848983056723242s"></animate>
|
||||||
|
<animate attributeName="r" values="18;0;0" keyTimes="0;0.7526875949615255;1" dur="1s" repeatCount="indefinite" begin="-0.2848983056723242s"></animate>
|
||||||
|
</circle><circle cx="82" cy="111.49802891873294" r="5" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="111.49802891873294;12.140748225430922" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.40945179236345397s"></animate>
|
||||||
|
<animate attributeName="r" values="5;0;0" keyTimes="0;0.703997116139137;1" dur="1s" repeatCount="indefinite" begin="-0.40945179236345397s"></animate>
|
||||||
|
</circle><circle cx="68" cy="140.96466884045572" r="22" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="140.96466884045572;-4.079142984351218" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.40439383112303107s"></animate>
|
||||||
|
<animate attributeName="r" values="22;0;0" keyTimes="0;0.5493704483007363;1" dur="1s" repeatCount="indefinite" begin="-0.40439383112303107s"></animate>
|
||||||
|
</circle><circle cx="41" cy="116.24169615516264" r="16" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="116.24169615516264;-13.644720096932094" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.22449184929827926s"></animate>
|
||||||
|
<animate attributeName="r" values="16;0;0" keyTimes="0;0.6587866247823291;1" dur="1s" repeatCount="indefinite" begin="-0.22449184929827926s"></animate>
|
||||||
|
</circle><circle cx="20" cy="124.66929057881916" r="15" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="124.66929057881916;2.5505611618972814" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.017560126563357925s"></animate>
|
||||||
|
<animate attributeName="r" values="15;0;0" keyTimes="0;0.6128429739262174;1" dur="1s" repeatCount="indefinite" begin="-0.017560126563357925s"></animate>
|
||||||
|
</circle><circle cx="63" cy="126.5115900704738" r="26" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="126.5115900704738;-20.921901271813873" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.5285257319858678s"></animate>
|
||||||
|
<animate attributeName="r" values="26;0;0" keyTimes="0;0.9007468611639214;1" dur="1s" repeatCount="indefinite" begin="-0.5285257319858678s"></animate>
|
||||||
|
</circle><circle cx="90" cy="111.61440083571019" r="6" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="111.61440083571019;11.61930520437923" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.8167452043810126s"></animate>
|
||||||
|
<animate attributeName="r" values="6;0;0" keyTimes="0;0.9810779841180124;1" dur="1s" repeatCount="indefinite" begin="-0.8167452043810126s"></animate>
|
||||||
|
</circle><circle cx="78" cy="122.50775060552778" r="20" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="122.50775060552778;-4.59807973956865" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.11755589684814727s"></animate>
|
||||||
|
<animate attributeName="r" values="20;0;0" keyTimes="0;0.6705237343698631;1" dur="1s" repeatCount="indefinite" begin="-0.11755589684814727s"></animate>
|
||||||
|
</circle><circle cx="31" cy="127.90703241028092" r="9" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="127.90703241028092;0.829718008041219" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.5851309189776632s"></animate>
|
||||||
|
<animate attributeName="r" values="9;0;0" keyTimes="0;0.6889560303799027;1" dur="1s" repeatCount="indefinite" begin="-0.5851309189776632s"></animate>
|
||||||
|
</circle><circle cx="65" cy="117.43435709704966" r="4" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="117.43435709704966;15.28596080488979" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.8492165554334472s"></animate>
|
||||||
|
<animate attributeName="r" values="4;0;0" keyTimes="0;0.5287459347086204;1" dur="1s" repeatCount="indefinite" begin="-0.8492165554334472s"></animate>
|
||||||
|
</circle><circle cx="89" cy="122.93132420091489" r="3" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="122.93132420091489;5.980513428860888" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.06884209677796871s"></animate>
|
||||||
|
<animate attributeName="r" values="3;0;0" keyTimes="0;0.5868616814040618;1" dur="1s" repeatCount="indefinite" begin="-0.06884209677796871s"></animate>
|
||||||
|
</circle><circle cx="68" cy="129.1441504106191" r="26" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="129.1441504106191;-22.781245889673905" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.26191875209122073s"></animate>
|
||||||
|
<animate attributeName="r" values="26;0;0" keyTimes="0;0.6200648439404779;1" dur="1s" repeatCount="indefinite" begin="-0.26191875209122073s"></animate>
|
||||||
|
</circle><circle cx="22" cy="130.63745849588264" r="20" fill="#f47e60">
|
||||||
|
<animate attributeName="cy" values="130.63745849588264;-10.695329441338862" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.6192951915425052s"></animate>
|
||||||
|
<animate attributeName="r" values="20;0;0" keyTimes="0;0.6969346125529845;1" dur="1s" repeatCount="indefinite" begin="-0.6192951915425052s"></animate>
|
||||||
|
</circle></g><g filter="url(#ldio-ekpf7uvh2aq-filter)"><circle cx="57" cy="123.68953191890479" r="12" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="123.68953191890479;4.854991577389438" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.9097135632734302s"></animate>
|
||||||
|
<animate attributeName="r" values="12;0;0" keyTimes="0;0.9463910575266388;1" dur="1s" repeatCount="indefinite" begin="-0.9097135632734302s"></animate>
|
||||||
|
</circle><circle cx="24" cy="124.54645838615471" r="12" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="124.54645838615471;-11.813810322332547" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.007050694143823311s"></animate>
|
||||||
|
<animate attributeName="r" values="12;0;0" keyTimes="0;0.7078891674964196;1" dur="1s" repeatCount="indefinite" begin="-0.007050694143823311s"></animate>
|
||||||
|
</circle><circle cx="54" cy="110.08044357995595" r="3" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="110.08044357995595;13.402947007936334" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.994432759852213s"></animate>
|
||||||
|
<animate attributeName="r" values="3;0;0" keyTimes="0;0.8430605754104277;1" dur="1s" repeatCount="indefinite" begin="-0.994432759852213s"></animate>
|
||||||
|
</circle><circle cx="49" cy="127.80477114160061" r="16" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="127.80477114160061;2.7658256519770603" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.07188593356616135s"></animate>
|
||||||
|
<animate attributeName="r" values="16;0;0" keyTimes="0;0.6049768163612267;1" dur="1s" repeatCount="indefinite" begin="-0.07188593356616135s"></animate>
|
||||||
|
</circle><circle cx="52" cy="112.09746694041411" r="10" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="112.09746694041411;-2.8104821907767574" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.4132445270517203s"></animate>
|
||||||
|
<animate attributeName="r" values="10;0;0" keyTimes="0;0.7843188648425736;1" dur="1s" repeatCount="indefinite" begin="-0.4132445270517203s"></animate>
|
||||||
|
</circle><circle cx="68" cy="119.76797510227266" r="15" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="119.76797510227266;-2.3187957684067317" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.6317748306797277s"></animate>
|
||||||
|
<animate attributeName="r" values="15;0;0" keyTimes="0;0.8464277838946668;1" dur="1s" repeatCount="indefinite" begin="-0.6317748306797277s"></animate>
|
||||||
|
</circle><circle cx="17" cy="121.7997527406382" r="5" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="121.7997527406382;13.556957891026624" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.9136732084136533s"></animate>
|
||||||
|
<animate attributeName="r" values="5;0;0" keyTimes="0;0.5349721785314134;1" dur="1s" repeatCount="indefinite" begin="-0.9136732084136533s"></animate>
|
||||||
|
</circle><circle cx="59" cy="116.30296558149124" r="4" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="116.30296558149124;-1.0433564145924477" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.08891813207741484s"></animate>
|
||||||
|
<animate attributeName="r" values="4;0;0" keyTimes="0;0.6574981312374213;1" dur="1s" repeatCount="indefinite" begin="-0.08891813207741484s"></animate>
|
||||||
|
</circle><circle cx="88" cy="113.1583378513422" r="12" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="113.1583378513422;1.456869512308952" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.14992898603700067s"></animate>
|
||||||
|
<animate attributeName="r" values="12;0;0" keyTimes="0;0.9565108058771807;1" dur="1s" repeatCount="indefinite" begin="-0.14992898603700067s"></animate>
|
||||||
|
</circle><circle cx="84" cy="112.41279273844411" r="10" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="112.41279273844411;1.6491176590177243" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.5833010262862421s"></animate>
|
||||||
|
<animate attributeName="r" values="10;0;0" keyTimes="0;0.5438806242531744;1" dur="1s" repeatCount="indefinite" begin="-0.5833010262862421s"></animate>
|
||||||
|
</circle><circle cx="87" cy="120.26530337145327" r="5" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="120.26530337145327;9.388664939149207" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.05018189342538548s"></animate>
|
||||||
|
<animate attributeName="r" values="5;0;0" keyTimes="0;0.637897648645736;1" dur="1s" repeatCount="indefinite" begin="-0.05018189342538548s"></animate>
|
||||||
|
</circle><circle cx="24" cy="123.99448894779877" r="9" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="123.99448894779877;2.3750067806866078" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.8890495329191316s"></animate>
|
||||||
|
<animate attributeName="r" values="9;0;0" keyTimes="0;0.663064102718458;1" dur="1s" repeatCount="indefinite" begin="-0.8890495329191316s"></animate>
|
||||||
|
</circle><circle cx="73" cy="120.00019528994846" r="12" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="120.00019528994846;-9.503507375076166" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.6351313241419324s"></animate>
|
||||||
|
<animate attributeName="r" values="12;0;0" keyTimes="0;0.9354194941922095;1" dur="1s" repeatCount="indefinite" begin="-0.6351313241419324s"></animate>
|
||||||
|
</circle><circle cx="74" cy="113.88820186698781" r="4" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="113.88820186698781;10.570535200732685" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.7132998998028989s"></animate>
|
||||||
|
<animate attributeName="r" values="4;0;0" keyTimes="0;0.91895021859856;1" dur="1s" repeatCount="indefinite" begin="-0.7132998998028989s"></animate>
|
||||||
|
</circle><circle cx="68" cy="129.5841522641359" r="12" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="129.5841522641359;3.894919008898638" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.29330391921510546s"></animate>
|
||||||
|
<animate attributeName="r" values="12;0;0" keyTimes="0;0.9096568793749455;1" dur="1s" repeatCount="indefinite" begin="-0.29330391921510546s"></animate>
|
||||||
|
</circle><circle cx="53" cy="119.31720358172306" r="9" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="119.31720358172306;9.73624644875764" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.9958245939061628s"></animate>
|
||||||
|
<animate attributeName="r" values="9;0;0" keyTimes="0;0.8571965277158554;1" dur="1s" repeatCount="indefinite" begin="-0.9958245939061628s"></animate>
|
||||||
|
</circle><circle cx="76" cy="134.80739606982607" r="17" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="134.80739606982607;0.3932385595869441" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.8607153243461125s"></animate>
|
||||||
|
<animate attributeName="r" values="17;0;0" keyTimes="0;0.8654455107706405;1" dur="1s" repeatCount="indefinite" begin="-0.8607153243461125s"></animate>
|
||||||
|
</circle><circle cx="75" cy="122.61568996754474" r="7" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="122.61568996754474;10.652526875734779" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.959721298983397s"></animate>
|
||||||
|
<animate attributeName="r" values="7;0;0" keyTimes="0;0.6271803990132601;1" dur="1s" repeatCount="indefinite" begin="-0.959721298983397s"></animate>
|
||||||
|
</circle><circle cx="87" cy="115.0788054109218" r="12" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="115.0788054109218;-8.15567938666852" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.0690058777440068s"></animate>
|
||||||
|
<animate attributeName="r" values="12;0;0" keyTimes="0;0.6627211388649489;1" dur="1s" repeatCount="indefinite" begin="-0.0690058777440068s"></animate>
|
||||||
|
</circle><circle cx="21" cy="118.08738171978098" r="9" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="118.08738171978098;-4.9475469075625504" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.7078831683260647s"></animate>
|
||||||
|
<animate attributeName="r" values="9;0;0" keyTimes="0;0.9501044367725069;1" dur="1s" repeatCount="indefinite" begin="-0.7078831683260647s"></animate>
|
||||||
|
</circle><circle cx="24" cy="128.09150085659442" r="9" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="128.09150085659442;2.7320353690265122" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.521121701341132s"></animate>
|
||||||
|
<animate attributeName="r" values="9;0;0" keyTimes="0;0.7357531229285373;1" dur="1s" repeatCount="indefinite" begin="-0.521121701341132s"></animate>
|
||||||
|
</circle><circle cx="26" cy="127.49368345428452" r="15" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="127.49368345428452;-10.361246269666196" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.9420307783603239s"></animate>
|
||||||
|
<animate attributeName="r" values="15;0;0" keyTimes="0;0.7467409545014994;1" dur="1s" repeatCount="indefinite" begin="-0.9420307783603239s"></animate>
|
||||||
|
</circle><circle cx="39" cy="114.20744515306558" r="6" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="114.20744515306558;5.606516894440285" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.49268347147689695s"></animate>
|
||||||
|
<animate attributeName="r" values="6;0;0" keyTimes="0;0.5874854761603912;1" dur="1s" repeatCount="indefinite" begin="-0.49268347147689695s"></animate>
|
||||||
|
</circle><circle cx="61" cy="123.10463246179438" r="11" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="123.10463246179438;-5.189366828773049" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.21359109324800063s"></animate>
|
||||||
|
<animate attributeName="r" values="11;0;0" keyTimes="0;0.6970744691674484;1" dur="1s" repeatCount="indefinite" begin="-0.21359109324800063s"></animate>
|
||||||
|
</circle><circle cx="37" cy="115.40335155247101" r="10" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="115.40335155247101;3.4285850566842946" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.5344545499798534s"></animate>
|
||||||
|
<animate attributeName="r" values="10;0;0" keyTimes="0;0.9983685792824288;1" dur="1s" repeatCount="indefinite" begin="-0.5344545499798534s"></animate>
|
||||||
|
</circle><circle cx="22" cy="124.59228223795324" r="7" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="124.59228223795324;-3.5076355130396912" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.8102510016775601s"></animate>
|
||||||
|
<animate attributeName="r" values="7;0;0" keyTimes="0;0.6369981578428732;1" dur="1s" repeatCount="indefinite" begin="-0.8102510016775601s"></animate>
|
||||||
|
</circle><circle cx="34" cy="111.69621652751701" r="5" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="111.69621652751701;13.965538669421832" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.3819120829819431s"></animate>
|
||||||
|
<animate attributeName="r" values="5;0;0" keyTimes="0;0.9240036927970401;1" dur="1s" repeatCount="indefinite" begin="-0.3819120829819431s"></animate>
|
||||||
|
</circle><circle cx="61" cy="121.99207528226256" r="6" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="121.99207528226256;-1.1884130816048284" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.351012424136126s"></animate>
|
||||||
|
<animate attributeName="r" values="6;0;0" keyTimes="0;0.9527855705617168;1" dur="1s" repeatCount="indefinite" begin="-0.351012424136126s"></animate>
|
||||||
|
</circle><circle cx="32" cy="115.36386365084275" r="13" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="115.36386365084275;-7.635796261623495" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.22026693987990997s"></animate>
|
||||||
|
<animate attributeName="r" values="13;0;0" keyTimes="0;0.6822821982216503;1" dur="1s" repeatCount="indefinite" begin="-0.22026693987990997s"></animate>
|
||||||
|
</circle><circle cx="38" cy="123.93260454500944" r="10" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="123.93260454500944;-9.019646946232784" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.5897767052001425s"></animate>
|
||||||
|
<animate attributeName="r" values="10;0;0" keyTimes="0;0.747643174639248;1" dur="1s" repeatCount="indefinite" begin="-0.5897767052001425s"></animate>
|
||||||
|
</circle><circle cx="91" cy="111.20360670124936" r="4" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="111.20360670124936;-2.7511383786778185" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.5936715943771124s"></animate>
|
||||||
|
<animate attributeName="r" values="4;0;0" keyTimes="0;0.5292863982274825;1" dur="1s" repeatCount="indefinite" begin="-0.5936715943771124s"></animate>
|
||||||
|
</circle><circle cx="93" cy="109.08688866758263" r="6" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="109.08688866758263;13.986514639855155" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.20182465253134418s"></animate>
|
||||||
|
<animate attributeName="r" values="6;0;0" keyTimes="0;0.9578727930035874;1" dur="1s" repeatCount="indefinite" begin="-0.20182465253134418s"></animate>
|
||||||
|
</circle><circle cx="90" cy="115.44258946143852" r="3" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="115.44258946143852;7.971557449807172" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.8138344996352406s"></animate>
|
||||||
|
<animate attributeName="r" values="3;0;0" keyTimes="0;0.822677504532275;1" dur="1s" repeatCount="indefinite" begin="-0.8138344996352406s"></animate>
|
||||||
|
</circle><circle cx="24" cy="130.98782632438636" r="15" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="130.98782632438636;-11.868426017755008" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.8574009914089539s"></animate>
|
||||||
|
<animate attributeName="r" values="15;0;0" keyTimes="0;0.8610318085552064;1" dur="1s" repeatCount="indefinite" begin="-0.8574009914089539s"></animate>
|
||||||
|
</circle><circle cx="49" cy="122.24309971563434" r="14" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="122.24309971563434;3.5685994935617273" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.4267384904796552s"></animate>
|
||||||
|
<animate attributeName="r" values="14;0;0" keyTimes="0;0.5503829186981541;1" dur="1s" repeatCount="indefinite" begin="-0.4267384904796552s"></animate>
|
||||||
|
</circle><circle cx="18" cy="117.38217971971676" r="9" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="117.38217971971676;6.631006164776416" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.6828218424869835s"></animate>
|
||||||
|
<animate attributeName="r" values="9;0;0" keyTimes="0;0.6808177575913787;1" dur="1s" repeatCount="indefinite" begin="-0.6828218424869835s"></animate>
|
||||||
|
</circle><circle cx="78" cy="124.28678852303256" r="15" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="124.28678852303256;1.3740946843405304" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.4161035078940827s"></animate>
|
||||||
|
<animate attributeName="r" values="15;0;0" keyTimes="0;0.6388001474427218;1" dur="1s" repeatCount="indefinite" begin="-0.4161035078940827s"></animate>
|
||||||
|
</circle><circle cx="44" cy="106.6189204965897" r="3" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="106.6189204965897;16.750815514807034" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.0510803765953457s"></animate>
|
||||||
|
<animate attributeName="r" values="3;0;0" keyTimes="0;0.7907276882734477;1" dur="1s" repeatCount="indefinite" begin="-0.0510803765953457s"></animate>
|
||||||
|
</circle><circle cx="41" cy="119.64799537397232" r="5" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="119.64799537397232;6.398667601394809" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.4280945050279754s"></animate>
|
||||||
|
<animate attributeName="r" values="5;0;0" keyTimes="0;0.5751942250658201;1" dur="1s" repeatCount="indefinite" begin="-0.4280945050279754s"></animate>
|
||||||
|
</circle><circle cx="19" cy="120.0916729802829" r="10" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="120.0916729802829;-9.513704965243033" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.043405970368113445s"></animate>
|
||||||
|
<animate attributeName="r" values="10;0;0" keyTimes="0;0.5435267537060107;1" dur="1s" repeatCount="indefinite" begin="-0.043405970368113445s"></animate>
|
||||||
|
</circle><circle cx="61" cy="123.62714133794762" r="5" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="123.62714133794762;2.362315551662477" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.5256540407430482s"></animate>
|
||||||
|
<animate attributeName="r" values="5;0;0" keyTimes="0;0.9222037100732456;1" dur="1s" repeatCount="indefinite" begin="-0.5256540407430482s"></animate>
|
||||||
|
</circle><circle cx="64" cy="115.25525614926073" r="13" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="115.25525614926073;-10.304511881341815" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.6633519944592159s"></animate>
|
||||||
|
<animate attributeName="r" values="13;0;0" keyTimes="0;0.5401283508859178;1" dur="1s" repeatCount="indefinite" begin="-0.6633519944592159s"></animate>
|
||||||
|
</circle><circle cx="12" cy="129.13660549492693" r="11" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="129.13660549492693;-7.965594883525825" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.9929282227674491s"></animate>
|
||||||
|
<animate attributeName="r" values="11;0;0" keyTimes="0;0.9536114994321867;1" dur="1s" repeatCount="indefinite" begin="-0.9929282227674491s"></animate>
|
||||||
|
</circle><circle cx="39" cy="106.95504126040025" r="2" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="106.95504126040025;5.834416891524681" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.22005892301327157s"></animate>
|
||||||
|
<animate attributeName="r" values="2;0;0" keyTimes="0;0.6089960643653531;1" dur="1s" repeatCount="indefinite" begin="-0.22005892301327157s"></animate>
|
||||||
|
</circle><circle cx="30" cy="112.12744151244388" r="8" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="112.12744151244388;-4.465606537168944" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.24710322548242414s"></animate>
|
||||||
|
<animate attributeName="r" values="8;0;0" keyTimes="0;0.7479705418636007;1" dur="1s" repeatCount="indefinite" begin="-0.24710322548242414s"></animate>
|
||||||
|
</circle><circle cx="67" cy="124.83294711941956" r="16" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="124.83294711941956;-7.6291463245052284" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.614066023590482s"></animate>
|
||||||
|
<animate attributeName="r" values="16;0;0" keyTimes="0;0.7584434636145084;1" dur="1s" repeatCount="indefinite" begin="-0.614066023590482s"></animate>
|
||||||
|
</circle><circle cx="22" cy="119.36463088979876" r="4" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="119.36463088979876;12.12664234343379" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.527385385953813s"></animate>
|
||||||
|
<animate attributeName="r" values="4;0;0" keyTimes="0;0.5661680148267347;1" dur="1s" repeatCount="indefinite" begin="-0.527385385953813s"></animate>
|
||||||
|
</circle><circle cx="12" cy="122.52124979151506" r="7" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="122.52124979151506;3.7506712743784085" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.37225883133903837s"></animate>
|
||||||
|
<animate attributeName="r" values="7;0;0" keyTimes="0;0.9003327357718601;1" dur="1s" repeatCount="indefinite" begin="-0.37225883133903837s"></animate>
|
||||||
|
</circle><circle cx="69" cy="130.5210986475815" r="14" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="130.5210986475815;-0.30973651460238827" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.6062299863585278s"></animate>
|
||||||
|
<animate attributeName="r" values="14;0;0" keyTimes="0;0.9220180768904789;1" dur="1s" repeatCount="indefinite" begin="-0.6062299863585278s"></animate>
|
||||||
|
</circle><circle cx="20" cy="114.80243604193255" r="9" fill="#f8b26a">
|
||||||
|
<animate attributeName="cy" values="114.80243604193255;7.19374553530416" keyTimes="0;1" dur="1s" repeatCount="indefinite" begin="-0.6866227460985781s"></animate>
|
||||||
|
<animate attributeName="r" values="9;0;0" keyTimes="0;0.6690048284116141;1" dur="1s" repeatCount="indefinite" begin="-0.6866227460985781s"></animate>
|
||||||
|
</circle></g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 58 KiB |
BIN
assets/test.pdf
Normal file
BIN
assets/test.pdf
Normal file
Binary file not shown.
BIN
assets/web_demo.gif
Normal file
BIN
assets/web_demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 MiB |
@@ -1,10 +1,6 @@
|
|||||||
transformers
|
transformers
|
||||||
datasets
|
datasets
|
||||||
evaluate
|
evaluate
|
||||||
streamlit
|
|
||||||
|
|
||||||
gradio
|
|
||||||
|
|
||||||
opencv-python
|
opencv-python
|
||||||
ray[serve]
|
ray[serve]
|
||||||
accelerate
|
accelerate
|
||||||
@@ -12,4 +8,8 @@ tensorboardX
|
|||||||
nltk
|
nltk
|
||||||
python-multipart
|
python-multipart
|
||||||
|
|
||||||
augraphy
|
augraphy
|
||||||
|
onnxruntime
|
||||||
|
|
||||||
|
streamlit==1.30
|
||||||
|
streamlit-paste-button
|
||||||
|
|||||||
197
src/infer_det.py
Normal file
197
src/infer_det.py
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
import os
|
||||||
|
import yaml
|
||||||
|
import argparse
|
||||||
|
import numpy as np
|
||||||
|
import glob
|
||||||
|
from onnxruntime import InferenceSession
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
from models.det_model.preprocess import Compose
|
||||||
|
import cv2
|
||||||
|
|
||||||
|
# 注意:文件名要标准,最好都用下划线
|
||||||
|
|
||||||
|
# Global dictionary
|
||||||
|
SUPPORT_MODELS = {
|
||||||
|
'YOLO', 'PPYOLOE', 'RCNN', 'SSD', 'Face', 'FCOS', 'SOLOv2', 'TTFNet',
|
||||||
|
'S2ANet', 'JDE', 'FairMOT', 'DeepSORT', 'GFL', 'PicoDet', 'CenterNet',
|
||||||
|
'TOOD', 'RetinaNet', 'StrongBaseline', 'STGCN', 'YOLOX', 'HRNet',
|
||||||
|
'DETR'
|
||||||
|
}
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--infer_cfg", type=str, help="infer_cfg.yml",
|
||||||
|
default="./models/det_model/model/infer_cfg.yml"
|
||||||
|
)
|
||||||
|
parser.add_argument('--onnx_file', type=str, help="onnx model file path",
|
||||||
|
default="./models/det_model/model/rtdetr_r50vd_6x_coco.onnx"
|
||||||
|
)
|
||||||
|
parser.add_argument("--image_dir", type=str)
|
||||||
|
parser.add_argument("--image_file", type=str, default='/data/ljm/TexTeller/src/Tr00_0001015-page02.jpg')
|
||||||
|
parser.add_argument("--imgsave_dir", type=str,
|
||||||
|
default="."
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_test_images(infer_dir, infer_img):
|
||||||
|
"""
|
||||||
|
Get image path list in TEST mode
|
||||||
|
"""
|
||||||
|
assert infer_img is not None or infer_dir is not None, \
|
||||||
|
"--image_file or --image_dir should be set"
|
||||||
|
assert infer_img is None or os.path.isfile(infer_img), \
|
||||||
|
"{} is not a file".format(infer_img)
|
||||||
|
assert infer_dir is None or os.path.isdir(infer_dir), \
|
||||||
|
"{} is not a directory".format(infer_dir)
|
||||||
|
|
||||||
|
# infer_img has a higher priority
|
||||||
|
if infer_img and os.path.isfile(infer_img):
|
||||||
|
return [infer_img]
|
||||||
|
|
||||||
|
images = set()
|
||||||
|
infer_dir = os.path.abspath(infer_dir)
|
||||||
|
assert os.path.isdir(infer_dir), \
|
||||||
|
"infer_dir {} is not a directory".format(infer_dir)
|
||||||
|
exts = ['jpg', 'jpeg', 'png', 'bmp']
|
||||||
|
exts += [ext.upper() for ext in exts]
|
||||||
|
for ext in exts:
|
||||||
|
images.update(glob.glob('{}/*.{}'.format(infer_dir, ext)))
|
||||||
|
images = list(images)
|
||||||
|
|
||||||
|
assert len(images) > 0, "no image found in {}".format(infer_dir)
|
||||||
|
print("Found {} inference images in total.".format(len(images)))
|
||||||
|
|
||||||
|
return images
|
||||||
|
|
||||||
|
|
||||||
|
class PredictConfig(object):
|
||||||
|
"""set config of preprocess, postprocess and visualize
|
||||||
|
Args:
|
||||||
|
infer_config (str): path of infer_cfg.yml
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, infer_config):
|
||||||
|
# parsing Yaml config for Preprocess
|
||||||
|
with open(infer_config) as f:
|
||||||
|
yml_conf = yaml.safe_load(f)
|
||||||
|
self.check_model(yml_conf)
|
||||||
|
self.arch = yml_conf['arch']
|
||||||
|
self.preprocess_infos = yml_conf['Preprocess']
|
||||||
|
self.min_subgraph_size = yml_conf['min_subgraph_size']
|
||||||
|
self.label_list = yml_conf['label_list']
|
||||||
|
self.use_dynamic_shape = yml_conf['use_dynamic_shape']
|
||||||
|
self.draw_threshold = yml_conf.get("draw_threshold", 0.5)
|
||||||
|
self.mask = yml_conf.get("mask", False)
|
||||||
|
self.tracker = yml_conf.get("tracker", None)
|
||||||
|
self.nms = yml_conf.get("NMS", None)
|
||||||
|
self.fpn_stride = yml_conf.get("fpn_stride", None)
|
||||||
|
|
||||||
|
# 预定义颜色池
|
||||||
|
color_pool = [(0, 255, 0), (255, 0, 0), (0, 0, 255), (255, 255, 0), (0, 255, 255)]
|
||||||
|
# 根据label_list动态生成颜色映射
|
||||||
|
self.colors = {label: color_pool[i % len(color_pool)] for i, label in enumerate(self.label_list)}
|
||||||
|
|
||||||
|
if self.arch == 'RCNN' and yml_conf.get('export_onnx', False):
|
||||||
|
print(
|
||||||
|
'The RCNN export model is used for ONNX and it only supports batch_size = 1'
|
||||||
|
)
|
||||||
|
self.print_config()
|
||||||
|
|
||||||
|
def check_model(self, yml_conf):
|
||||||
|
"""
|
||||||
|
Raises:
|
||||||
|
ValueError: loaded model not in supported model type
|
||||||
|
"""
|
||||||
|
for support_model in SUPPORT_MODELS:
|
||||||
|
if support_model in yml_conf['arch']:
|
||||||
|
return True
|
||||||
|
raise ValueError("Unsupported arch: {}, expect {}".format(yml_conf[
|
||||||
|
'arch'], SUPPORT_MODELS))
|
||||||
|
|
||||||
|
def print_config(self):
|
||||||
|
print('----------- Model Configuration -----------')
|
||||||
|
print('%s: %s' % ('Model Arch', self.arch))
|
||||||
|
print('%s: ' % ('Transform Order'))
|
||||||
|
for op_info in self.preprocess_infos:
|
||||||
|
print('--%s: %s' % ('transform op', op_info['type']))
|
||||||
|
print('--------------------------------------------')
|
||||||
|
|
||||||
|
|
||||||
|
def draw_bbox(image, outputs, infer_config):
|
||||||
|
for output in outputs:
|
||||||
|
cls_id, score, xmin, ymin, xmax, ymax = output
|
||||||
|
if score > infer_config.draw_threshold:
|
||||||
|
# 获取类别名
|
||||||
|
label = infer_config.label_list[int(cls_id)]
|
||||||
|
# 根据类别名获取颜色
|
||||||
|
color = infer_config.colors[label]
|
||||||
|
cv2.rectangle(image, (int(xmin), int(ymin)), (int(xmax), int(ymax)), color, 2)
|
||||||
|
cv2.putText(image, "{}: {:.2f}".format(label, score),
|
||||||
|
(int(xmin), int(ymin - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
|
||||||
|
return image
|
||||||
|
|
||||||
|
|
||||||
|
def predict_image(infer_config, predictor, img_list):
|
||||||
|
# load preprocess transforms
|
||||||
|
transforms = Compose(infer_config.preprocess_infos)
|
||||||
|
errImgList = []
|
||||||
|
|
||||||
|
# Check and create subimg_save_dir if not exist
|
||||||
|
subimg_save_dir = os.path.join(FLAGS.imgsave_dir, 'subimages')
|
||||||
|
os.makedirs(subimg_save_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# predict image
|
||||||
|
for img_path in tqdm(img_list):
|
||||||
|
img = cv2.imread(img_path)
|
||||||
|
if img is None:
|
||||||
|
print(f"Warning: Could not read image {img_path}. Skipping...")
|
||||||
|
errImgList.append(img_path)
|
||||||
|
continue
|
||||||
|
|
||||||
|
inputs = transforms(img_path)
|
||||||
|
inputs_name = [var.name for var in predictor.get_inputs()]
|
||||||
|
inputs = {k: inputs[k][None, ] for k in inputs_name}
|
||||||
|
|
||||||
|
outputs = predictor.run(output_names=None, input_feed=inputs)
|
||||||
|
|
||||||
|
print("ONNXRuntime predict: ")
|
||||||
|
if infer_config.arch in ["HRNet"]:
|
||||||
|
print(np.array(outputs[0]))
|
||||||
|
else:
|
||||||
|
bboxes = np.array(outputs[0])
|
||||||
|
for bbox in bboxes:
|
||||||
|
if bbox[0] > -1 and bbox[1] > infer_config.draw_threshold:
|
||||||
|
print(f"{int(bbox[0])} {bbox[1]} "
|
||||||
|
f"{bbox[2]} {bbox[3]} {bbox[4]} {bbox[5]}")
|
||||||
|
|
||||||
|
# Save the subimages (crop from the original image)
|
||||||
|
subimg_counter = 1
|
||||||
|
for output in np.array(outputs[0]):
|
||||||
|
cls_id, score, xmin, ymin, xmax, ymax = output
|
||||||
|
if score > infer_config.draw_threshold:
|
||||||
|
label = infer_config.label_list[int(cls_id)]
|
||||||
|
subimg = img[int(ymin):int(ymax), int(xmin):int(xmax)]
|
||||||
|
subimg_filename = f"{os.path.splitext(os.path.basename(img_path))[0]}_{label}_{xmin:.2f}_{ymin:.2f}_{xmax:.2f}_{ymax:.2f}.jpg"
|
||||||
|
subimg_path = os.path.join(subimg_save_dir, subimg_filename)
|
||||||
|
cv2.imwrite(subimg_path, subimg)
|
||||||
|
subimg_counter += 1
|
||||||
|
|
||||||
|
# Draw bounding boxes and save the image with bounding boxes
|
||||||
|
img_with_bbox = draw_bbox(img, np.array(outputs[0]), infer_config)
|
||||||
|
output_dir = FLAGS.imgsave_dir
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
output_file = os.path.join(output_dir, "output_" + os.path.basename(img_path))
|
||||||
|
cv2.imwrite(output_file, img_with_bbox)
|
||||||
|
|
||||||
|
print("ErrorImgs:")
|
||||||
|
print(errImgList)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
FLAGS = parser.parse_args()
|
||||||
|
# load image list
|
||||||
|
img_list = get_test_images(FLAGS.image_dir, FLAGS.image_file)
|
||||||
|
# load predictor
|
||||||
|
predictor = InferenceSession(FLAGS.onnx_file)
|
||||||
|
# load infer config
|
||||||
|
infer_config = PredictConfig(FLAGS.infer_cfg)
|
||||||
|
|
||||||
|
predict_image(infer_config, predictor, img_list)
|
||||||
@@ -19,10 +19,16 @@ if __name__ == '__main__':
|
|||||||
help='path to the input image'
|
help='path to the input image'
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-cuda',
|
'--inference-mode',
|
||||||
default=False,
|
type=str,
|
||||||
action='store_true',
|
default='cpu',
|
||||||
help='use cuda or not'
|
help='Inference mode, select one of cpu, cuda, or mps'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--num-beam',
|
||||||
|
type=int,
|
||||||
|
default=1,
|
||||||
|
help='number of beam search for decoding'
|
||||||
)
|
)
|
||||||
# ================= new feature ==================
|
# ================= new feature ==================
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@@ -37,6 +43,7 @@ if __name__ == '__main__':
|
|||||||
# You can use your own checkpoint and tokenizer path.
|
# You can use your own checkpoint and tokenizer path.
|
||||||
print('Loading model and tokenizer...')
|
print('Loading model and tokenizer...')
|
||||||
latex_rec_model = TexTeller.from_pretrained()
|
latex_rec_model = TexTeller.from_pretrained()
|
||||||
|
latex_rec_model = TexTeller.from_pretrained()
|
||||||
tokenizer = TexTeller.get_tokenizer()
|
tokenizer = TexTeller.get_tokenizer()
|
||||||
print('Model and tokenizer loaded.')
|
print('Model and tokenizer loaded.')
|
||||||
|
|
||||||
@@ -44,7 +51,7 @@ if __name__ == '__main__':
|
|||||||
img = cv.imread(args.img)
|
img = cv.imread(args.img)
|
||||||
print('Inference...')
|
print('Inference...')
|
||||||
if not args.mix:
|
if not args.mix:
|
||||||
res = latex_inference(latex_rec_model, tokenizer, [img], args.cuda)
|
res = latex_inference(latex_rec_model, tokenizer, [img], args.inference_mode, args.num_beam)
|
||||||
res = to_katex(res[0])
|
res = to_katex(res[0])
|
||||||
print(res)
|
print(res)
|
||||||
else:
|
else:
|
||||||
|
|||||||
27
src/models/det_model/model/infer_cfg.yml
Normal file
27
src/models/det_model/model/infer_cfg.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
mode: paddle
|
||||||
|
draw_threshold: 0.5
|
||||||
|
metric: COCO
|
||||||
|
use_dynamic_shape: false
|
||||||
|
arch: DETR
|
||||||
|
min_subgraph_size: 3
|
||||||
|
Preprocess:
|
||||||
|
- interp: 2
|
||||||
|
keep_ratio: false
|
||||||
|
target_size:
|
||||||
|
- 640
|
||||||
|
- 640
|
||||||
|
type: Resize
|
||||||
|
- mean:
|
||||||
|
- 0.0
|
||||||
|
- 0.0
|
||||||
|
- 0.0
|
||||||
|
norm_type: none
|
||||||
|
std:
|
||||||
|
- 1.0
|
||||||
|
- 1.0
|
||||||
|
- 1.0
|
||||||
|
type: NormalizeImage
|
||||||
|
- type: Permute
|
||||||
|
label_list:
|
||||||
|
- isolated
|
||||||
|
- embedding
|
||||||
494
src/models/det_model/preprocess.py
Normal file
494
src/models/det_model/preprocess.py
Normal file
@@ -0,0 +1,494 @@
|
|||||||
|
import numpy as np
|
||||||
|
import cv2
|
||||||
|
import copy
|
||||||
|
|
||||||
|
|
||||||
|
def decode_image(img_path):
|
||||||
|
with open(img_path, 'rb') as f:
|
||||||
|
im_read = f.read()
|
||||||
|
data = np.frombuffer(im_read, dtype='uint8')
|
||||||
|
im = cv2.imdecode(data, 1) # BGR mode, but need RGB mode
|
||||||
|
im = cv2.cvtColor(im, cv2.COLOR_BGR2RGB)
|
||||||
|
img_info = {
|
||||||
|
"im_shape": np.array(
|
||||||
|
im.shape[:2], dtype=np.float32),
|
||||||
|
"scale_factor": np.array(
|
||||||
|
[1., 1.], dtype=np.float32)
|
||||||
|
}
|
||||||
|
return im, img_info
|
||||||
|
|
||||||
|
|
||||||
|
class Resize(object):
|
||||||
|
"""resize image by target_size and max_size
|
||||||
|
Args:
|
||||||
|
target_size (int): the target size of image
|
||||||
|
keep_ratio (bool): whether keep_ratio or not, default true
|
||||||
|
interp (int): method of resize
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, target_size, keep_ratio=True, interp=cv2.INTER_LINEAR):
|
||||||
|
if isinstance(target_size, int):
|
||||||
|
target_size = [target_size, target_size]
|
||||||
|
self.target_size = target_size
|
||||||
|
self.keep_ratio = keep_ratio
|
||||||
|
self.interp = interp
|
||||||
|
|
||||||
|
def __call__(self, im, im_info):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
im (np.ndarray): image (np.ndarray)
|
||||||
|
im_info (dict): info of image
|
||||||
|
Returns:
|
||||||
|
im (np.ndarray): processed image (np.ndarray)
|
||||||
|
im_info (dict): info of processed image
|
||||||
|
"""
|
||||||
|
assert len(self.target_size) == 2
|
||||||
|
assert self.target_size[0] > 0 and self.target_size[1] > 0
|
||||||
|
im_channel = im.shape[2]
|
||||||
|
im_scale_y, im_scale_x = self.generate_scale(im)
|
||||||
|
im = cv2.resize(
|
||||||
|
im,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
fx=im_scale_x,
|
||||||
|
fy=im_scale_y,
|
||||||
|
interpolation=self.interp)
|
||||||
|
im_info['im_shape'] = np.array(im.shape[:2]).astype('float32')
|
||||||
|
im_info['scale_factor'] = np.array(
|
||||||
|
[im_scale_y, im_scale_x]).astype('float32')
|
||||||
|
return im, im_info
|
||||||
|
|
||||||
|
def generate_scale(self, im):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
im (np.ndarray): image (np.ndarray)
|
||||||
|
Returns:
|
||||||
|
im_scale_x: the resize ratio of X
|
||||||
|
im_scale_y: the resize ratio of Y
|
||||||
|
"""
|
||||||
|
origin_shape = im.shape[:2]
|
||||||
|
im_c = im.shape[2]
|
||||||
|
if self.keep_ratio:
|
||||||
|
im_size_min = np.min(origin_shape)
|
||||||
|
im_size_max = np.max(origin_shape)
|
||||||
|
target_size_min = np.min(self.target_size)
|
||||||
|
target_size_max = np.max(self.target_size)
|
||||||
|
im_scale = float(target_size_min) / float(im_size_min)
|
||||||
|
if np.round(im_scale * im_size_max) > target_size_max:
|
||||||
|
im_scale = float(target_size_max) / float(im_size_max)
|
||||||
|
im_scale_x = im_scale
|
||||||
|
im_scale_y = im_scale
|
||||||
|
else:
|
||||||
|
resize_h, resize_w = self.target_size
|
||||||
|
im_scale_y = resize_h / float(origin_shape[0])
|
||||||
|
im_scale_x = resize_w / float(origin_shape[1])
|
||||||
|
return im_scale_y, im_scale_x
|
||||||
|
|
||||||
|
|
||||||
|
class NormalizeImage(object):
|
||||||
|
"""normalize image
|
||||||
|
Args:
|
||||||
|
mean (list): im - mean
|
||||||
|
std (list): im / std
|
||||||
|
is_scale (bool): whether need im / 255
|
||||||
|
norm_type (str): type in ['mean_std', 'none']
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, mean, std, is_scale=True, norm_type='mean_std'):
|
||||||
|
self.mean = mean
|
||||||
|
self.std = std
|
||||||
|
self.is_scale = is_scale
|
||||||
|
self.norm_type = norm_type
|
||||||
|
|
||||||
|
def __call__(self, im, im_info):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
im (np.ndarray): image (np.ndarray)
|
||||||
|
im_info (dict): info of image
|
||||||
|
Returns:
|
||||||
|
im (np.ndarray): processed image (np.ndarray)
|
||||||
|
im_info (dict): info of processed image
|
||||||
|
"""
|
||||||
|
im = im.astype(np.float32, copy=False)
|
||||||
|
if self.is_scale:
|
||||||
|
scale = 1.0 / 255.0
|
||||||
|
im *= scale
|
||||||
|
|
||||||
|
if self.norm_type == 'mean_std':
|
||||||
|
mean = np.array(self.mean)[np.newaxis, np.newaxis, :]
|
||||||
|
std = np.array(self.std)[np.newaxis, np.newaxis, :]
|
||||||
|
im -= mean
|
||||||
|
im /= std
|
||||||
|
return im, im_info
|
||||||
|
|
||||||
|
|
||||||
|
class Permute(object):
|
||||||
|
"""permute image
|
||||||
|
Args:
|
||||||
|
to_bgr (bool): whether convert RGB to BGR
|
||||||
|
channel_first (bool): whether convert HWC to CHW
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, ):
|
||||||
|
super(Permute, self).__init__()
|
||||||
|
|
||||||
|
def __call__(self, im, im_info):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
im (np.ndarray): image (np.ndarray)
|
||||||
|
im_info (dict): info of image
|
||||||
|
Returns:
|
||||||
|
im (np.ndarray): processed image (np.ndarray)
|
||||||
|
im_info (dict): info of processed image
|
||||||
|
"""
|
||||||
|
im = im.transpose((2, 0, 1)).copy()
|
||||||
|
return im, im_info
|
||||||
|
|
||||||
|
|
||||||
|
class PadStride(object):
|
||||||
|
""" padding image for model with FPN, instead PadBatch(pad_to_stride) in original config
|
||||||
|
Args:
|
||||||
|
stride (bool): model with FPN need image shape % stride == 0
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, stride=0):
|
||||||
|
self.coarsest_stride = stride
|
||||||
|
|
||||||
|
def __call__(self, im, im_info):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
im (np.ndarray): image (np.ndarray)
|
||||||
|
im_info (dict): info of image
|
||||||
|
Returns:
|
||||||
|
im (np.ndarray): processed image (np.ndarray)
|
||||||
|
im_info (dict): info of processed image
|
||||||
|
"""
|
||||||
|
coarsest_stride = self.coarsest_stride
|
||||||
|
if coarsest_stride <= 0:
|
||||||
|
return im, im_info
|
||||||
|
im_c, im_h, im_w = im.shape
|
||||||
|
pad_h = int(np.ceil(float(im_h) / coarsest_stride) * coarsest_stride)
|
||||||
|
pad_w = int(np.ceil(float(im_w) / coarsest_stride) * coarsest_stride)
|
||||||
|
padding_im = np.zeros((im_c, pad_h, pad_w), dtype=np.float32)
|
||||||
|
padding_im[:, :im_h, :im_w] = im
|
||||||
|
return padding_im, im_info
|
||||||
|
|
||||||
|
|
||||||
|
class LetterBoxResize(object):
|
||||||
|
def __init__(self, target_size):
|
||||||
|
"""
|
||||||
|
Resize image to target size, convert normalized xywh to pixel xyxy
|
||||||
|
format ([x_center, y_center, width, height] -> [x0, y0, x1, y1]).
|
||||||
|
Args:
|
||||||
|
target_size (int|list): image target size.
|
||||||
|
"""
|
||||||
|
super(LetterBoxResize, self).__init__()
|
||||||
|
if isinstance(target_size, int):
|
||||||
|
target_size = [target_size, target_size]
|
||||||
|
self.target_size = target_size
|
||||||
|
|
||||||
|
def letterbox(self, img, height, width, color=(127.5, 127.5, 127.5)):
|
||||||
|
# letterbox: resize a rectangular image to a padded rectangular
|
||||||
|
shape = img.shape[:2] # [height, width]
|
||||||
|
ratio_h = float(height) / shape[0]
|
||||||
|
ratio_w = float(width) / shape[1]
|
||||||
|
ratio = min(ratio_h, ratio_w)
|
||||||
|
new_shape = (round(shape[1] * ratio),
|
||||||
|
round(shape[0] * ratio)) # [width, height]
|
||||||
|
padw = (width - new_shape[0]) / 2
|
||||||
|
padh = (height - new_shape[1]) / 2
|
||||||
|
top, bottom = round(padh - 0.1), round(padh + 0.1)
|
||||||
|
left, right = round(padw - 0.1), round(padw + 0.1)
|
||||||
|
|
||||||
|
img = cv2.resize(
|
||||||
|
img, new_shape, interpolation=cv2.INTER_AREA) # resized, no border
|
||||||
|
img = cv2.copyMakeBorder(
|
||||||
|
img, top, bottom, left, right, cv2.BORDER_CONSTANT,
|
||||||
|
value=color) # padded rectangular
|
||||||
|
return img, ratio, padw, padh
|
||||||
|
|
||||||
|
def __call__(self, im, im_info):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
im (np.ndarray): image (np.ndarray)
|
||||||
|
im_info (dict): info of image
|
||||||
|
Returns:
|
||||||
|
im (np.ndarray): processed image (np.ndarray)
|
||||||
|
im_info (dict): info of processed image
|
||||||
|
"""
|
||||||
|
assert len(self.target_size) == 2
|
||||||
|
assert self.target_size[0] > 0 and self.target_size[1] > 0
|
||||||
|
height, width = self.target_size
|
||||||
|
h, w = im.shape[:2]
|
||||||
|
im, ratio, padw, padh = self.letterbox(im, height=height, width=width)
|
||||||
|
|
||||||
|
new_shape = [round(h * ratio), round(w * ratio)]
|
||||||
|
im_info['im_shape'] = np.array(new_shape, dtype=np.float32)
|
||||||
|
im_info['scale_factor'] = np.array([ratio, ratio], dtype=np.float32)
|
||||||
|
return im, im_info
|
||||||
|
|
||||||
|
|
||||||
|
class Pad(object):
|
||||||
|
def __init__(self, size, fill_value=[114.0, 114.0, 114.0]):
|
||||||
|
"""
|
||||||
|
Pad image to a specified size.
|
||||||
|
Args:
|
||||||
|
size (list[int]): image target size
|
||||||
|
fill_value (list[float]): rgb value of pad area, default (114.0, 114.0, 114.0)
|
||||||
|
"""
|
||||||
|
super(Pad, self).__init__()
|
||||||
|
if isinstance(size, int):
|
||||||
|
size = [size, size]
|
||||||
|
self.size = size
|
||||||
|
self.fill_value = fill_value
|
||||||
|
|
||||||
|
def __call__(self, im, im_info):
|
||||||
|
im_h, im_w = im.shape[:2]
|
||||||
|
h, w = self.size
|
||||||
|
if h == im_h and w == im_w:
|
||||||
|
im = im.astype(np.float32)
|
||||||
|
return im, im_info
|
||||||
|
|
||||||
|
canvas = np.ones((h, w, 3), dtype=np.float32)
|
||||||
|
canvas *= np.array(self.fill_value, dtype=np.float32)
|
||||||
|
canvas[0:im_h, 0:im_w, :] = im.astype(np.float32)
|
||||||
|
im = canvas
|
||||||
|
return im, im_info
|
||||||
|
|
||||||
|
|
||||||
|
def rotate_point(pt, angle_rad):
|
||||||
|
"""Rotate a point by an angle.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pt (list[float]): 2 dimensional point to be rotated
|
||||||
|
angle_rad (float): rotation angle by radian
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[float]: Rotated point.
|
||||||
|
"""
|
||||||
|
assert len(pt) == 2
|
||||||
|
sn, cs = np.sin(angle_rad), np.cos(angle_rad)
|
||||||
|
new_x = pt[0] * cs - pt[1] * sn
|
||||||
|
new_y = pt[0] * sn + pt[1] * cs
|
||||||
|
rotated_pt = [new_x, new_y]
|
||||||
|
|
||||||
|
return rotated_pt
|
||||||
|
|
||||||
|
|
||||||
|
def _get_3rd_point(a, b):
|
||||||
|
"""To calculate the affine matrix, three pairs of points are required. This
|
||||||
|
function is used to get the 3rd point, given 2D points a & b.
|
||||||
|
|
||||||
|
The 3rd point is defined by rotating vector `a - b` by 90 degrees
|
||||||
|
anticlockwise, using b as the rotation center.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
a (np.ndarray): point(x,y)
|
||||||
|
b (np.ndarray): point(x,y)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.ndarray: The 3rd point.
|
||||||
|
"""
|
||||||
|
assert len(a) == 2
|
||||||
|
assert len(b) == 2
|
||||||
|
direction = a - b
|
||||||
|
third_pt = b + np.array([-direction[1], direction[0]], dtype=np.float32)
|
||||||
|
|
||||||
|
return third_pt
|
||||||
|
|
||||||
|
|
||||||
|
def get_affine_transform(center,
|
||||||
|
input_size,
|
||||||
|
rot,
|
||||||
|
output_size,
|
||||||
|
shift=(0., 0.),
|
||||||
|
inv=False):
|
||||||
|
"""Get the affine transform matrix, given the center/scale/rot/output_size.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
center (np.ndarray[2, ]): Center of the bounding box (x, y).
|
||||||
|
scale (np.ndarray[2, ]): Scale of the bounding box
|
||||||
|
wrt [width, height].
|
||||||
|
rot (float): Rotation angle (degree).
|
||||||
|
output_size (np.ndarray[2, ]): Size of the destination heatmaps.
|
||||||
|
shift (0-100%): Shift translation ratio wrt the width/height.
|
||||||
|
Default (0., 0.).
|
||||||
|
inv (bool): Option to inverse the affine transform direction.
|
||||||
|
(inv=False: src->dst or inv=True: dst->src)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.ndarray: The transform matrix.
|
||||||
|
"""
|
||||||
|
assert len(center) == 2
|
||||||
|
assert len(output_size) == 2
|
||||||
|
assert len(shift) == 2
|
||||||
|
if not isinstance(input_size, (np.ndarray, list)):
|
||||||
|
input_size = np.array([input_size, input_size], dtype=np.float32)
|
||||||
|
scale_tmp = input_size
|
||||||
|
|
||||||
|
shift = np.array(shift)
|
||||||
|
src_w = scale_tmp[0]
|
||||||
|
dst_w = output_size[0]
|
||||||
|
dst_h = output_size[1]
|
||||||
|
|
||||||
|
rot_rad = np.pi * rot / 180
|
||||||
|
src_dir = rotate_point([0., src_w * -0.5], rot_rad)
|
||||||
|
dst_dir = np.array([0., dst_w * -0.5])
|
||||||
|
|
||||||
|
src = np.zeros((3, 2), dtype=np.float32)
|
||||||
|
src[0, :] = center + scale_tmp * shift
|
||||||
|
src[1, :] = center + src_dir + scale_tmp * shift
|
||||||
|
src[2, :] = _get_3rd_point(src[0, :], src[1, :])
|
||||||
|
|
||||||
|
dst = np.zeros((3, 2), dtype=np.float32)
|
||||||
|
dst[0, :] = [dst_w * 0.5, dst_h * 0.5]
|
||||||
|
dst[1, :] = np.array([dst_w * 0.5, dst_h * 0.5]) + dst_dir
|
||||||
|
dst[2, :] = _get_3rd_point(dst[0, :], dst[1, :])
|
||||||
|
|
||||||
|
if inv:
|
||||||
|
trans = cv2.getAffineTransform(np.float32(dst), np.float32(src))
|
||||||
|
else:
|
||||||
|
trans = cv2.getAffineTransform(np.float32(src), np.float32(dst))
|
||||||
|
|
||||||
|
return trans
|
||||||
|
|
||||||
|
|
||||||
|
class WarpAffine(object):
|
||||||
|
"""Warp affine the image
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
keep_res=False,
|
||||||
|
pad=31,
|
||||||
|
input_h=512,
|
||||||
|
input_w=512,
|
||||||
|
scale=0.4,
|
||||||
|
shift=0.1):
|
||||||
|
self.keep_res = keep_res
|
||||||
|
self.pad = pad
|
||||||
|
self.input_h = input_h
|
||||||
|
self.input_w = input_w
|
||||||
|
self.scale = scale
|
||||||
|
self.shift = shift
|
||||||
|
|
||||||
|
def __call__(self, im, im_info):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
im (np.ndarray): image (np.ndarray)
|
||||||
|
im_info (dict): info of image
|
||||||
|
Returns:
|
||||||
|
im (np.ndarray): processed image (np.ndarray)
|
||||||
|
im_info (dict): info of processed image
|
||||||
|
"""
|
||||||
|
img = cv2.cvtColor(im, cv2.COLOR_RGB2BGR)
|
||||||
|
|
||||||
|
h, w = img.shape[:2]
|
||||||
|
|
||||||
|
if self.keep_res:
|
||||||
|
input_h = (h | self.pad) + 1
|
||||||
|
input_w = (w | self.pad) + 1
|
||||||
|
s = np.array([input_w, input_h], dtype=np.float32)
|
||||||
|
c = np.array([w // 2, h // 2], dtype=np.float32)
|
||||||
|
|
||||||
|
else:
|
||||||
|
s = max(h, w) * 1.0
|
||||||
|
input_h, input_w = self.input_h, self.input_w
|
||||||
|
c = np.array([w / 2., h / 2.], dtype=np.float32)
|
||||||
|
|
||||||
|
trans_input = get_affine_transform(c, s, 0, [input_w, input_h])
|
||||||
|
img = cv2.resize(img, (w, h))
|
||||||
|
inp = cv2.warpAffine(
|
||||||
|
img, trans_input, (input_w, input_h), flags=cv2.INTER_LINEAR)
|
||||||
|
return inp, im_info
|
||||||
|
|
||||||
|
|
||||||
|
# keypoint preprocess
|
||||||
|
def get_warp_matrix(theta, size_input, size_dst, size_target):
|
||||||
|
"""This code is based on
|
||||||
|
https://github.com/open-mmlab/mmpose/blob/master/mmpose/core/post_processing/post_transforms.py
|
||||||
|
|
||||||
|
Calculate the transformation matrix under the constraint of unbiased.
|
||||||
|
Paper ref: Huang et al. The Devil is in the Details: Delving into Unbiased
|
||||||
|
Data Processing for Human Pose Estimation (CVPR 2020).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
theta (float): Rotation angle in degrees.
|
||||||
|
size_input (np.ndarray): Size of input image [w, h].
|
||||||
|
size_dst (np.ndarray): Size of output image [w, h].
|
||||||
|
size_target (np.ndarray): Size of ROI in input plane [w, h].
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
matrix (np.ndarray): A matrix for transformation.
|
||||||
|
"""
|
||||||
|
theta = np.deg2rad(theta)
|
||||||
|
matrix = np.zeros((2, 3), dtype=np.float32)
|
||||||
|
scale_x = size_dst[0] / size_target[0]
|
||||||
|
scale_y = size_dst[1] / size_target[1]
|
||||||
|
matrix[0, 0] = np.cos(theta) * scale_x
|
||||||
|
matrix[0, 1] = -np.sin(theta) * scale_x
|
||||||
|
matrix[0, 2] = scale_x * (
|
||||||
|
-0.5 * size_input[0] * np.cos(theta) + 0.5 * size_input[1] *
|
||||||
|
np.sin(theta) + 0.5 * size_target[0])
|
||||||
|
matrix[1, 0] = np.sin(theta) * scale_y
|
||||||
|
matrix[1, 1] = np.cos(theta) * scale_y
|
||||||
|
matrix[1, 2] = scale_y * (
|
||||||
|
-0.5 * size_input[0] * np.sin(theta) - 0.5 * size_input[1] *
|
||||||
|
np.cos(theta) + 0.5 * size_target[1])
|
||||||
|
return matrix
|
||||||
|
|
||||||
|
|
||||||
|
class TopDownEvalAffine(object):
|
||||||
|
"""apply affine transform to image and coords
|
||||||
|
|
||||||
|
Args:
|
||||||
|
trainsize (list): [w, h], the standard size used to train
|
||||||
|
use_udp (bool): whether to use Unbiased Data Processing.
|
||||||
|
records(dict): the dict contained the image and coords
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
records (dict): contain the image and coords after tranformed
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, trainsize, use_udp=False):
|
||||||
|
self.trainsize = trainsize
|
||||||
|
self.use_udp = use_udp
|
||||||
|
|
||||||
|
def __call__(self, image, im_info):
|
||||||
|
rot = 0
|
||||||
|
imshape = im_info['im_shape'][::-1]
|
||||||
|
center = im_info['center'] if 'center' in im_info else imshape / 2.
|
||||||
|
scale = im_info['scale'] if 'scale' in im_info else imshape
|
||||||
|
if self.use_udp:
|
||||||
|
trans = get_warp_matrix(
|
||||||
|
rot, center * 2.0,
|
||||||
|
[self.trainsize[0] - 1.0, self.trainsize[1] - 1.0], scale)
|
||||||
|
image = cv2.warpAffine(
|
||||||
|
image,
|
||||||
|
trans, (int(self.trainsize[0]), int(self.trainsize[1])),
|
||||||
|
flags=cv2.INTER_LINEAR)
|
||||||
|
else:
|
||||||
|
trans = get_affine_transform(center, scale, rot, self.trainsize)
|
||||||
|
image = cv2.warpAffine(
|
||||||
|
image,
|
||||||
|
trans, (int(self.trainsize[0]), int(self.trainsize[1])),
|
||||||
|
flags=cv2.INTER_LINEAR)
|
||||||
|
|
||||||
|
return image, im_info
|
||||||
|
|
||||||
|
|
||||||
|
class Compose:
|
||||||
|
def __init__(self, transforms):
|
||||||
|
self.transforms = []
|
||||||
|
for op_info in transforms:
|
||||||
|
new_op_info = op_info.copy()
|
||||||
|
op_type = new_op_info.pop('type')
|
||||||
|
self.transforms.append(eval(op_type)(**new_op_info))
|
||||||
|
|
||||||
|
def __call__(self, img_path):
|
||||||
|
img, im_info = decode_image(img_path)
|
||||||
|
for t in self.transforms:
|
||||||
|
img, im_info = t(img, im_info)
|
||||||
|
inputs = copy.deepcopy(im_info)
|
||||||
|
inputs['image'] = img
|
||||||
|
return inputs
|
||||||
@@ -13,8 +13,8 @@ from models.globals import MAX_TOKEN_SIZE
|
|||||||
def inference(
|
def inference(
|
||||||
model: TexTeller,
|
model: TexTeller,
|
||||||
tokenizer: RobertaTokenizerFast,
|
tokenizer: RobertaTokenizerFast,
|
||||||
imgs: Union[List[str], List[np.ndarray]],
|
imgs_path: Union[List[str], List[np.ndarray]],
|
||||||
use_cuda: bool,
|
inf_mode: str = 'cpu',
|
||||||
num_beams: int = 1,
|
num_beams: int = 1,
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
model.eval()
|
model.eval()
|
||||||
@@ -26,9 +26,8 @@ def inference(
|
|||||||
imgs = inference_transform(imgs)
|
imgs = inference_transform(imgs)
|
||||||
pixel_values = torch.stack(imgs)
|
pixel_values = torch.stack(imgs)
|
||||||
|
|
||||||
if use_cuda:
|
model = model.to(inf_mode)
|
||||||
model = model.to('cuda')
|
pixel_values = pixel_values.to(inf_mode)
|
||||||
pixel_values = pixel_values.to('cuda')
|
|
||||||
|
|
||||||
generate_config = GenerationConfig(
|
generate_config = GenerationConfig(
|
||||||
max_new_tokens=MAX_TOKEN_SIZE,
|
max_new_tokens=MAX_TOKEN_SIZE,
|
||||||
|
|||||||
59
src/rec_infer_from_crop_imgs.py
Normal file
59
src/rec_infer_from_crop_imgs.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import os
|
||||||
|
import argparse
|
||||||
|
import cv2 as cv
|
||||||
|
from pathlib import Path
|
||||||
|
from utils import to_katex
|
||||||
|
from models.ocr_model.utils.inference import inference as latex_inference
|
||||||
|
from models.ocr_model.model.TexTeller import TexTeller
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
os.chdir(Path(__file__).resolve().parent)
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument(
|
||||||
|
'-img',
|
||||||
|
type=str,
|
||||||
|
required=True,
|
||||||
|
help='path to the input image'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--inference-mode',
|
||||||
|
type=str,
|
||||||
|
default='cpu',
|
||||||
|
help='Inference mode, select one of cpu, cuda, or mps'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--num-beam',
|
||||||
|
type=int,
|
||||||
|
default=1,
|
||||||
|
help='number of beam search for decoding'
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
print('Loading model and tokenizer...')
|
||||||
|
latex_rec_model = TexTeller.from_pretrained()
|
||||||
|
tokenizer = TexTeller.get_tokenizer()
|
||||||
|
print('Model and tokenizer loaded.')
|
||||||
|
|
||||||
|
# Create the output directory if it doesn't exist
|
||||||
|
os.makedirs(args.output_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Loop through all images in the input directory
|
||||||
|
for filename in os.listdir(args.img_dir):
|
||||||
|
img_path = os.path.join(args.img_dir, filename)
|
||||||
|
img = cv.imread(img_path)
|
||||||
|
|
||||||
|
if img is not None:
|
||||||
|
print(f'Inference for {filename}...')
|
||||||
|
res = latex_inference(latex_rec_model, tokenizer, [img], inf_mode=args.inference_mode, num_beams=args.num_beam)
|
||||||
|
res = to_katex(res[0])
|
||||||
|
|
||||||
|
# Save the recognition result to a text file
|
||||||
|
output_file = os.path.join(args.output_dir, os.path.splitext(filename)[0] + '.txt')
|
||||||
|
with open(output_file, 'w') as f:
|
||||||
|
f.write(res)
|
||||||
|
|
||||||
|
print(f'Result saved to {output_file}')
|
||||||
|
else:
|
||||||
|
print(f"Warning: Could not read image {img_path}. Skipping...")
|
||||||
@@ -23,8 +23,8 @@ parser.add_argument('--num_replicas', type=int, default=1)
|
|||||||
parser.add_argument('--ncpu_per_replica', type=float, default=1.0)
|
parser.add_argument('--ncpu_per_replica', type=float, default=1.0)
|
||||||
parser.add_argument('--ngpu_per_replica', type=float, default=0.0)
|
parser.add_argument('--ngpu_per_replica', type=float, default=0.0)
|
||||||
|
|
||||||
parser.add_argument('--use_cuda', action='store_true', default=False)
|
parser.add_argument('--inference-mode', type=str, default='cpu')
|
||||||
parser.add_argument('--num_beam', type=int, default=1)
|
parser.add_argument('--num_beams', type=int, default=1)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
if args.ngpu_per_replica > 0 and not args.use_cuda:
|
if args.ngpu_per_replica > 0 and not args.use_cuda:
|
||||||
@@ -43,18 +43,21 @@ class TexTellerServer:
|
|||||||
self,
|
self,
|
||||||
checkpoint_path: str,
|
checkpoint_path: str,
|
||||||
tokenizer_path: str,
|
tokenizer_path: str,
|
||||||
use_cuda: bool = False,
|
inf_mode: str = 'cpu',
|
||||||
num_beam: int = 1
|
num_beams: int = 1
|
||||||
) -> None:
|
) -> None:
|
||||||
self.model = TexTeller.from_pretrained(checkpoint_path)
|
self.model = TexTeller.from_pretrained(checkpoint_path)
|
||||||
self.tokenizer = TexTeller.get_tokenizer(tokenizer_path)
|
self.tokenizer = TexTeller.get_tokenizer(tokenizer_path)
|
||||||
self.use_cuda = use_cuda
|
self.inf_mode = inf_mode
|
||||||
self.num_beam = num_beam
|
self.num_beams = num_beams
|
||||||
|
|
||||||
self.model = self.model.to('cuda') if use_cuda else self.model
|
self.model = self.model.to(inf_mode) if inf_mode != 'cpu' else self.model
|
||||||
|
|
||||||
def predict(self, image_nparray) -> str:
|
def predict(self, image_nparray) -> str:
|
||||||
return inference(self.model, self.tokenizer, [image_nparray], self.use_cuda, self.num_beam)[0]
|
return inference(
|
||||||
|
self.model, self.tokenizer, [image_nparray],
|
||||||
|
inf_mode=self.inf_mode, num_beams=self.num_beams
|
||||||
|
)[0]
|
||||||
|
|
||||||
|
|
||||||
@serve.deployment()
|
@serve.deployment()
|
||||||
@@ -78,7 +81,11 @@ if __name__ == '__main__':
|
|||||||
tknz_dir = args.tokenizer_dir
|
tknz_dir = args.tokenizer_dir
|
||||||
|
|
||||||
serve.start(http_options={"port": args.server_port})
|
serve.start(http_options={"port": args.server_port})
|
||||||
texteller_server = TexTellerServer.bind(ckpt_dir, tknz_dir, use_cuda=args.use_cuda, num_beam=args.num_beam)
|
texteller_server = TexTellerServer.bind(
|
||||||
|
ckpt_dir, tknz_dir,
|
||||||
|
inf_mode=args.inference_mode,
|
||||||
|
num_beams=args.num_beams
|
||||||
|
)
|
||||||
ingress = Ingress.bind(texteller_server)
|
ingress = Ingress.bind(texteller_server)
|
||||||
|
|
||||||
ingress_handle = serve.run(ingress, route_prefix="/predict")
|
ingress_handle = serve.run(ingress, route_prefix="/predict")
|
||||||
|
|||||||
9
src/start_web.bat
Normal file
9
src/start_web.bat
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
@echo off
|
||||||
|
SETLOCAL ENABLEEXTENSIONS
|
||||||
|
|
||||||
|
set CHECKPOINT_DIR=default
|
||||||
|
set TOKENIZER_DIR=default
|
||||||
|
|
||||||
|
streamlit run web.py
|
||||||
|
|
||||||
|
ENDLOCAL
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -exu
|
set -exu
|
||||||
|
|
||||||
export CHECKPOINT_DIR="/home/lhy/code/TexTeller/src/models/ocr_model/train/train_result/TexTellerv3/checkpoint-460000"
|
export CHECKPOINT_DIR="default"
|
||||||
# export CHECKPOINT_DIR="default"
|
export TOKENIZER_DIR="default"
|
||||||
export TOKENIZER_DIR="/home/lhy/code/TexTeller/src/models/tokenizer/roberta-tokenizer-7Mformulas"
|
|
||||||
export USE_CUDA=True # True or False (case-sensitive)
|
|
||||||
export NUM_BEAM=3
|
|
||||||
|
|
||||||
streamlit run web.py
|
streamlit run web.py
|
||||||
|
|||||||
134
src/web.py
134
src/web.py
@@ -6,16 +6,22 @@ import shutil
|
|||||||
import streamlit as st
|
import streamlit as st
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
from streamlit_paste_button import paste_image_button as pbutton
|
||||||
from models.ocr_model.utils.inference import inference
|
from models.ocr_model.utils.inference import inference
|
||||||
from models.ocr_model.model.TexTeller import TexTeller
|
from models.ocr_model.model.TexTeller import TexTeller
|
||||||
from utils import to_katex
|
from utils import to_katex
|
||||||
|
|
||||||
|
|
||||||
|
st.set_page_config(
|
||||||
|
page_title="TexTeller",
|
||||||
|
page_icon="🧮"
|
||||||
|
)
|
||||||
|
|
||||||
html_string = '''
|
html_string = '''
|
||||||
<h1 style="color: black; text-align: center;">
|
<h1 style="color: black; text-align: center;">
|
||||||
<img src="https://slackmojis.com/emojis/429-troll/download" width="50">
|
<img src="https://raw.githubusercontent.com/OleehyO/TexTeller/main/assets/fire.svg" width="100">
|
||||||
TexTeller
|
𝚃𝚎𝚡𝚃𝚎𝚕𝚕𝚎𝚛
|
||||||
<img src="https://slackmojis.com/emojis/429-troll/download" width="50">
|
<img src="https://raw.githubusercontent.com/OleehyO/TexTeller/main/assets/fire.svg" width="100">
|
||||||
</h1>
|
</h1>
|
||||||
'''
|
'''
|
||||||
|
|
||||||
@@ -35,8 +41,6 @@ fail_gif_html = '''
|
|||||||
</h1>
|
</h1>
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@st.cache_resource
|
@st.cache_resource
|
||||||
def get_model():
|
def get_model():
|
||||||
return TexTeller.from_pretrained(os.environ['CHECKPOINT_DIR'])
|
return TexTeller.from_pretrained(os.environ['CHECKPOINT_DIR'])
|
||||||
@@ -52,6 +56,12 @@ def get_image_base64(img_file):
|
|||||||
img.save(buffered, format="PNG")
|
img.save(buffered, format="PNG")
|
||||||
return base64.b64encode(buffered.getvalue()).decode()
|
return base64.b64encode(buffered.getvalue()).decode()
|
||||||
|
|
||||||
|
def on_file_upload():
|
||||||
|
st.session_state["UPLOADED_FILE_CHANGED"] = True
|
||||||
|
|
||||||
|
def change_side_bar():
|
||||||
|
st.session_state["CHANGE_SIDEBAR_FLAG"] = True
|
||||||
|
|
||||||
model = get_model()
|
model = get_model()
|
||||||
tokenizer = get_tokenizer()
|
tokenizer = get_tokenizer()
|
||||||
|
|
||||||
@@ -59,37 +69,106 @@ if "start" not in st.session_state:
|
|||||||
st.session_state["start"] = 1
|
st.session_state["start"] = 1
|
||||||
st.toast('Hooray!', icon='🎉')
|
st.toast('Hooray!', icon='🎉')
|
||||||
|
|
||||||
|
if "UPLOADED_FILE_CHANGED" not in st.session_state:
|
||||||
|
st.session_state["UPLOADED_FILE_CHANGED"] = False
|
||||||
|
|
||||||
# ============================ pages =============================== #
|
if "CHANGE_SIDEBAR_FLAG" not in st.session_state:
|
||||||
|
st.session_state["CHANGE_SIDEBAR_FLAG"] = False
|
||||||
|
|
||||||
|
# ============================ begin sidebar =============================== #
|
||||||
|
|
||||||
|
with st.sidebar:
|
||||||
|
num_beams = 1
|
||||||
|
inf_mode = 'cpu'
|
||||||
|
|
||||||
|
st.markdown("# 🔨️ Config")
|
||||||
|
st.markdown("")
|
||||||
|
|
||||||
|
model_type = st.selectbox(
|
||||||
|
"Model type",
|
||||||
|
("TexTeller", "None"),
|
||||||
|
on_change=change_side_bar
|
||||||
|
)
|
||||||
|
if model_type == "TexTeller":
|
||||||
|
num_beams = st.number_input(
|
||||||
|
'Number of beams',
|
||||||
|
min_value=1,
|
||||||
|
max_value=20,
|
||||||
|
step=1,
|
||||||
|
on_change=change_side_bar
|
||||||
|
)
|
||||||
|
|
||||||
|
inf_mode = st.radio(
|
||||||
|
"Inference mode",
|
||||||
|
("cpu", "cuda", "mps"),
|
||||||
|
on_change=change_side_bar
|
||||||
|
)
|
||||||
|
|
||||||
|
# ============================ end sidebar =============================== #
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ============================ begin pages =============================== #
|
||||||
|
|
||||||
st.markdown(html_string, unsafe_allow_html=True)
|
st.markdown(html_string, unsafe_allow_html=True)
|
||||||
|
|
||||||
uploaded_file = st.file_uploader("",type=['jpg', 'png', 'pdf'])
|
uploaded_file = st.file_uploader(
|
||||||
|
" ",
|
||||||
|
type=['jpg', 'png'],
|
||||||
|
on_change=on_file_upload
|
||||||
|
)
|
||||||
|
|
||||||
|
paste_result = pbutton(
|
||||||
|
label="📋 Paste an image",
|
||||||
|
background_color="#5BBCFF",
|
||||||
|
hover_background_color="#3498db",
|
||||||
|
)
|
||||||
|
st.write("")
|
||||||
|
|
||||||
|
if st.session_state["CHANGE_SIDEBAR_FLAG"] == True:
|
||||||
|
st.session_state["CHANGE_SIDEBAR_FLAG"] = False
|
||||||
|
elif uploaded_file or paste_result.image_data is not None:
|
||||||
|
if st.session_state["UPLOADED_FILE_CHANGED"] == False and paste_result.image_data is not None:
|
||||||
|
uploaded_file = io.BytesIO()
|
||||||
|
paste_result.image_data.save(uploaded_file, format='PNG')
|
||||||
|
uploaded_file.seek(0)
|
||||||
|
|
||||||
|
if st.session_state["UPLOADED_FILE_CHANGED"] == True:
|
||||||
|
st.session_state["UPLOADED_FILE_CHANGED"] = False
|
||||||
|
|
||||||
if uploaded_file:
|
|
||||||
img = Image.open(uploaded_file)
|
img = Image.open(uploaded_file)
|
||||||
|
|
||||||
temp_dir = tempfile.mkdtemp()
|
temp_dir = tempfile.mkdtemp()
|
||||||
png_file_path = os.path.join(temp_dir, 'image.png')
|
png_file_path = os.path.join(temp_dir, 'image.png')
|
||||||
img.save(png_file_path, 'PNG')
|
img.save(png_file_path, 'PNG')
|
||||||
|
|
||||||
img_base64 = get_image_base64(uploaded_file)
|
with st.container(height=300):
|
||||||
|
img_base64 = get_image_base64(uploaded_file)
|
||||||
|
|
||||||
|
st.markdown(f"""
|
||||||
|
<style>
|
||||||
|
.centered-container {{
|
||||||
|
text-align: center;
|
||||||
|
}}
|
||||||
|
.centered-image {{
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
max-height: 350px;
|
||||||
|
max-width: 100%;
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
<div class="centered-container">
|
||||||
|
<img src="data:image/png;base64,{img_base64}" class="centered-image" alt="Input image">
|
||||||
|
</div>
|
||||||
|
""", unsafe_allow_html=True)
|
||||||
st.markdown(f"""
|
st.markdown(f"""
|
||||||
<style>
|
<style>
|
||||||
.centered-container {{
|
.centered-container {{
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}}
|
}}
|
||||||
.centered-image {{
|
|
||||||
display: block;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
max-width: 500px;
|
|
||||||
max-height: 500px;
|
|
||||||
}}
|
|
||||||
</style>
|
</style>
|
||||||
<div class="centered-container">
|
<div class="centered-container">
|
||||||
<img src="data:image/png;base64,{img_base64}" class="centered-image" alt="Input image">
|
|
||||||
<p style="color:gray;">Input image ({img.height}✖️{img.width})</p>
|
<p style="color:gray;">Input image ({img.height}✖️{img.width})</p>
|
||||||
</div>
|
</div>
|
||||||
""", unsafe_allow_html=True)
|
""", unsafe_allow_html=True)
|
||||||
@@ -102,15 +181,28 @@ if uploaded_file:
|
|||||||
model,
|
model,
|
||||||
tokenizer,
|
tokenizer,
|
||||||
[png_file_path],
|
[png_file_path],
|
||||||
True if os.environ['USE_CUDA'] == 'True' else False,
|
inf_mode=inf_mode,
|
||||||
int(os.environ['NUM_BEAM'])
|
num_beams=num_beams
|
||||||
)[0]
|
)[0]
|
||||||
st.success('Completed!', icon="✅")
|
st.success('Completed!', icon="✅")
|
||||||
st.markdown(suc_gif_html, unsafe_allow_html=True)
|
st.markdown(suc_gif_html, unsafe_allow_html=True)
|
||||||
katex_res = to_katex(TexTeller_result)
|
katex_res = to_katex(TexTeller_result)
|
||||||
st.text_area(":red[Predicted formula]", katex_res, height=150)
|
st.text_area(":blue[*** 𝑃r𝑒d𝑖c𝑡e𝑑 𝑓o𝑟m𝑢l𝑎 ***]", katex_res, height=150)
|
||||||
st.latex(katex_res)
|
st.latex(katex_res)
|
||||||
|
|
||||||
|
st.write("")
|
||||||
|
st.write("")
|
||||||
|
|
||||||
|
with st.expander(":star2: :gray[Tips for better results]"):
|
||||||
|
st.markdown('''
|
||||||
|
* :mag_right: Use a clear and high-resolution image.
|
||||||
|
* :scissors: Crop images as accurately as possible.
|
||||||
|
* :jigsaw: Split large multi line formulas into smaller ones.
|
||||||
|
* :page_facing_up: Use images with **white background and black text** as much as possible.
|
||||||
|
* :book: Use a font with good readability.
|
||||||
|
''')
|
||||||
shutil.rmtree(temp_dir)
|
shutil.rmtree(temp_dir)
|
||||||
|
|
||||||
# ============================ pages =============================== #
|
paste_result.image_data = None
|
||||||
|
|
||||||
|
# ============================ end pages =============================== #
|
||||||
|
|||||||
Reference in New Issue
Block a user