Ouzhang commited on
Commit
1925a33
·
verified ·
1 Parent(s): 7e2d9c4

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/en/.readthedocs.yaml +17 -0
  2. reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/en/ConfigSystem.md +67 -0
  3. reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/en/Contributors.md +21 -0
  4. reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/en/Development.md +145 -0
  5. reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/en/EvalByLMDeploy.md +27 -0
  6. reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/en/Makefile +20 -0
  7. reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/en/Quickstart.md +236 -0
  8. reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/en/_templates/404.html +18 -0
  9. reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/en/conf.py +234 -0
  10. reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/en/docutils.conf +2 -0
  11. reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/en/index.rst +41 -0
  12. reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/ja/README_ja.md +117 -0
  13. reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/.readthedocs.yaml +17 -0
  14. reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/ConfigSystem.md +69 -0
  15. reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/Development.md +139 -0
  16. reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/EvalByLMDeploy.md +28 -0
  17. reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/Makefile +20 -0
  18. reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/Quickstart.md +231 -0
  19. reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/README_zh-CN.md +131 -0
  20. reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/_static/css/readthedocs.css +63 -0
  21. reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/_static/image/logo.svg +24 -0
  22. reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/_static/image/logo_icon.svg +31 -0
  23. reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/_static/js/custom.js +10 -0
  24. reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/_templates/404.html +18 -0
  25. reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/_templates/autosummary/class.rst +13 -0
  26. reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/_templates/callable.rst +14 -0
  27. reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/conf.py +242 -0
  28. reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/cp_origin_docs.sh +9 -0
  29. reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/docutils.conf +2 -0
  30. reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/index.rst +49 -0
  31. reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/CGAVCounting/__init__.py +0 -0
  32. reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/CGAVCounting/cg_av_counting.py +415 -0
  33. reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/CGAVCounting/requirements.txt +2 -0
  34. reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/CGAVCounting/utils.py +423 -0
  35. reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/EgoExoBench/tf2023_preprocess.py +72 -0
  36. reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/EgoExoBench/utils.py +758 -0
  37. reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/GUI/__init__.py +0 -0
  38. reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/GUI/osworld_g.py +441 -0
  39. reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/GUI/screenspot.py +462 -0
  40. reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/GUI/screenspot_pro.py +461 -0
  41. reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/GUI/screenspot_v2.py +203 -0
  42. reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/GUI/vbgd.py +447 -0
  43. reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/GUI/venusbench.py +186 -0
  44. reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/OmniDocBench/__init__.py +0 -0
  45. reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/OmniDocBench/data_preprocess.py +449 -0
  46. reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/OmniDocBench/metrics.py +486 -0
  47. reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/OmniDocBench/omnidocbench.py +557 -0
  48. reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/OmniDocBench/requirements.txt +13 -0
  49. reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/OmniDocBench/utils.py +1918 -0
  50. reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/SGI_Bench_1_0/__init__.py +0 -0
reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/en/.readthedocs.yaml ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: 2
2
+
3
+ # Set the version of Python and other tools you might need
4
+ build:
5
+ os: ubuntu-22.04
6
+ tools:
7
+ python: "3.8"
8
+
9
+ formats:
10
+ - epub
11
+
12
+ sphinx:
13
+ configuration: docs/en/conf.py
14
+
15
+ python:
16
+ install:
17
+ - requirements: requirements/docs.txt
reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/en/ConfigSystem.md ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Config System
2
+
3
+ By default, VLMEvalKit launches the evaluation by setting the model name(s) (defined in `/vlmeval/config.py`) and dataset name(s) (defined in `vlmeval/dataset/__init__.py` or `vlmeval/dataset/video_dataset_config.py`) in the `run.py` script with the `--model` and `--data` arguments. Such approach is simple and efficient in most scenarios, however, it may not be flexible enough when the user wants to evaluate multiple models / datasets with different settings.
4
+
5
+ To address this, VLMEvalKit provides a more flexible config system. The user can specify the model and dataset settings in a json file, and pass the path to the config file to the `run.py` script with the `--config` argument. Here is a sample config json:
6
+
7
+ ```json
8
+ {
9
+ "model": {
10
+ "GPT4o_20240806_T00_HIGH": {
11
+ "class": "GPT4V",
12
+ "model": "gpt-4o-2024-08-06",
13
+ "temperature": 0,
14
+ "img_detail": "high"
15
+ },
16
+ "GPT4o_20240806_T10_Low": {
17
+ "class": "GPT4V",
18
+ "model": "gpt-4o-2024-08-06",
19
+ "temperature": 1.0,
20
+ "img_detail": "low"
21
+ },
22
+ "GPT4o_20241120": {}
23
+ },
24
+ "data": {
25
+ "MME-RealWorld-Lite": {
26
+ "class": "MMERealWorld",
27
+ "dataset": "MME-RealWorld-Lite"
28
+ },
29
+ "MMBench_DEV_EN_V11": {
30
+ "class": "ImageMCQDataset",
31
+ "dataset": "MMBench_DEV_EN_V11"
32
+ },
33
+ "MMBench_Video_8frame_nopack":{},
34
+ "Video-MME_16frame_subs": {
35
+ "class": "VideoMME",
36
+ "dataset": "Video-MME",
37
+ "nframe": 16,
38
+ "use_subtitle": true
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ Explanation of the config json:
45
+
46
+ 1. Now we support two fields: `model` and `data`, each of which is a dictionary. The key of the dictionary is the name of the model / dataset (set by the user), and the value is the setting of the model / dataset.
47
+ 2. For items in `model`, the value is a dictionary containing the following keys:
48
+ - `class`: The class name of the model, which should be a class name defined in `vlmeval/vlm/__init__.py` (open-source models) or `vlmeval/api/__init__.py` (API models).
49
+ - Other kwargs: Other kwargs are model-specific parameters, please refer to the definition of the model class for detailed usage. For example, `model`, `temperature`, `img_detail` are arguments of the `GPT4V` class. It's noteworthy that the `model` argument is required by most model classes.
50
+ - Tip: The defined model in the `supported_VLM` of `vlmeval/config.py` can be used as a shortcut, for example, `GPT4o_20241120: {}` is equivalent to `GPT4o_20241120: {'class': 'GPT4V', 'model': 'gpt-4o-2024-11-20', 'temperature': 0, 'img_size': -1, 'img_detail': 'high', 'retry': 10, 'verbose': False}`
51
+ 3. For the dictionary `data`, we suggest users to use the official dataset name as the key (or part of the key), since we frequently determine the post-processing / judging settings based on the dataset name. For items in `data`, the value is a dictionary containing the following keys:
52
+ - `class`: The class name of the dataset, which should be a class name defined in `vlmeval/dataset/__init__.py`.
53
+ - Other kwargs: Other kwargs are dataset-specific parameters, please refer to the definition of the dataset class for detailed usage. Typically, the `dataset` argument is required by most dataset classes. It's noteworthy that the `nframe` argument or `fps` argument is required by most video dataset classes.
54
+ - Tip: The defined dataset in the `supported_video_datasets` of `vlmeval/dataset/video_dataset_config.py` can be used as a shortcut, for example, `MMBench_Video_8frame_nopack: {}` is equivalent to `MMBench_Video_8frame_nopack: {'class': 'MMBenchVideo', 'dataset': 'MMBench-Video', 'nframe': 8, 'pack': False}`.
55
+ Saving the example config json to `config.json`, you can launch the evaluation by:
56
+
57
+ ```bash
58
+ python run.py --config config.json
59
+ ```
60
+
61
+ That will generate the following output files under the working directory `$WORK_DIR` (Following the format `{$WORK_DIR}/{$MODEL_NAME}/{$MODEL_NAME}_{$DATASET_NAME}_*`):
62
+
63
+ - `$WORK_DIR/GPT4o_20240806_T00_HIGH/GPT4o_20240806_T00_HIGH_MME-RealWorld-Lite*`
64
+ - `$WORK_DIR/GPT4o_20240806_T10_Low/GPT4o_20240806_T10_Low_MME-RealWorld-Lite*`
65
+ - `$WORK_DIR/GPT4o_20240806_T00_HIGH/GPT4o_20240806_T00_HIGH_MMBench_DEV_EN_V11*`
66
+ - `$WORK_DIR/GPT4o_20240806_T10_Low/GPT4o_20240806_T10_Low_MMBench_DEV_EN_V11*`
67
+ ...
reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/en/Contributors.md ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Contributors
2
+
3
+ ## Contributors w. 3+ Major Contributions
4
+
5
+ > In this section, we list all the contributors who have made significant contributions (3+) to the development of VLMEvalKit.
6
+
7
+ New Qualified Contributors (2024.09):
8
+
9
+ 1. [amitbcp](https://github.com/amitbcp): The contributor helped support MUIRBench, Phi-3.5, Idefics3, VILA, and xGen-MM
10
+ 2. [czczup](https://github.com/czczup): The contributor helped support the InternVL Series (V1.5, Mini-InternVL, V2, etc.)
11
+ 3. [DseidLi](https://github.com/DseidLi): The contributor helped support LLaVA-OneVision, GQA, and developed the readthedocs site for VLMEvalKit
12
+ 4. [mayubo2333](https://github.com/mayubo2333): The contributor helped support MMLongBench, SlideVQA, and DUDE
13
+ 5. [sun-hailong](https://github.com/sun-hailong): The contributor helped support A-OKVQA, Parrot, MMMB, and MTL-MMBench
14
+ 6. [PhoenixZ810](https://github.com/PhoenixZ810): The contributor helped support Video-ChatGPT, Chat-UniVI, and Llama-VID
15
+ 7. [Cuiunbo](https://github.com/Cuiunbo): The contributor helped support OmniLMM-12B, MiniCPM-V Series (V1, V2, V2.5)
16
+
17
+ ## Full Contributor List
18
+
19
+ > In this section, we list all the contributors as well as their corresponding contributions to the development of VLMEvalKit.
20
+
21
+ TBD.
reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/en/Development.md ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Develop new Benchmark / MLLM
2
+
3
+ > 🛠️ How to implement a new Benchmark / VLM in VLMEvalKit?
4
+
5
+ ## Implement a new benchmark
6
+
7
+ Example PR: **Math-Vision Benchmark** ([#292](https://github.com/open-compass/VLMEvalKit/pull/292/files))
8
+
9
+ In VLMEvalKit, benchmarks are organized as dataset classes. When you try to implement a new benchmark, you can either reuse existing dataset classes (*e.g.*, You can reuse `ImageMCQDataset` when implementing a new multi-choice benchmark), or support a new dataset class. Each dataset must have the following two member functions (either reuse the one of the parent class or implement your own):
10
+
11
+ - `build_prompt(self, line)`: The function input `line` is an integer (the sample index) or a `pd.Series` object (the raw record of the sample). The function outputs a `multi-modal message`, serving as the input of an MLLM. The `multi-modal message` is an interleaved list of multi-modal messages adopting the following format (the example includes an image and a text message): `[dict(type='image', value=IMAGE_PTH), dict(type='text', value=prompt)]`.
12
+ - `evaluate(self, eval_file, **judge_kwargs)`: The function input `eval_file` is the MLLM prediction (typically in `.xlsx` format). If the benchmark requires an external LLM (typically GPT) for evaluation, then `judge_kwargs` can pass the arguments for the LLM. The function outputs the benchmark evaluation results (metrics) in the form of `dict` or `pd.DataFrame`.
13
+
14
+ We then brief the typical steps to implement a new benchmark under VLMEvalKit:
15
+
16
+ ### 1. Prepare your benchmark tsv file
17
+
18
+ Currently, we organize a benchmark as one single TSV file. During inference, the data file will be automatically downloaded from the definited `DATASET_URL` link to `$LMUData` file (default path is `$HOME/LMUData`, if not set explicitly). You can upload the prepared TSV file to a downloadable address (e.g., Huggingface) or send it to us at <opencompass@pjlab.org.cn>. We will assist in uploading the dataset to the server. You can also customize `LMUData` path in the environment variable `LMUData=/path/to/your/data`.
19
+
20
+ The contents of the TSV file consist of:
21
+
22
+ | Dataset Name \ Fields | index | image | image_path | question | hint | multi-choice<br>options | answer | category | l2-category | split |
23
+ | --------------------------------------- | ----- | ----- | ---------- | -------- | ---- | ----------------------- | ------ | -------- | ----------- | ----- |
24
+ | MMBench_DEV_[CN/EN] | ✅ | ✅ | | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
25
+ | MMBench_TEST_[CN/EN] | ✅ | ✅ | | ✅ | ✅ | ✅ | | ✅ | ✅ | ✅ |
26
+ | CCBench | ✅ | ✅ | | ✅ | | ✅ | ✅ | ✅ | | |
27
+ | SEEDBench_IMG | ✅ | ✅ | | ✅ | | ✅ | ✅ | ✅ | | |
28
+ | MME | ✅ | ✅ | | ✅ | | | ✅ | ✅ | | |
29
+ | MMVet | ✅ | ✅ | | ✅ | | | ✅ | ✅ | | |
30
+ | MMMU_DEV_VAL | ✅ | ✅ | ✅ | ✅ | | ✅ | ✅ | ✅ | ✅ | ✅ |
31
+ | COCO_VAL | ✅ | ✅ | | | | | ✅ | | | |
32
+ | OCRVQA_[TEST/TESTCORE] | ✅ | ✅ | | ✅ | | | ✅ | | | |
33
+ | TextVQA_VAL | ✅ | ✅ | | ✅ | | | ✅ | | | |
34
+ | VCR_[EN/ZH]\_[EASY/HARD]\_[ALL/500/100] | ✅ | ✅ | | ✅ | | | ✅ | | | |
35
+ | MMMB_[en/cn/pt/ar/tr/ru] | ✅ | ✅ | | ✅ | ✅ | ✅ | ✅ | ✅ | |✅ |
36
+ | MMBench_dev_[en/cn/pt/ar/tr/ru] | ✅ | ✅ | | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |✅ |
37
+
38
+ <div align="center"><b>Table 1. TSV fields of supported datasets.</b></div>
39
+
40
+ **Intro to mandatory fields in the `TSV` file:**
41
+
42
+ - **index:** Integer, Unique for each line in `tsv`
43
+ - **image:** The base64 of the image, you can use APIs implemented in `vlmeval/smp/vlm.py` for encoding and decoding:
44
+ - Encoding: `encode_image_to_base64 `(for PIL Image) / `encode_image_file_to_base64` (for image file path)
45
+ - Decoding: `decode_base64_to_image`(for PIL Image) / `decode_base64_to_image_file` (for image file path)
46
+ - **question**: The question corresponding to the image, a string
47
+ - **answer**: The answer to the question, a string. The `test` split does not need this field
48
+
49
+ ### 2. Cutomize your benchmark prompt
50
+
51
+ `ImageBaseDataset` defines the default prompt format. If you need to add prompts specific to the dataset or input data in the `Interleave` format to the model, you can implement this through the `build_prompt(line)` function. This function takes a line from a TSV file as input, containing fields such as index, image, question, etc. The function returns a dictionary list of multimodal messages `msg` in the format `[dict(type='image', value=IMAGE_PTH), dict(type='text', value=prompt)]`, including the image path and the text prompt to be input into VLMs. For interleave type inputs, you can directly place the dictionary of the image path at the image token position.
52
+
53
+ ### 3. Cutomize your benchmark metrics
54
+
55
+ To add evaluation for a new benchmark, you need to customize a class object to implement the dataset’s metrics calculation. Multimodal datasets inherit from the `ImageBaseDataset` object in `vlmeval/dataset/image_base.py`. The TYPE defines the type of dataset, `DATASET_URL` is the download address of the dataset, and `DATASET_MD5` is the MD5 checksum for consistency checking of the dataset file.
56
+
57
+ In this class, **you need to implement** the `evaluate(eval_file, **judge_kwargs)` class function to calculate metrics and output results for the custom dataset. The function input `eval_file` is the path to the model prediction results file `{model_name}_{dataset}.xlsx`. This file can be read as a pandas.DataFrame using the `load(eval_file)` method, containing fields such as index, question, answer, category, prediction, etc. The judge_kwargs will pass a dictionary related to evaluation, such as the name of the `judge model`, the number of API request threads, etc. **The return value** of the function is the calculated accuracy and other metrics, formatted as a dictionary composed of lists, organized into a pandas.DataFrame.
58
+
59
+ ## Implement a new model
60
+
61
+ Example PR: **Support LLaVA-Next-Interleave** ([#294](https://github.com/open-compass/VLMEvalKit/pull/294))
62
+
63
+ **1. Support `generate_inner` API (mandatory).**
64
+
65
+ All existing models are implemented in `vlmeval/vlm`. For a minimal model, your model class **must implement the method** `generate_inner(msgs, dataset=None)`. In this function, you feed a multi-modal message to your VLM and return the VLM prediction (which is a string). The optional argument `dataset` can be used as the flag for the model to switch among various inference strategies.
66
+
67
+ The multi-modal messages `msgs` is a list of dictionaries, each dictionary has two keys: type and value:
68
+ - `type`: We currently support two types, choices are ["image", "text"].
69
+ - `value`: When type=='text' , the value is the text message (a single string); when type=='image', the value can be the local path of an image file, or the image URL.
70
+
71
+ Currently a multi-modal message may contain arbitrarily interleaved images and texts. If your model do not support that, a practice can be taking the 1st image and concatenated text messages as the input. You can set the `INTERLEAVE = False` in your model class and use `self.message_to_promptimg(message, dataset=dataset)` to build your prompt and the first image's path.
72
+
73
+ Here are some examples of multi-modal messages:
74
+
75
+ ```python
76
+ IMAGE_PTH = 'assets/apple.jpg'
77
+ IMAGE_URL = 'https://raw.githubusercontent.com/open-compass/VLMEvalKit/main/assets/apple.jpg'
78
+ msg1 = [
79
+ dict(type='image', value=IMAGE_PTH),
80
+ dict(type='text', value='What is in this image?')
81
+ ]
82
+ msg2 = [
83
+ dict(type='image', value=IMAGE_URL),
84
+ dict(type='image', value=IMAGE_URL),
85
+ dict(type='text', value='How many apples are there in these images?')
86
+ ]
87
+ response = model.generate(msg1)
88
+ ```
89
+
90
+ For convenience sake, we also support to take a list of string as inputs. In that case, we will check if a string is an image path or image URL and automatically convert it to the list[dict] format:
91
+
92
+ ```python
93
+ IMAGE_PTH = 'assets/apple.jpg'
94
+ IMAGE_URL = 'https://raw.githubusercontent.com/open-compass/VLMEvalKit/main/assets/apple.jpg'
95
+ msg1 = [IMAGE_PTH, 'What is in this image?']
96
+ msg2 = [IMAGE_URL, IMAGE_URL, 'How many apples are there in these images?']
97
+ response = model.generate(msg1)
98
+ ```
99
+
100
+ **Support Custom Prompt (optional).**
101
+
102
+ Besides, your model can support **custom prompt building** by implementing two optional methods: `use_custom_prompt(dataset)` and `build_prompt(line, dataset=None)`.
103
+
104
+ Both functions take the dataset name as the input:
105
+
106
+ - `use_custom_prompt(dataset)` returns a boolean flag, indicating whether the model should use the custom prompt building strategy.
107
+ - If `use_custom_prompt(dataset)` returns True, `build_prompt(line, dataset)` should return a customly bulit multimodal message for the corresponding `dataset`, given `line`, which is a dictionary that includes the necessary information of a data sample. If `use_custom_prompt(dataset)` returns False, the default prompt building strategy will be used.
108
+
109
+ **Support multi-turn chatting (optional).**
110
+
111
+ You can also support the multi-turn chatting and evaluation with your VLM by supporting the `chat_inner(message, dataset)` function. The function outputs a single string response, and the `message` is a list of chat history, following the below format.
112
+
113
+ ```python
114
+ # Assume msg1, msg2, msg3, ... are multi-modal messages following the previously described format
115
+ # `chat_inner` take the following chat history list as input:
116
+ message = [
117
+ dict(role='user', content=msg1),
118
+ dict(role='assistant', content=msg2),
119
+ dict(role='user', content=msg3),
120
+ dict(role='assistant', content=msg4),
121
+ ......
122
+ dict(role='user', content=msgn),
123
+ ]
124
+ # `message` should contain an odd number of chat utterances, the role of utterances should be interleaved "user" and "assistant", with the role of the last utterance to be "user".
125
+ # The chat function will call `chat_inner`
126
+ response = model.chat(message)
127
+ ```
128
+
129
+ ### Example PRs:
130
+
131
+ - VLM that doesn't support interleaved images and texts, and does not use custom prompts: [[Model] Support glm-4v-9b](https://github.com/open-compass/VLMEvalKit/pull/221)
132
+ - VLM that supports interleaved images and texts and custom prompts: [Add MiniCPM-Llama3-V-2.5](https://github.com/open-compass/VLMEvalKit/pull/205)
133
+ - VLM API: [Feature add glmv](https://github.com/open-compass/VLMEvalKit/pull/201)
134
+
135
+ ## Contribute to VLMEvalKit
136
+
137
+ If you want to contribute codes to **VLMEvalKit**, please do the pre-commit check before you submit a PR. That helps to keep the code tidy.
138
+
139
+ ```bash
140
+ # Under the directory of VLMEvalKit, install the pre-commit hook:
141
+ pip install pre-commit
142
+ pre-commit install
143
+ pre-commit run --all-files
144
+ # Then you can commit your code.
145
+ ```
reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/en/EvalByLMDeploy.md ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Using LMDeploy to Accelerate Evaluation and Inference
2
+
3
+ VLMEvalKit supports testing VLM models deployed by LMDeploy. Below, we use InternVL2-8B as an example to show how to test the model.
4
+
5
+ ## Step 0: Install LMDeploy
6
+
7
+ ```bash
8
+ pip install lmdeploy
9
+ ```
10
+ For other installation methods, you can refer to LMDeploy's [documentation](https://github.com/InternLM/lmdeploy).
11
+
12
+ ## Step 1: Start the Inference Service
13
+
14
+ ```bash
15
+ lmdeploy serve api_server OpenGVLab/InternVL2-8B --model-name InternVL2-8B
16
+ ```
17
+ > [!IMPORTANT]
18
+ > Since models in VLMEvalKit may have custom behaviors when building prompts for different datasets, such as InternVL2's handling of HallusionBench, it is necessary to specify `--model-name` when starting the server. This allows the VLMEvalKit to select appropriate prompt construction strategy based on the name when using the LMDeploy API.
19
+ >
20
+ > If `--server-port`, is specified, the corresponding environment variable `LMDEPLOY_API_BASE` needs to be set.
21
+
22
+
23
+ ## Step 2: Evaluation
24
+
25
+ ```bash
26
+ python run.py --data MMStar --model lmdeploy --verbose --api-nproc 64
27
+ ```
reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/en/Makefile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Minimal makefile for Sphinx documentation
2
+ #
3
+
4
+ # You can set these variables from the command line, and also
5
+ # from the environment for the first two.
6
+ SPHINXOPTS ?=
7
+ SPHINXBUILD ?= sphinx-build
8
+ SOURCEDIR = .
9
+ BUILDDIR = _build
10
+
11
+ # Put it first so that "make" without argument is like "make help".
12
+ help:
13
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14
+
15
+ .PHONY: help Makefile
16
+
17
+ # Catch-all target: route all unknown targets to Sphinx using the new
18
+ # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19
+ %: Makefile
20
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/en/Quickstart.md ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Quickstart
2
+
3
+ Before running the evaluation script, you need to **configure** the VLMs and set the model_paths properly.
4
+
5
+ After that, you can use a single script `run.py` to inference and evaluate multiple VLMs and benchmarks at a same time.
6
+
7
+ ## Step 0. Installation & Setup essential keys
8
+
9
+ **Installation.**
10
+
11
+ ```bash
12
+ git clone https://github.com/open-compass/VLMEvalKit.git
13
+ cd VLMEvalKit
14
+ pip install -e .
15
+ ```
16
+
17
+ **Setup Keys.**
18
+
19
+ To infer with API models (GPT-4v, Gemini-Pro-V, etc.) or use LLM APIs as the **judge or choice extractor**, you need to first setup API keys. VLMEvalKit will use an judge **LLM** to extract answer from the output if you set the key, otherwise it uses the **exact matching** mode (find "Yes", "No", "A", "B", "C"... in the output strings). **The exact matching can only be applied to the Yes-or-No tasks and the Multi-choice tasks.**
20
+ - You can place the required keys in `$VLMEvalKit/.env` or directly set them as the environment variable. If you choose to create a `.env` file, its content will look like:
21
+
22
+ ```bash
23
+ # The .env file, place it under $VLMEvalKit
24
+ # API Keys of Proprietary VLMs
25
+ # QwenVL APIs
26
+ DASHSCOPE_API_KEY=
27
+ # Gemini w. Google Cloud Backends
28
+ GOOGLE_API_KEY=
29
+ # OpenAI API
30
+ OPENAI_API_KEY=
31
+ OPENAI_API_BASE=
32
+ # StepAI API
33
+ STEPAI_API_KEY=
34
+ # REKA API
35
+ REKA_API_KEY=
36
+ # GLMV API
37
+ GLMV_API_KEY=
38
+ # CongRong API
39
+ CW_API_BASE=
40
+ CW_API_KEY=
41
+ # SenseNova API
42
+ SENSENOVA_API_KEY=
43
+ # Hunyuan-Vision API
44
+ HUNYUAN_SECRET_KEY=
45
+ HUNYUAN_SECRET_ID=
46
+ # LMDeploy API
47
+ LMDEPLOY_API_BASE=
48
+ # MiniMax API
49
+ MINIMAX_API_KEY=
50
+ # You can also set a proxy for calling api models during the evaluation stage
51
+ EVAL_PROXY=
52
+ ```
53
+
54
+ - Fill the blanks with your API keys (if necessary). Those API keys will be automatically loaded when doing the inference and evaluation.
55
+ ## Step 1. Configuration
56
+
57
+ **VLM Configuration**: All VLMs are configured in `vlmeval/config.py`. Few legacy VLMs (like MiniGPT-4, LLaVA-v1-7B) requires additional configuration (configuring the code / model_weight root in the config file). During evaluation, you should use the model name specified in `supported_VLM` in `vlmeval/config.py` to select the VLM. Make sure you can successfully infer with the VLM before starting the evaluation with the following command `vlmutil check {MODEL_NAME}`.
58
+
59
+ Note: For the Qwen-VL series models (Qwen-VL, Qwen2-VL, Qwen2.5-VL), the upper and lower bounds of the number of pixels specified in vlmeval/config.py are as follows:
60
+
61
+ ```
62
+ min_pixels=1280 * 28 * 28,
63
+ max_pixels=16384 * 28 * 28,
64
+ ```
65
+ Where 1280 is the maximum value recommended by Qwen for balancing performance, computational resources, and memory, and 16384 is the theoretical maximum value for model input. This setting has a positive effect on some vision tasks that require high resolution (such as document understanding). However, there is no practical basis for considering this setting. If you need to align with the official settings, you can remove these two values, or set them to the following values from the official Qwen demo:
66
+
67
+ ```
68
+ min_pixels=256 * 28 * 28,
69
+ max_pixels=1280 * 28 * 28,
70
+ ```
71
+
72
+ ## Step 2. Evaluation
73
+
74
+ **New!!!** We integrated a new config system to enable more flexible evaluation settings. Check the [Document](/docs/en/ConfigSystem.md) or run `python run.py --help` for more details 🔥🔥🔥
75
+
76
+ We use `run.py` for evaluation. To use the script, you can use `$VLMEvalKit/run.py` or create a soft-link of the script (to use the script anywhere):
77
+
78
+ **Arguments**
79
+
80
+ - `--data (list[str])`: Set the dataset names that are supported in VLMEvalKit (names can be found in the codebase README).
81
+ - `--model (list[str])`: Set the VLM names that are supported in VLMEvalKit (defined in `supported_VLM` in `vlmeval/config.py`).
82
+ - `--mode (str, default to 'all', choices are ['all', 'infer'])`: When `mode` set to "all", will perform both inference and evaluation; when set to "infer", will only perform the inference.
83
+ - `--api-nproc (int, default to 4)`: The number of threads for OpenAI API calling.
84
+ - `--work-dir (str, default to '.')`: The directory to save evaluation results.
85
+
86
+ **Command for Evaluating Image Benchmarks **
87
+
88
+ You can run the script with `python` or `torchrun`:
89
+
90
+ ```bash
91
+ # When running with `python`, only one VLM instance is instantiated, and it might use multiple GPUs (depending on its default behavior).
92
+ # That is recommended for evaluating very large VLMs (like IDEFICS-80B-Instruct).
93
+
94
+ # IDEFICS-80B-Instruct on MMBench_DEV_EN, MME, and SEEDBench_IMG, Inference and Evalution
95
+ python run.py --data MMBench_DEV_EN MME SEEDBench_IMG --model idefics_80b_instruct --verbose
96
+ # IDEFICS-80B-Instruct on MMBench_DEV_EN, MME, and SEEDBench_IMG, Inference only
97
+ python run.py --data MMBench_DEV_EN MME SEEDBench_IMG --model idefics_80b_instruct --verbose --mode infer
98
+
99
+ # When running with `torchrun`, one VLM instance is instantiated on each GPU. It can speed up the inference.
100
+ # However, that is only suitable for VLMs that consume small amounts of GPU memory.
101
+
102
+ # IDEFICS-9B-Instruct, Qwen-VL-Chat, mPLUG-Owl2 on MMBench_DEV_EN, MME, and SEEDBench_IMG. On a node with 8 GPU. Inference and Evaluation.
103
+ torchrun --nproc-per-node=8 run.py --data MMBench_DEV_EN MME SEEDBench_IMG --model idefics_80b_instruct qwen_chat mPLUG-Owl2 --verbose
104
+ # Qwen-VL-Chat on MME. On a node with 2 GPU. Inference and Evaluation.
105
+ torchrun --nproc-per-node=2 run.py --data MME --model qwen_chat --verbose
106
+ ```
107
+
108
+ **Command for Evaluating Video Benchmarks**
109
+
110
+ ```bash
111
+ # When running with `python`, only one VLM instance is instantiated, and it might use multiple GPUs (depending on its default behavior).
112
+ # That is recommended for evaluating very large VLMs (like IDEFICS-80B-Instruct).
113
+
114
+ # IDEFICS2-8B on MMBench-Video, with 8 frames as inputs and vanilla evaluation. On a node with 8 GPUs. MMBench_Video_8frame_nopack is a defined dataset setting in `vlmeval/dataset/video_dataset_config.py`.
115
+ torchrun --nproc-per-node=8 run.py --data MMBench_Video_8frame_nopack --model idefics2_8
116
+ # GPT-4o (API model) on MMBench-Video, with 1 frame per second as inputs and pack evaluation (all questions of a video in a single query).
117
+ python run.py --data MMBench_Video_1fps_pack --model GPT4o
118
+ ```
119
+
120
+ The evaluation results will be printed as logs, besides. **Result Files** will also be generated in the directory `$YOUR_WORKING_DIRECTORY/{model_name}`. Files ending with `.csv` contain the evaluated metrics.
121
+
122
+ ### Frequently Asked Questions
123
+
124
+ #### Constructing Input Prompt: The `build_prompt()` Function
125
+ If you find that the model's output does not match the expected results when evaluating a specific benchmark, it could be due to the model not constructing the input prompt correctly.
126
+
127
+ In VLMEvalKit, each `dataset` class includes a function named `build_prompt()`, which is responsible for formatting input questions. Different benchmarks can either customize their own `build_prompt()` function or use the default implementation.
128
+
129
+ For instance, when handling the default [Multiple-Choice QA](https://github.com/open-compass/VLMEvalKit/blob/43af13e052de6805a8b08cd04aed5e0d74f82ff5/vlmeval/dataset/image_mcq.py#L164), the `ImageMCQDataset.build_prompt()` method combines elements such as `hint`, `question`, and `options` (if present in the dataset) into a complete question format, as shown below:
130
+
131
+ ```
132
+ HINT
133
+ QUESTION
134
+ Options:
135
+ A. Option A
136
+ B. Option B
137
+ ···
138
+ Please select the correct answer from the options above.
139
+ ```
140
+
141
+ Additionally, since different models may have varying evaluation requirements, VLMEvalKit also supports customizing the prompt construction method at the model level through `model.build_prompt()`. For an example, you can refer to [InternVL](https://github.com/open-compass/VLMEvalKit/blob/43af13e052de6805a8b08cd04aed5e0d74f82ff5/vlmeval/vlm/internvl_chat.py#L324).
142
+
143
+ **Note: If both `model.build_prompt()` and `dataset.build_prompt()` are defined, `model.build_prompt()` will take precedence over `dataset.build_prompt()`, effectively overriding it.**
144
+
145
+ Some models, such as Qwen2VL and InternVL, define extensive prompt-building methods for various types of benchmarks. To provide more flexibility in adapting to different benchmarks, VLMEvalKit allows users to customize the `model.use_custom_prompt()` function within the model. By adding or modifying the `use_custom_prompt()` function, you can decide which benchmarks should utilize the model's custom prompt logic. Below is an example:
146
+
147
+ ```python
148
+ def use_custom_prompt(self, dataset: str) -> bool:
149
+ from vlmeval.dataset import DATASET_TYPE, DATASET_MODALITY
150
+ dataset_type = DATASET_TYPE(dataset, default=None)
151
+ if not self._use_custom_prompt:
152
+ return False
153
+ if listinstr(['MMVet'], dataset):
154
+ return True
155
+ if dataset_type == 'MCQ':
156
+ return True
157
+ if DATASET_MODALITY(dataset) == 'VIDEO':
158
+ return False
159
+ return False
160
+ ```
161
+ Only when the `use_custom_prompt()` function returns `True` will VLMEvalKit call the model's `build_prompt()` function for the current benchmark.
162
+ With this approach, you can flexibly control which benchmarks use the model's custom prompt logic based on your specific needs, thereby better adapting to different models and tasks.
163
+
164
+ #### Model Splitting
165
+
166
+ Currently, VLMEvalKit automatically supports GPU resource allocation and model splitting between processes on the same machine. This feature is supported when the inference backend is `lmdeploy` or `transformers`, with the following behaviors:
167
+
168
+ - When launching with `python` command, the model is by default allocated to all available GPUs. If you want to specify which GPUs to use, you can use `CUDA_VISIBLE_DEVICES` environment variable.
169
+ - When starting with `torchrun` command, each model instance will be allocated to `N_GPU // N_PROC` GPUs, where `N_PROC` is the number of processes specified by the `--nproc-per-node` parameter in the torchrun command. The value of `N_GPU` is determined as follows:
170
+ - If `CUDA_VISIBLE_DEVICES` environment variable is not set, `N_GPU` will be the total number of available GPUs.
171
+ - If `CUDA_VISIBLE_DEVICES` environment variable is set, `N_GPU` will be the number of GPUs specified by the `CUDA_VISIBLE_DEVICES` environment variable, and only the specified GPUs will be utilized.
172
+ Below are specific examples of running evaluation tasks on a machine equipped with 8 GPUs:
173
+
174
+ ```bash
175
+ <!-- Launch two model instances in data parallel, each instance using 4 GPUs -->
176
+ torchrun --nproc-per-node=2 run.py --data MMBench_DEV_EN --model InternVL3-78B
177
+ <!-- Launch one model instance, using all 8 GPUs -->
178
+ python run.py --data MMBench_DEV_EN --model InternVL3-78B
179
+ <!-- Launch three model instances, each instance using 2 GPUs, GPU 0 and 7 are not used -->
180
+ CUDA_VISIBLE_DEVICES=1,2,3,4,5,6 torchrun --nproc-per-node=3 run.py --data MMBench_DEV_EN --model InternVL3-38B
181
+ ```
182
+
183
+ PS: The feature is not compatible with `vllm` backend. When you evaluate a model with `vllm` backend, please use `python` to launch, and all visible GPU devices will be used.
184
+
185
+ #### Performance Discrepancies
186
+
187
+ Model performance may vary across different environments. As a result, you might observe discrepancies between your evaluation results and those listed on the official VLMEvalKit leaderboard. These differences could be attributed to variations in versions of libraries such as `transformers`, `cuda`, and `torch`.
188
+
189
+ Besides, if you encounter unexpected performance, we recommend first reviewing the local generation records (`{model}_{dataset}.xlsx`) or the evaluation records (`{model}_{dataset}_{judge_model}.xlsx`). This may help you better understand the evaluation outcomes and identify potential issues.
190
+
191
+ ## Deploy a local language model as the judge / choice extractor
192
+ The default setting mentioned above uses OpenAI's GPT as the judge LLM. However, you can also deploy a local judge LLM with [LMDeploy](https://github.com/InternLM/lmdeploy).
193
+
194
+ First install:
195
+ ```
196
+ pip install lmdeploy openai
197
+ ```
198
+
199
+ And then deploy a local judge LLM with the single line of code. LMDeploy will automatically download the model from Huggingface. Assuming we use internlm2-chat-1_8b as the judge, port 23333, and the key sk-123456 (the key must start with "sk-" and follow with any number you like):
200
+ ```
201
+ lmdeploy serve api_server internlm/internlm2-chat-1_8b --server-port 23333
202
+ ```
203
+
204
+ You need to get the model name registered by LMDeploy with the following python code:
205
+ ```
206
+ from openai import OpenAI
207
+ client = OpenAI(
208
+ api_key='sk-123456',
209
+ base_url="http://0.0.0.0:23333/v1"
210
+ )
211
+ model_name = client.models.list().data[0].id
212
+ ```
213
+
214
+ Now set some environment variables to tell VLMEvalKit how to use the local judge LLM. As mentioned above, you can also set them in `$VLMEvalKit/.env` file:
215
+ ```
216
+ OPENAI_API_KEY=sk-123456
217
+ OPENAI_API_BASE=http://0.0.0.0:23333/v1/chat/completions
218
+ LOCAL_LLM=<model_name you get>
219
+ ```
220
+
221
+ Finally, you can run the commands in step 2 to evaluate your VLM with the local judge LLM.
222
+
223
+ Note that
224
+
225
+ - If you hope to deploy the judge LLM in a single GPU and evaluate your VLM on other GPUs because of limited GPU memory, try `CUDA_VISIBLE_DEVICES=x` like
226
+ ```
227
+ CUDA_VISIBLE_DEVICES=0 lmdeploy serve api_server internlm/internlm2-chat-1_8b --server-port 23333
228
+ CUDA_VISIBLE_DEVICES=1,2,3 torchrun --nproc-per-node=3 run.py --data HallusionBench --model qwen_chat --verbose
229
+ ```
230
+ - If the local judge LLM is not good enough in following the instructions, the evaluation may fail. Please report such failures (e.g., by issues).
231
+ - It's possible to deploy the judge LLM in different ways, e.g., use a private LLM (not from HuggingFace) or use a quantized LLM. Please refer to the [LMDeploy doc](https://lmdeploy.readthedocs.io/en/latest/serving/api_server.html). You can use any other deployment framework if they support OpenAI API.
232
+
233
+
234
+ ### Using LMDeploy to Accelerate Evaluation and Inference
235
+
236
+ You can refer this [doc](/docs/en/EvalByLMDeploy.md)
reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/en/_templates/404.html ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "layout.html" %}
2
+
3
+ {% block body %}
4
+
5
+ <h1>Page Not Found</h1>
6
+ <p>
7
+ The page you are looking for cannot be found.
8
+ </p>
9
+ <p>
10
+ If you just switched documentation versions, it is likely that the page you were on is moved. You can look for it in
11
+ the content table left, or go to <a href="{{ pathto(root_doc) }}">the homepage</a>.
12
+ </p>
13
+ <!-- <p>
14
+ If you cannot find documentation you want, please <a
15
+ href="">open an issue</a> to tell us!
16
+ </p> -->
17
+
18
+ {% endblock %}
reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/en/conf.py ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # flake8: noqa
2
+ # Configuration file for the Sphinx documentation builder.
3
+ #
4
+ # This file only contains a selection of the most common options. For a full
5
+ # list see the documentation:
6
+ # https://www.sphinx-doc.org/en/master/usage/configuration.html
7
+
8
+ # -- Path setup --------------------------------------------------------------
9
+
10
+ # If extensions (or modules to document with autodoc) are in another directory,
11
+ # add these directories to sys.path here. If the directory is relative to the
12
+ # documentation root, use os.path.abspath to make it absolute, like shown here.
13
+
14
+ import ast
15
+ import os
16
+ import subprocess
17
+ import sys
18
+
19
+ import pytorch_sphinx_theme
20
+ from sphinx.builders.html import StandaloneHTMLBuilder
21
+
22
+ sys.path.insert(0, os.path.abspath('../../'))
23
+
24
+ # -- Project information -----------------------------------------------------
25
+
26
+ project = 'VLMEvalKit'
27
+ copyright = '2023, VLMEvalKit'
28
+ author = 'VLMEvalKit Authors'
29
+
30
+ # The full version, including alpha/beta/rc tags
31
+ version_file = '../../vlmeval/__init__.py'
32
+
33
+
34
+ def get_version():
35
+ with open(version_file, 'r') as f:
36
+ file_content = f.read()
37
+ # Parse the file content into an abstract syntax tree (AST)
38
+ tree = ast.parse(file_content, filename=version_file)
39
+
40
+ # Iterate through the body of the AST, looking for an assignment to __version__
41
+ for node in tree.body:
42
+ if isinstance(node, ast.Assign):
43
+ for target in node.targets:
44
+ if isinstance(target, ast.Name) and target.id == '__version__':
45
+ return node.value.s
46
+ raise ValueError('__version__ not found')
47
+
48
+
49
+ release = get_version()
50
+
51
+ # -- General configuration ---------------------------------------------------
52
+
53
+ # Add any Sphinx extension module names here, as strings. They can be
54
+ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
55
+ # ones.
56
+ extensions = [
57
+ 'sphinx.ext.autodoc',
58
+ 'sphinx.ext.autosummary',
59
+ 'sphinx.ext.intersphinx',
60
+ 'sphinx.ext.napoleon',
61
+ 'sphinx.ext.viewcode',
62
+ 'myst_parser',
63
+ 'sphinx_copybutton',
64
+ 'sphinx_tabs.tabs',
65
+ 'notfound.extension',
66
+ 'sphinxcontrib.jquery',
67
+ 'sphinx_design',
68
+ ]
69
+
70
+ # Add any paths that contain templates here, relative to this directory.
71
+ templates_path = ['_templates']
72
+
73
+ # The suffix(es) of source filenames.
74
+ # You can specify multiple suffix as a list of string:
75
+ #
76
+ source_suffix = {
77
+ '.rst': 'restructuredtext',
78
+ '.md': 'markdown',
79
+ }
80
+
81
+ language = 'en'
82
+
83
+ # The master toctree document.
84
+ root_doc = 'index'
85
+ html_context = {
86
+ 'github_version': 'latest',
87
+ }
88
+ # List of patterns, relative to source directory, that match files and
89
+ # directories to ignore when looking for source files.
90
+ # This pattern also affects html_static_path and html_extra_path.
91
+ exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
92
+
93
+ # -- Options for HTML output -------------------------------------------------
94
+
95
+ # The theme to use for HTML and HTML Help pages. See the documentation for
96
+ # a list of builtin themes.
97
+ #
98
+ html_theme = 'pytorch_sphinx_theme'
99
+ html_theme_path = [pytorch_sphinx_theme.get_html_theme_path()]
100
+
101
+ # Theme options are theme-specific and customize the look and feel of a theme
102
+ # further. For a list of options available for each theme, see the
103
+ # documentation.
104
+ # yapf: disable
105
+ html_theme_options = {
106
+ 'menu': [
107
+ {
108
+ 'name': 'GitHub',
109
+ 'url': 'https://github.com/open-compass/VLMEvalKit'
110
+ },
111
+ ],
112
+ # Specify the language of shared menu
113
+ 'menu_lang': 'en',
114
+ # Disable the default edit on GitHub
115
+ 'default_edit_on_github': False,
116
+ }
117
+ # yapf: enable
118
+
119
+ # Add any paths that contain custom static files (such as style sheets) here,
120
+ # relative to this directory. They are copied after the builtin static files,
121
+ # so a file named "default.css" will overwrite the builtin "default.css".
122
+ html_static_path = ['_static']
123
+ html_css_files = [
124
+ 'https://cdn.datatables.net/v/bs4/dt-1.12.1/datatables.min.css',
125
+ 'css/readthedocs.css'
126
+ ]
127
+ html_js_files = [
128
+ 'https://cdn.datatables.net/v/bs4/dt-1.12.1/datatables.min.js',
129
+ 'js/custom.js'
130
+ ]
131
+
132
+ # -- Options for HTMLHelp output ---------------------------------------------
133
+
134
+ # Output file base name for HTML help builder.
135
+ htmlhelp_basename = 'vlmevalkitdoc'
136
+
137
+ # -- Options for LaTeX output ------------------------------------------------
138
+
139
+ latex_elements = {
140
+ # The paper size ('letterpaper' or 'a4paper').
141
+ #
142
+ # 'papersize': 'letterpaper',
143
+
144
+ # The font size ('10pt', '11pt' or '12pt').
145
+ #
146
+ # 'pointsize': '10pt',
147
+
148
+ # Additional stuff for the LaTeX preamble.
149
+ #
150
+ # 'preamble': '',
151
+ }
152
+
153
+ # Grouping the document tree into LaTeX files. List of tuples
154
+ # (source start file, target name, title,
155
+ # author, documentclass [howto, manual, or own class]).
156
+ latex_documents = [
157
+ (root_doc, 'vlmevalkit.tex', 'VLMEvalKit Documentation', author,
158
+ 'manual'),
159
+ ]
160
+
161
+ # -- Options for manual page output ------------------------------------------
162
+
163
+ # One entry per manual page. List of tuples
164
+ # (source start file, name, description, authors, manual section).
165
+ man_pages = [(root_doc, 'vlmevalkit', 'VLMEvalKit Documentation', [author],
166
+ 1)]
167
+
168
+ # -- Options for Texinfo output ----------------------------------------------
169
+
170
+ # Grouping the document tree into Texinfo files. List of tuples
171
+ # (source start file, target name, title, author,
172
+ # dir menu entry, description, category)
173
+ texinfo_documents = [
174
+ (root_doc, 'vlmevalkit', 'VLMEvalKit Documentation', author,
175
+ 'VLMEvalKit Authors', 'AGI evaluation toolbox and benchmark.',
176
+ 'Miscellaneous'),
177
+ ]
178
+
179
+ # -- Options for Epub output -------------------------------------------------
180
+
181
+ # Bibliographic Dublin Core info.
182
+ epub_title = project
183
+
184
+ # The unique identifier of the text. This can be a ISBN number
185
+ # or the project homepage.
186
+ #
187
+ # epub_identifier = ''
188
+
189
+ # A unique identification for the text.
190
+ #
191
+ # epub_uid = ''
192
+
193
+ # A list of files that should not be packed into the epub file.
194
+ epub_exclude_files = ['search.html']
195
+
196
+ # set priority when building html
197
+ StandaloneHTMLBuilder.supported_image_types = [
198
+ 'image/svg+xml', 'image/gif', 'image/png', 'image/jpeg'
199
+ ]
200
+
201
+ # -- Extension configuration -------------------------------------------------
202
+ # Ignore >>> when copying code
203
+ copybutton_prompt_text = r'>>> |\.\.\. '
204
+ copybutton_prompt_is_regexp = True
205
+
206
+ # Auto-generated header anchors
207
+ myst_heading_anchors = 3
208
+ # Enable "colon_fence" extension of myst.
209
+ myst_enable_extensions = ['colon_fence', 'dollarmath']
210
+
211
+ # Configuration for intersphinx
212
+ intersphinx_mapping = {
213
+ 'python': ('https://docs.python.org/3', None),
214
+ 'numpy': ('https://numpy.org/doc/stable', None),
215
+ 'torch': ('https://pytorch.org/docs/stable/', None),
216
+ 'mmengine': ('https://mmengine.readthedocs.io/en/latest/', None),
217
+ 'transformers':
218
+ ('https://huggingface.co/docs/transformers/main/en/', None),
219
+ }
220
+ napoleon_custom_sections = [
221
+ # Custom sections for data elements.
222
+ ('Meta fields', 'params_style'),
223
+ ('Data fields', 'params_style'),
224
+ ]
225
+
226
+ # Disable docstring inheritance
227
+ autodoc_inherit_docstrings = False
228
+ # Mock some imports during generate API docs.
229
+ autodoc_mock_imports = ['rich', 'attr', 'einops']
230
+ # Disable displaying type annotations, these can be very verbose
231
+ autodoc_typehints = 'none'
232
+
233
+ # The not found page
234
+ notfound_template = '404.html'
reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/en/docutils.conf ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ [html writers]
2
+ table_style: colwidths-auto
reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/en/index.rst ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Welcome to the VLMEvalKit Tutorial!
2
+ ==========================================
3
+
4
+ VLMEvalKit Getting Started Guide
5
+ -------------------------------
6
+
7
+ To help users get started quickly, we recommend the following process:
8
+
9
+ - For users who want to use VLMEvalKit, we recommend reading the "Start Your First Step" section to set up the environment and start a mini-experiment to familiarize yourself with the process.
10
+
11
+ - If you want to customize more modules, such as adding datasets and models, we provide an "Advanced Tutorial."
12
+
13
+ We always welcome users' PRs (Pull Requests) and Issues to improve VLMEvalKit!
14
+
15
+ .. _Start Your First Step:
16
+ .. toctree::
17
+ :maxdepth: 1
18
+ :caption: Start Your First Step
19
+
20
+ Quickstart.md
21
+
22
+ .. _Advanced Tutorial:
23
+ .. toctree::
24
+ :maxdepth: 1
25
+ :caption: Advanced Tutorial
26
+
27
+ Development.md
28
+ ConfigSystem.md
29
+
30
+ .. _Other Notes:
31
+ .. toctree::
32
+ :maxdepth: 1
33
+ :caption: Other Notes
34
+
35
+ Contributors.md
36
+
37
+ Index and Tables
38
+ ==================
39
+
40
+ * :ref:`genindex`
41
+ * :ref:`search`
reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/ja/README_ja.md ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div align="center">
2
+
3
+ ![LOGO](http://opencompass.openxlab.space/utils/MMLB.jpg)
4
+
5
+ <b>VLMEvalKit: 大規模視覚言語モデルの評価ツールキット</b>
6
+
7
+ [![][github-contributors-shield]][github-contributors-link] • [![][github-forks-shield]][github-forks-link] • [![][github-stars-shield]][github-stars-link] • [![][github-issues-shield]][github-issues-link] • [![][github-license-shield]][github-license-link]
8
+
9
+ [English](/README.md) | [简体中文](/docs/zh-CN/README_zh-CN.md) | 日本語
10
+
11
+ <a href="https://rank.opencompass.org.cn/leaderboard-multimodal">🏆 OpenCompass Learderboard </a> •
12
+ <a href="#-datasets-models-and-evaluation-results">📊Datasets & Models </a> •
13
+ <a href="#%EF%B8%8F-quickstart">🏗️Quickstart </a> •
14
+ <a href="#%EF%B8%8F-development-guide">🛠️Development </a> •
15
+ <a href="#-the-goal-of-vlmevalkit">🎯Goal </a> •
16
+ <a href="#%EF%B8%8F-citation">🖊️Citation </a>
17
+
18
+ <a href="https://huggingface.co/spaces/opencompass/open_vlm_leaderboard">🤗 HF Leaderboard</a> •
19
+ <a href="https://huggingface.co/datasets/VLMEval/OpenVLMRecords">🤗 Evaluation Records</a> •
20
+ <a href="https://discord.gg/evDT4GZmxN">🔊 Discord Channel</a> •
21
+ <a href="https://www.arxiv.org/abs/2407.11691">📝 Technical Report</a>
22
+ </div>
23
+
24
+ **VLMEvalKit**(pythonパッケージ名は**vlmeval**)は、**大規模視覚言語モデル(LVLMs)**の**オープンソース評価ツールキット**です。このツールキットは、複数のリポジトリでのデータ準備という重労働なしに、さまざまなベンチマークでLVLMsの**ワンコマンド評価**を可能にします。VLMEvalKitでは、すべてのLVLMsに対して**生成ベースの評価**を採用し、**正確なマッチング**と**LLMベースの回答抽出**の両方で得られた評価結果を提供します。
25
+
26
+ PS: 日本語の README には最新のアップデートがすべて含まれていない場合があります。英語版をご確認ください。
27
+
28
+ ## 📊 データセット、モデル、および評価結果
29
+
30
+ **公式のマルチモーダルリーダーボードでのパフォーマンス数値は、ここからダウンロードできます!**
31
+
32
+ [**OpenVLM Leaderboard**](https://huggingface.co/spaces/opencompass/open_vlm_leaderboard): [すべての詳細な結果をダウンロード](http://opencompass.openxlab.space/assets/OpenVLM.json)。
33
+
34
+ **Supported Benchmarks** in [**VLMEvalKit Features**](https://aicarrier.feishu.cn/wiki/Qp7wwSzQ9iK1Y6kNUJVcr6zTnPe?table=tblsdEpLieDoCxtb) を確認して、すべてのサポートされているベンチマーク(70以上)を表示してください。
35
+
36
+ **Supported LMMs** in [**VLMEvalKit Features**](https://aicarrier.feishu.cn/wiki/Qp7wwSzQ9iK1Y6kNUJVcr6zTnPe?table=tblsdEpLieDoCxtb) を確認して、すべてのサポートされている LMMs(200以上)を表示してください。
37
+
38
+ **Transformersバージョンの推奨事項:**
39
+
40
+ 特定のtransformerバージョンで一部のVLMが実行できない可能性があることに注意してください。各VLMを評価するために、以下の設定を推奨します:
41
+
42
+ - **`transformers==4.33.0`を使用してください**: `Qwenシリーズ`, `Monkeyシリーズ`, `InternLM-XComposerシリーズ`, `mPLUG-Owl2`, `OpenFlamingo v2`, `IDEFICSシリーズ`, `VisualGLM`, `MMAlaya`, `ShareCaptioner`, `MiniGPT-4シリーズ`, `InstructBLIPシリーズ`, `PandaGPT`, `VXVERSE`, `GLM-4v-9B`.
43
+ - **`transformers==4.37.0`を使用してください**: `LLaVAシリーズ`, `ShareGPT4Vシリーズ`, `TransCore-M`, `LLaVA (XTuner)`, `CogVLMシリーズ`, `EMU2シリーズ`, `Yi-VLシリーズ`, `MiniCPM-[V1/V2]`, `OmniLMM-12B`, `DeepSeek-VLシリーズ`, `InternVLシリーズ`, `Cambrianシリーズ`, `VILA-VLシリーズ`.
44
+ - **`transformers==4.40.0`を使用してください**: `IDEFICS2`, `Bunny-Llama3`, `MiniCPM-Llama3-V2.5`, `360VL-70B`, `Phi-3-Vision`, `WeMM`.
45
+ - **`transformers==4.42.0`を使用してください**: `AKI`.
46
+ - **`transformers==latest`を使用してください**: `LLaVA-Nextシリーズ`, `PaliGemma-3B`, `Chameleon-VLシリーズ`, `Video-LLaVA-7B-HF`, `Ovis1.5シリーズ`, `Mantisシリーズ`, `MiniCPM-V2.6`.
47
+
48
+ ```python
49
+ # デモ
50
+ from vlmeval.config import supported_VLM
51
+ model = supported_VLM['idefics_9b_instruct']()
52
+ # 単一画像のフォワード
53
+ ret = model.generate(['assets/apple.jpg', 'この画像には何がありますか?'])
54
+ print(ret) # この画像には葉がついた赤いリンゴがあります。
55
+ # 複数画像のフォワード
56
+ ret = model.generate(['assets/apple.jpg', 'assets/apple.jpg', '提供された画像にはリンゴが何個ありますか?'])
57
+ print(ret) # 提供された画像にはリンゴが2個あります。
58
+ ```
59
+
60
+ ## 🏗️ クイックスタート
61
+
62
+ クイックスタートガイドについては、[クイックスタート](/docs/en/Quickstart.md)を参照してください。
63
+
64
+ ## 🛠️ 開発ガイド
65
+
66
+ カスタムベンチマーク、VLMsを開発するか���単に**VLMEvalKit**に他のコードを貢献する場合は、[開発ガイド](/docs/en/Development.md)を参照してください。
67
+
68
+ コミュニティからの共有を奨励し、それに応じたクレジットを共有するために、次回のレポート更新では以下のことを実施します:
69
+
70
+ - 全ての貢献に対して感謝の意を示します
71
+ - 新しいモデル、評価セット、または主要な機能への3つ以上の主要な貢献を持つ貢献者は、テクニカルレポートの著者リストに加わることができます。適格な貢献者は、issueを作成するか、または[VLM評価キット ディスコードチャンネル](https://discord.com/invite/evDT4GZmxN)で kennyutc にDMを送ることができます。私たちはそれに応じてフォローアップします。
72
+
73
+ ## 🎯 VLMEvalKitの目標
74
+
75
+ **このコードベースは以下を目的として設計されています:**
76
+
77
+ 1. 研究者や開発者が既存のLVLMsを評価し、評価結果を**簡単に再現できるようにする**ための**使いやすい**、**オープンソースの評価ツールキット**を提供します。
78
+ 2. VLMの開発者が自分のモデルを簡単に評価できるようにします。複数のサポートされているベンチマークでVLMを評価するには、単一の`generate_inner()`関数を**実装するだけで**、他のすべてのワークロード(データのダウンロード、データの前処理、予測の推論、メトリックの計算)はコードベースによって処理されます。
79
+
80
+ **このコードベースは以下を目的として設計されていません:**
81
+
82
+ 1. すべての**第三者ベンチマーク**の元の論文で報告された正確な精度数値を再現すること。その理由は2つあります:
83
+ 1. VLMEvalKitは、すべてのVLMに対して**生成ベースの評価**を使用します(オプションで**LLMベースの回答抽出**を使用)。一方、一部のベンチマークは異なるアプローチを使用する場合があります(SEEDBenchはPPLベースの評価を使用します)。これらのベンチマークについては、対応する結果で両方のスコアを比較します。開発者には、コードベースで他の評価パラダイムをサポートすることをお勧めします。
84
+ 2. デフォルトでは、すべてのVLMに対して同じプロンプトテンプレートを使用してベンチマークを評価します。一方、**一部のVLMには特定のプロンプトテンプレートがある**場合があります(現時点ではコードベースでカバーされていない場合があります)。VLMの開発者には、現在カバーされていない場合でも、VLMEvalKitで独自のプロンプトテンプレートを実装することをお勧めします。これにより、再現性が向上します。
85
+
86
+ ## 🖊️ 引用
87
+
88
+ この作業が役立つ場合は、このリポジトリに**スター🌟**を付けてください。サポートありがとうございます!
89
+
90
+ [![Stargazers repo roster for @open-compass/VLMEvalKit](https://reporoster.com/stars/open-compass/VLMEvalKit)](https://github.com/open-compass/VLMEvalKit/stargazers)
91
+
92
+ 研究でVLMEvalKitを使用する場合、または公開されたオープンソースの評価結果を参照する場合は、以下のBibTeXエントリと、使用した特定のVLM/ベンチマークに対応するBibTexエントリを使用してください。
93
+
94
+ ```bib
95
+ @misc{duan2024vlmevalkit,
96
+ title={VLMEvalKit: An Open-Source Toolkit for Evaluating Large Multi-Modality Models},
97
+ author={Haodong Duan and Junming Yang and Yuxuan Qiao and Xinyu Fang and Lin Chen and Yuan Liu and Xiaoyi Dong and Yuhang Zang and Pan Zhang and Jiaqi Wang and Dahua Lin and Kai Chen},
98
+ year={2024},
99
+ eprint={2407.11691},
100
+ archivePrefix={arXiv},
101
+ primaryClass={cs.CV},
102
+ url={https://arxiv.org/abs/2407.11691},
103
+ }
104
+ ```
105
+
106
+ <p align="right"><a href="#top">🔝Top に戻る</a></p>
107
+
108
+ [github-contributors-link]: https://github.com/open-compass/VLMEvalKit/graphs/contributors
109
+ [github-contributors-shield]: https://img.shields.io/github/contributors/open-compass/VLMEvalKit?color=c4f042&labelColor=black&style=flat-square
110
+ [github-forks-link]: https://github.com/open-compass/VLMEvalKit/network/members
111
+ [github-forks-shield]: https://img.shields.io/github/forks/open-compass/VLMEvalKit?color=8ae8ff&labelColor=black&style=flat-square
112
+ [github-issues-link]: https://github.com/open-compass/VLMEvalKit/issues
113
+ [github-issues-shield]: https://img.shields.io/github/issues/open-compass/VLMEvalKit?color=ff80eb&labelColor=black&style=flat-square
114
+ [github-license-link]: https://github.com/open-compass/VLMEvalKit/blob/main/LICENSE
115
+ [github-license-shield]: https://img.shields.io/github/license/open-compass/VLMEvalKit?color=white&labelColor=black&style=flat-square
116
+ [github-stars-link]: https://github.com/open-compass/VLMEvalKit/stargazers
117
+ [github-stars-shield]: https://img.shields.io/github/stars/open-compass/VLMEvalKit?color=ffcb47&labelColor=black&style=flat-square
reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/.readthedocs.yaml ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: 2
2
+
3
+ # Set the version of Python and other tools you might need
4
+ build:
5
+ os: ubuntu-22.04
6
+ tools:
7
+ python: "3.8"
8
+
9
+ formats:
10
+ - epub
11
+
12
+ sphinx:
13
+ configuration: docs/zh-CN/conf.py
14
+
15
+ python:
16
+ install:
17
+ - requirements: requirements/docs.txt
reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/ConfigSystem.md ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # 配置系统
3
+
4
+ 默认情况下,VLMEvalKit通过在`run.py`脚本中使用`--model`和`--data`参数设置模型名称(在`/vlmeval/config.py`中定义)和数据集名称(在`vlmeval/dataset/__init__.py` 或 `vlmeval/dataset/video_dataset_config.py` 中定义)来启动评估。这种方法在大多数情况下简单且高效,但当用户希望使用不同设置评估多个模型/数据集时,可能不够灵活。
5
+
6
+ 为了解决这个问题,VLMEvalKit提供了一个更灵活的配置系统。用户可以在json文件中指定模型和数据集设置,并通过`--config`参数将配置文件的路径传递给`run.py`脚本。以下是一个示例配置json:
7
+
8
+ ```json
9
+ {
10
+ "model": {
11
+ "GPT4o_20240806_T00_HIGH": {
12
+ "class": "GPT4V",
13
+ "model": "gpt-4o-2024-08-06",
14
+ "temperature": 0,
15
+ "img_detail": "high"
16
+ },
17
+ "GPT4o_20240806_T10_Low": {
18
+ "class": "GPT4V",
19
+ "model": "gpt-4o-2024-08-06",
20
+ "temperature": 1.0,
21
+ "img_detail": "low"
22
+ },
23
+ "GPT4o_20241120": {}
24
+ },
25
+ "data": {
26
+ "MME-RealWorld-Lite": {
27
+ "class": "MMERealWorld",
28
+ "dataset": "MME-RealWorld-Lite"
29
+ },
30
+ "MMBench_DEV_EN_V11": {
31
+ "class": "ImageMCQDataset",
32
+ "dataset": "MMBench_DEV_EN_V11"
33
+ },
34
+ "MMBench_Video_8frame_nopack":{},
35
+ "Video-MME_16frame_subs": {
36
+ "class": "VideoMME",
37
+ "dataset": "Video-MME",
38
+ "nframe": 16,
39
+ "use_subtitle": true
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ 配置json的解释:
46
+
47
+ 1. 现在我们支持两个字段:`model`和`data`,每个字段都是一个字典。字典的键是模型/数据集的名称(由用户设置),值是模型/数据集的设置。
48
+ 2. 对于`model`中的项目,值是一个包含以下键的字典:
49
+ - `class`:模型的类名,应该是`vlmeval/vlm/__init__.py`(开源模型)或`vlmeval/api/__init__.py`(API模型)中定义的类名。
50
+ - 其他kwargs:其他kwargs是模型特定的参数,请参考模型类的定义以获取详细用法。例如,`model`、`temperature`、`img_detail`是`GPT4V`类的参数。值得注意的是,大多数模型类都需要`model`参数。
51
+ - Tip:在位于`vlmeval/config.py`的变量`supported_VLM`中的已经被定义的模型可以作为`model`的键,而不需要填对应的值即可启动。例如,`GPT4o_20240806_T00_HIGH: {}`是等价于`GPT4o_20240806_T00_HIGH: {'class': 'GPT4V', 'model': 'gpt-4o-2024-08-06', 'temperature': 0, 'img_size': -1, 'img_detail': 'high', 'retry': 10, 'verbose': False}`。
52
+ 3. 对于字典`data`,我们建议用户使用官方数据集名称作为键(或键的一部分),因为我们经常根据数据集名称确定后处理/判断设置。对于`data`中的项目,值是一个包含以下键的字典:
53
+ - `class`:数据集的类名,应该是`vlmeval/dataset/__init__.py`中定义的类名。
54
+ - 其他kwargs:其他kwargs是数据集特定的参数,请参考数据集类的定义以获取详细用法。通常,大多数数据集类都需要`dataset`参数。大多数视频数据集类都需要 `nframe` 或 `fps` 参数。
55
+ - Tip:在位于`vlmeval/dataset/video_dataset_config.py`的变量`supported_video_dataset`中的已经被定义的数据集可以作为`data`的键,而不需要填对应的值即可启动。例如,`MMBench_Video_8frame_nopack: {}`是等价于`MMBench_Video_8frame_nopack: {'class': 'MMBenchVideo', 'dataset': 'MMBench-Video', 'nframe': 8, 'pack': False}`。
56
+
57
+ 将示例配置json保存为`config.json`,您可以通过以下命令启动评估:
58
+
59
+ ```bash
60
+ python run.py --config config.json
61
+ ```
62
+
63
+ 这将在工作目录`$WORK_DIR`下生成以下输出文件(格式为`{$WORK_DIR}/{$MODEL_NAME}/{$MODEL_NAME}_{$DATASET_NAME}_*`):
64
+
65
+ - `$WORK_DIR/GPT4o_20240806_T00_HIGH/GPT4o_20240806_T00_HIGH_MME-RealWorld-Lite*`
66
+ - `$WORK_DIR/GPT4o_20240806_T10_Low/GPT4o_20240806_T10_Low_MME-RealWorld-Lite*`
67
+ - `$WORK_DIR/GPT4o_20240806_T00_HIGH/GPT4o_20240806_T00_HIGH_MMBench_DEV_EN_V11*`
68
+ - `$WORK_DIR/GPT4o_20240806_T10_Low/GPT4o_20240806_T10_Low_MMBench_DEV_EN_V11*`
69
+ ......
reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/Development.md ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🛠️ 如何在 VLMEvalKit 中实现一个新的 Benchmark 或多模态模型(VLM)
2
+
3
+ ## 实现一个新的 benchmark
4
+
5
+ 示例 PR: **添加 Math-Vision Benchmark** ([#292](https://github.com/open-compass/VLMEvalKit/pull/292/files))
6
+
7
+ 目前在 VLMEvalKit 中,benchmark 以数据集类的形式呈现,当你新增一个 benchmark 时,你可以选择复用现有的数据集类 (如单选题 benchmark 可复用 `ImageMCQDataset`),或是实现新的数据集类。你的数据集类必须支持以下两种方法 (复用父类或自行实现):
8
+
9
+ - `build_prompt(self, line)`: 方法输入 `line` 类型为 int (对应数据 index) 或 `pd.Series` (对应数据原始 record)。方法输出一条 `multi-modal message` 作为多模态模型输入,`multi-modal message` 是一个图文交错的列表,如以下格式 (一图一文): `[dict(type='image', value=IMAGE_PTH), dict(type='text', value=prompt)]`。
10
+ - `evaluate(self, eval_file, **judge_kwargs)`: 方法输入 `eval_file` 为多模态模型的预测结果 (多以 `.xlsx` 格式存在),如 benchmark evaluation 需要大语言模型 (一般为 GPT) 辅助,则 `judge_kwargs` 传入大语言模型的参数。方法输出 benchmark 的评测结果,以 `dict` 或 `pd.DataFrame` 的形式。
11
+
12
+ 以下,我们简述新增数据集的通常步骤:
13
+
14
+ ### 1. TSV 数据文件准备 (图文评测集)
15
+
16
+ 目前,我们将每一个 benchmark 数据集设置为一个单独的 TSV 文件。在推理过程中,数据文件将从数据集定义的 `DATASET_URL` 链接地址自动下载到 `$LMUData` 中(如果没有明确设置的话,默认路径是 `$HOME/LMUData`)。你可以将准备好的 TSV 文件上传到一个可下载的地址(如:huggingface),或发送给我们 <opencompass@pjlab.org.cn>,我们将帮助上传数据集到服务器中。此外,你也可以在环境变量中自定义设置下载路径 `LMUData=/path/to/your/data`。
17
+
18
+ TSV 文件中的内容组成为:
19
+
20
+ | 数据集名称 \ 字段 | index | image | image_path | question | hint | multi-choice<br>options | answer | category | l2-category | split |
21
+ | ---------------------- | ----- | ----- | ---------- | -------- | ---- | ----------------------- | ------ | -------- | ----------- | ----- |
22
+ | MMBench_DEV_[CN/EN] | ✅ | ✅ | | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
23
+ | MMBench_TEST_[CN/EN] | ✅ | ✅ | | ✅ | ✅ | ✅ | | ✅ | ✅ | ✅ |
24
+ | CCBench | ✅ | ✅ | | ✅ | | ✅ | ✅ | ✅ | | |
25
+ | SEEDBench_IMG | ✅ | ✅ | | ✅ | | ✅ | ✅ | ✅ | | |
26
+ | MME | ✅ | ✅ | | ✅ | | | ✅ | ✅ | | |
27
+ | MMVet | ✅ | ✅ | | ✅ | | | ✅ | ✅ | | |
28
+ | MMMU_DEV_VAL | ✅ | ✅ | ✅ | ✅ | | ✅ | ✅ | ✅ | ✅ | ✅ |
29
+ | COCO_VAL | ✅ | ✅ | | | | | ✅ | | | |
30
+ | OCRVQA_[TEST/TESTCORE] | ✅ | ✅ | | ✅ | | | ✅ | | | |
31
+ | TextVQA_VAL | ✅ | ✅ | | ✅ | | | ✅ | | | |
32
+ | VCR_[EN/ZH]\_[EASY/HARD]_[ALL/500/100] | ✅ | ✅ | | ✅ | | | ✅ | | | |
33
+
34
+ <div align="center"><b>表 1. 支持的数据集的 TSV 字段。</b></div>
35
+
36
+ **TSV 中必须字段的介绍:**
37
+
38
+ - **index:** 一个整数,`tsv` 中每一行的唯一标识
39
+ - **image:** 图片的 base64 编码,你可以使用 `vlmeval/smp/vlm.py` 中实现的API进行编码和解码:
40
+ - 编码:`encode_image_to_base64`(对于PIL Image)/ `encode_image_file_to_base64`(对于图片文件路径)
41
+ - 解码:`decode_base64_to_image`(对于PIL Image)/ `decode_base64_to_image_file`(对于图片文件路径)
42
+ - **question:** 针对图像所提取出的问题,类型为字符串
43
+ - **answer:** 问题的答案,类型为字符串,Test 集可缺失这一字段
44
+
45
+ ### 2. 自定义数据集的 prompt 构建
46
+
47
+ `ImageBaseDataset` 定义了默认的 prompt 格式。如果需要针对数据集添加 prompt,或给模型输入 `Interleave` 的数据格式,可以通过 `build_prompt(line)` 函数实现。该函数输入为,每次给定 TSV 文件中的一行,包含 index, image, question 等内容作为 line。该函数将返回一个多模态消息 `msg` 的字典列表 `[dict(type='image', value=IMAGE_PTH), dict(type='text', value=prompt)]`,包括图片路径和将被输入到 VLMs 的文本 prompt。对于 interleave 类型输入,可以直接将图片路径的字典放置到 image token 位置。
48
+
49
+ ### 3. 自定义数据集的指标实现
50
+
51
+ 增加对 benchmark 的评测需要自定义一个该数据集的 class 对象,从而实现数据集的指标计算。图文多模态数据集均继承自 `vlmeval/dataset/image_base.py` 中的 `ImageBaseDataset` 对象。其中 `TYPE` 定义了数据集的类型;`DATASET_URL` 为数据集的下载地址;`DATASET_MD5` 为数据集文件的 md5 一致性编码检查。
52
+
53
+ 在 class 中**需要实现** `evaluate(eval_file, **judge_kwargs)` 类函数,对自定义的数据集结果进行指标计算和结果输出。函数输入 `eval_file` 为模型预测结果 `{model_name}_{dataset}.xlsx` 的路径。可以通过 `load(eval_file)` 文件将其读取为 panda.DataFrames 类型,其中包含 index, question, answer, category, prediction 等字段。`judge_kwargs` 参数将传递一个评测相关的字典,如:judge 模型的名称,api 请求线程数等。**函数的返回值**为评估完成的准确度等指标,其格式为由 list 组成的字典,并组织成 panda.DataFrames 类型。
54
+
55
+ ## 实现一个新的模型
56
+
57
+ 示例 PR: **支持 LLaVA-Next-Interleave** ([#294](https://github.com/open-compass/VLMEvalKit/pull/294))
58
+
59
+ **1. 支持 `generate_inner` API (必须)**
60
+
61
+ 现有所有的模型都在 `vlmeval/vlm` 中实现。对于一个最基本的模型,你的模型类**应该实现方法** `generate_inner(msgs, dataset=None)`。这个函数将向 VLM 输入一个多模态数据,并返回 VLM 的预测(一个字符串)。可选参数 `dataset` 可以用作模型在不同推理策略之间切换的标志。
62
+
63
+ 其中多模态消息 `msgs` 是一个字典列表,每个字典有两个键:类型和值:
64
+ - `type`:我们目前支持两种类型,选项是 ["image", "text"]。
65
+ - `value`:当类型为 `text` 时,值是文本消息(一个字符串);当类型为 `image` 时,值可以是图像文件的本地路径,或者是图像的URL。
66
+
67
+ > 目前,一个多模态消息可能包含任意交错的图像和文本。如果你的模型不支持这一点,我们推荐的做法是取第一张图像和连接的文本消息作为模型的输入。你可以在模型的 class 中设置 `INTERLEAVE = False` 并调用 `self.message_to_promptimg(message, dataset=dataset)` 函数来获取你的 prompt 和第一张图片的地址。
68
+
69
+ 一些多模态消息的例子:
70
+
71
+ ```python
72
+ IMAGE_PTH = 'assets/apple.jpg'
73
+ IMAGE_URL = 'https://raw.githubusercontent.com/open-compass/VLMEvalKit/main/assets/apple.jpg'
74
+ msg1 = [
75
+ dict(type='image', value=IMAGE_PTH),
76
+ dict(type='text', value='What is in this image?')
77
+ ]
78
+ msg2 = [
79
+ dict(type='image', value=IMAGE_URL),
80
+ dict(type='image', value=IMAGE_URL),
81
+ dict(type='text', value='How many apples are there in these images?')
82
+ ]
83
+ response = model.generate(msg1)
84
+ ```
85
+
86
+ 为了方便起见,我们还支持接受字符串列表作为输入。在这种情况下,我们将检查一个字符串是图像路径还是图像 URL,并自动将其转换为 `list[dict]` 格式:
87
+
88
+ ```python
89
+ IMAGE_PTH = 'assets/apple.jpg'
90
+ IMAGE_URL = 'https://raw.githubusercontent.com/open-compass/VLMEvalKit/main/assets/apple.jpg'
91
+ msg1 = [IMAGE_PTH, 'What is in this image?']
92
+ msg2 = [IMAGE_URL, IMAGE_URL, 'How many apples are there in these images?']
93
+ response = model.generate(msg1)
94
+ ```
95
+
96
+ **2. 支持自定义提示词构建 (可选)**
97
+
98
+ 此外,你的模型可以通过实现两个可选方法来支持自定义提示构建:`use_custom_prompt(dataset)` 和 `build_prompt(line, dataset=None)`。
99
+
100
+ - `use_custom_prompt(dataset)` 将返回一个布尔值,指示模型是否应使用自定义提示构建策略。
101
+ - 如果`use_custom_prompt(dataset)`返回 True,`build_prompt(line, dataset)` 应该为相应的数据集返回一个自定义构建的多模态消息,line 数据是一个包含数据样本所需信息的字典。如果`use_custom_prompt(dataset)` 返回False,则将使用默认的 prompt 构建策略。
102
+
103
+ **3. 支持多轮对话 (可选)**
104
+
105
+ 你可以通过支持 `chat_inner(message, dataset)` API 为你的模型新增多轮对话功能并兼容多轮对话评测。这个 API 输出一个字符串型回复,`message` 包含一个聊天记录的列表,格式如下:
106
+
107
+ ```python
108
+ # Assume msg1, msg2, msg3, ... are multi-modal messages following the previously described format
109
+ # `chat_inner` take the following chat history list as input:
110
+ message = [
111
+ dict(role='user', content=msg1),
112
+ dict(role='assistant', content=msg2),
113
+ dict(role='user', content=msg3),
114
+ dict(role='assistant', content=msg4),
115
+ ......
116
+ dict(role='user', content=msgn),
117
+ ]
118
+ # `message` should contain an odd number of chat utterances, the role of utterances should be interleaved "user" and "assistant", with the role of the last utterance to be "user".
119
+ # The chat function will call `chat_inner`
120
+ response = model.chat(message)
121
+ ```
122
+
123
+ ### 示例 PRs:
124
+
125
+ - 不支持交错的图像和文本,且不使用自定义提示的VLM:[[模型] 支持 glm-4v-9b](https://github.com/open-compass/VLMEvalKit/pull/221)
126
+ - 支持交错的图像和文本及自定义提示的VLM:[添加 MiniCPM-Llama3-V-2.5](https://github.com/open-compass/VLMEvalKit/pull/205)
127
+ - VLM API:[特征添加 glmv](https://github.com/open-compass/VLMEvalKit/pull/201)
128
+
129
+ ## 为 VLMEvalKit 贡献代码
130
+
131
+ 如果你想为 **VLMEvalKit** 贡献代码,请在提交PR之前进行预提交检查。这有助于保持代码整洁。
132
+
133
+ ```bash
134
+ # 在VLMEvalKit的目录下,安装预提交 hook:
135
+ pip install pre-commit
136
+ pre-commit install
137
+ pre-commit run --all-files
138
+ # 然后提交你的代码。
139
+ ```
reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/EvalByLMDeploy.md ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 使用 LMDeploy 加速评测推理
2
+
3
+ VLMEvalKit 支持测试由 LMDeploy 部署的 VLM 模型,下面以 InternVL2-8B 为例,展示如何测试模型
4
+
5
+ ## 第0步 安装 LMDeploy
6
+
7
+ ```bash
8
+ pip install lmdeploy
9
+ ```
10
+
11
+ 其他安装方式可以参考 LMDeploy 的[文档](https://github.com/InternLM/lmdeploy)
12
+
13
+ ## 第1步 启动推理服务
14
+
15
+ ```bash
16
+ lmdeploy serve api_server OpenGVLab/InternVL2-8B --model-name InternVL2-8B
17
+ ```
18
+ > [!IMPORTANT]
19
+ > 因为 VLMEvalKit 中的模型对于不同数据集在构建 prompt 时可能有自定义行为,如 InternVL2 对于 HallusionBench 的处理,所以,server 端在启动的时候需要指定 `--model-name`,这样在使用 LMDEploy api 时可以根据名字选择合适的 prompt 构建策略。
20
+ >
21
+ > 如果指定了 `--server-port`,需要设置对应的环境变量 `LMDEPLOY_API_BASE`
22
+
23
+
24
+ ## 第2步 评测
25
+
26
+ ```bash
27
+ python run.py --data MMStar --model InternVL2-8B --verbose --api-nproc 64
28
+ ```
reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/Makefile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Minimal makefile for Sphinx documentation
2
+ #
3
+
4
+ # You can set these variables from the command line, and also
5
+ # from the environment for the first two.
6
+ SPHINXOPTS ?=
7
+ SPHINXBUILD ?= sphinx-build
8
+ SOURCEDIR = .
9
+ BUILDDIR = _build
10
+
11
+ # Put it first so that "make" without argument is like "make help".
12
+ help:
13
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14
+
15
+ .PHONY: help Makefile
16
+
17
+ # Catch-all target: route all unknown targets to Sphinx using the new
18
+ # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19
+ %: Makefile
20
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/Quickstart.md ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 快速开始
2
+
3
+ 在运行评测脚本之前,你需要先**配置** VLMs,并正确设置模型路径。然后你可以使用脚本 `run.py` 进行多个VLMs和基准测试的推理和评估。
4
+
5
+ ## 第0步 安装和设置必要的密钥
6
+
7
+ **安装**
8
+
9
+ ```bash
10
+ git clone https://github.com/open-compass/VLMEvalKit.git
11
+ cd VLMEvalKit
12
+ pip install -e .
13
+ ```
14
+
15
+ **设置密钥**
16
+
17
+ 要使用 API 模型(如 GPT-4v, Gemini-Pro-V 等)进行推理,或使用 LLM API 作为**评判者或选择提取器**,你需要首先设置 API 密钥。如果你设置了密钥,VLMEvalKit 将使用一个评判 LLM 从输出中提取答案,否则它将使用**精确匹配模式**(在输出字符串中查找 "Yes", "No", "A", "B", "C"...)。**精确匹配模式只能应用于是或否任务和多项选择任务。**
18
+
19
+ - 你可以将所需的密钥放在 `$VLMEvalKit/.env` 中,或直接将它们设置为环境变量。如果你选择创建 `.env` 文件,其内容将如下所示:
20
+
21
+ ```bash
22
+ # .env 文件,将其放置在 $VLMEvalKit 下
23
+ # 专有 VLMs 的 API 密钥
24
+ # QwenVL APIs
25
+ DASHSCOPE_API_KEY=
26
+ # Gemini w. Google Cloud Backends
27
+ GOOGLE_API_KEY=
28
+ # OpenAI API
29
+ OPENAI_API_KEY=
30
+ OPENAI_API_BASE=
31
+ # StepAI API
32
+ STEPAI_API_KEY=
33
+ # REKA API
34
+ REKA_API_KEY=
35
+ # GLMV API
36
+ GLMV_API_KEY=
37
+ # CongRong API
38
+ CW_API_BASE=
39
+ CW_API_KEY=
40
+ # SenseNova API
41
+ SENSENOVA_API_KEY=
42
+ # Hunyuan-Vision API
43
+ HUNYUAN_SECRET_KEY=
44
+ HUNYUAN_SECRET_ID=
45
+ # LMDeploy API
46
+ LMDEPLOY_API_BASE=
47
+ # MiniMax API
48
+ MINIMAX_API_KEY=
49
+ # 你可以设置一个评估时代理,评估阶段产生的 API 调用将通过这个代理进行
50
+ EVAL_PROXY=
51
+ ```
52
+
53
+ - 如果需要使用 API 在对应键值空白处填写上你的密钥。这些 API 密钥将在进行推理和评估时自动加载。
54
+ ## 第1步 配置
55
+
56
+ **VLM 配置**:所有 VLMs 都在 `vlmeval/config.py` 中配置。对于某些 VLMs(如 MiniGPT-4、LLaVA-v1-7B),需要额外的配置(在配置文件中配置代码 / 模型权重根目录)。在评估时,你应该使用 `vlmeval/config.py` 中 `supported_VLM` 指定的模型名称来选择 VLM。确保在开始评估之前,你可以成功使用 VLM 进行推理,使用以下命令 `vlmutil check {MODEL_NAME}`。
57
+
58
+ 注:对于Qwen-VL系列模型(Qwen-VL, Qwen2-VL, Qwen2.5-VL),vlmeval/config.py 中所指定的像素数量上下界如下:
59
+
60
+ ```
61
+ min_pixels=1280 * 28 * 28,
62
+ max_pixels=16384 * 28 * 28,
63
+ ```
64
+ 其中,1280为Qwen官方为平衡性能、计算资源与内存的推荐最大值,而16384为模型输入的理论最大值。这种设定对于部分需要高分辨率的视觉任务(如文档理解)有着积极的作用。但考虑这一设定并没有实际的依据,如果需要与官方的设定对齐,可以去掉这两个数值,或是设置为以下来自Qwen官方demo的数值:
65
+
66
+ ```
67
+ min_pixels=256 * 28 * 28,
68
+ max_pixels=1280 * 28 * 28,
69
+ ```
70
+
71
+ ## 第2步 评测
72
+
73
+ **新功能!!!** 我们集成了一个新的配置系统,以实现更灵活的评估设置。查看[文档](/docs/zh-CN/ConfigSystem.md)或运行`python run.py --help`了解更多详情 🔥🔥🔥
74
+
75
+ 我们使用 `run.py` 进行评估。你可以使用 `$VLMEvalKit/run.py` 或创建脚本的软链接运行(以便在任何地方使用该脚本):
76
+
77
+ **参数**
78
+
79
+ - `--data (list[str])`: 设置在 VLMEvalKit 中支持的数据集名称(可以在代码库首页的 README 中找到支持的数据集列表)
80
+ - `--model (list[str])`: 设置在 VLMEvalKit 中支持的 VLM 名称(在 `vlmeval/config.py` 中的 `supported_VLM` 中定义)
81
+ - `--mode (str, 默认值为 'all', 可选值为 ['all', 'infer'])`:当 mode 设置为 "all" 时,将执行推理和评估;当设置为 "infer" 时,只执行推理
82
+ - `--api-nproc (int, 默认值为 4)`: 调用 API 的线程数
83
+ - `--work-dir (str, default to '.')`: 存放测试结果的目录
84
+
85
+ **用于评测图像多模态评测集的命令**
86
+
87
+ 你可以使用 `python` 或 `torchrun` 来运行脚本:
88
+
89
+ ```bash
90
+ # 使用 `python` 运行时,只实例化一个 VLM,并且它可能使用多个 GPU。
91
+ # 这推荐用于评估参数量非常大的 VLMs(如 IDEFICS-80B-Instruct)。
92
+
93
+ # 在 MMBench_DEV_EN、MME 和 SEEDBench_IMG 上使用 IDEFICS-80B-Instruct 进行推理和评估
94
+ python run.py --data MMBench_DEV_EN MME SEEDBench_IMG --model idefics_80b_instruct --verbose
95
+ # 在 MMBench_DEV_EN、MME 和 SEEDBench_IMG 上使用 IDEFICS-80B-Instruct 仅进行推理
96
+ python run.py --data MMBench_DEV_EN MME SEEDBench_IMG --model idefics_80b_instruct --verbose --mode infer
97
+
98
+ # 使用 `torchrun` 运行时,每个 GPU 上实例化一个 VLM 实例。这可以加快推理速度。
99
+ # 但是,这仅适用于消耗少量 GPU 内存的 VLMs。
100
+
101
+ # 在 MMBench_DEV_EN、MME 和 SEEDBench_IMG 上使用 IDEFICS-9B-Instruct、Qwen-VL-Chat、mPLUG-Owl2。在具有 8 个 GPU 的节点上进行推理和评估。
102
+ torchrun --nproc-per-node=8 run.py --data MMBench_DEV_EN MME SEEDBench_IMG --model idefics_80b_instruct qwen_chat mPLUG-Owl2 --verbose
103
+ # 在 MME 上使用 Qwen-VL-Chat。在具有 2 个 GPU 的节点上进行推理和评估。
104
+ torchrun --nproc-per-node=2 run.py --data MME --model qwen_chat --verbose
105
+ ```
106
+
107
+ **用于评测视频多模态评测集的命令**
108
+
109
+ ```bash
110
+ # 使用 `python` 运行时,只实例化一个 VLM,并且它可能使用多个 GPU。
111
+ # 这推荐用于评估参数量非常大的 VLMs(如 IDEFICS-80B-Instruct)。
112
+
113
+ # 在 MMBench-Video 上评测 IDEFCIS2-8B, 视频采样 8 帧作为输入,不采用 pack 模式评测. MMBench_Video_8frame_nopack 是一个定义在 `vlmeval/dataset/video_dataset_config.py` 的数据集设定.
114
+ torchrun --nproc-per-node=8 run.py --data MMBench_Video_8frame_nopack --model idefics2_8
115
+ # 在 MMBench-Video 上评测 GPT-4o (API 模型), 视频采样每秒一帧作为输入,采用 pack 模式评测
116
+ python run.py --data MMBench_Video_1fps_pack --model GPT4o
117
+ ```
118
+
119
+ 评估结果将作为日志打印出来。此外,**结果文件**也会在目录 `$YOUR_WORKING_DIRECTORY/{model_name}` 中生成。以 `.csv` 结尾的文件包含评估的指标。
120
+ ### 常见问题
121
+ #### 构建输入prompt:`build_prompt()`函数
122
+ 如果您在评测某个benchmark时,发现模型输出的结果与预期不符,可能是因为您使用的模型没有正确构建输入prompt。
123
+
124
+ 在VLMEvalkit中,每个`dataset`类都包含一个名为`build_prompt()`的函数,用于构建输入问题的格式。不同的benchmark可以选择自定义`build_prompt()`函数,也可以使用默认的实现。
125
+
126
+ 例如,在处理默认的[多选题/Multi-Choice QA]([vlmeval/dataset/image_mcq.py](https://github.com/open-compass/VLMEvalKit/blob/43af13e052de6805a8b08cd04aed5e0d74f82ff5/vlmeval/dataset/image_mcq.py#L164))时,`ImageMCQDataset.build_prompt()`类会将`hint`、`question`、`options`等元素(若数据集中包含)组合成一个完整的问题格式,如下所示:
127
+ ```
128
+ HINT
129
+ QUESTION
130
+ Options:
131
+ A. Option A
132
+ B. Option B
133
+ ···
134
+ Please select the correct answer from the options above.
135
+ ```
136
+
137
+ 此外,由于不同模型对评测的需求可能有所不同,VLMEvalkit也支持在模型层面自定义对不同benchmark构建prompt的方法,即`model.build_prompt()`,具体示例可以参考[InternVL](https://github.com/open-compass/VLMEvalKit/blob/43af13e052de6805a8b08cd04aed5e0d74f82ff5/vlmeval/vlm/internvl_chat.py#L324)。
138
+
139
+ **注意:当同时定义了`model.build_prompt()`以及`dataset.build_prompt()`时,`model.build_prompt()`将优先于`dataset.build_prompt()`,即前者会覆盖后者。**
140
+
141
+ 由于部分模型(如Qwen2VL,InternVL等)对于不同类型的benchmark定义了广泛的prompt构建方法,为了更灵活地适应不同的benchmark,VLMEvalkit支持在模型中自定义`model.use_custom_prompt()`函数。通过添加或者修改`use_custom_prompt()`函数,您可以决定对于哪些benchmark使用模型自定义的`use_custom_prompt()`方法,示例如下:
142
+ ```
143
+ def use_custom_prompt(self, dataset: str) -> bool:
144
+ from vlmeval.dataset import DATASET_TYPE, DATASET_MODALITY
145
+ dataset_type = DATASET_TYPE(dataset, default=None)
146
+ if not self._use_custom_prompt:
147
+ return False
148
+ if listinstr(['MMVet'], dataset):
149
+ return True
150
+ if dataset_type == 'MCQ':
151
+ return True
152
+ if DATASET_MODALITY(dataset) == 'VIDEO':
153
+ return False
154
+ return False
155
+ ```
156
+ 仅当`use_custom_prompt()`函数返回`True`时,VLMEvalkit才会对当前benchmark调用模型的`build_prompt()`函数。
157
+ 通过这种方式,您可以根据具体需求灵活地控制哪些benchmark使用模型自定义的prompt构建逻辑,从而更好地适配不同模型和任务的需求。
158
+
159
+ #### 模型切分
160
+
161
+ 目前 VLMEvalKit 的启动方式自动支持同机上进程间 GPU 资源的划分与模型切分。该功能在推理后端为 `lmdeploy` 或 `transformers` 时被支持,具体行为如下:
162
+
163
+ - 基于 `python` 命令启动时,模型默认分配到所有可用的 GPU 上,如想指定使用哪些 GPU,可以使用 `CUDA_VISIBLE_DEVICES` 环境变量。
164
+ - 基于 `torchrun` 命令启动时,每个模型实例会被分配到 `N_GPU // N_PROC` 个 GPU 上,`N_PROC` 为 torchrun 命令中的 `--nproc-per-node` 参数所指定的进程数。`N_GPU` 的取值为:
165
+ - 如 `CUDA_VISIBLE_DEVICES` 环境变量未设置,`N_GPU` 为全部可用 GPU 数量。
166
+ - 如 `CUDA_VISIBLE_DEVICES` 环境变量被设置,`N_GPU` 为 `CUDA_VISIBLE_DEVICES` 环境变量所指定的 GPU 数量,并且,仅有指定的 GPU 会被利用。
167
+
168
+ 下面提供了,在一台配备 8 块 GPU 的机器上运行评测任务的具体示例:
169
+ ```bash
170
+ # <!-- 起两个模型实例数据并行,每个实例用 4 GPU -->
171
+ torchrun --nproc-per-node=2 run.py --data MMBench_DEV_EN --model InternVL3-78B
172
+ # <!-- 起一个模型实例,每个实例用 8 GPU -->
173
+ python run.py --data MMBench_DEV_EN --model InternVL3-78B
174
+ # <!-- 起三个模型实例,每个实例用 2 GPU,0 号、7 号 GPU 未被使用 -->
175
+ CUDA_VISIBLE_DEVICES=1,2,3,4,5,6 torchrun --nproc-per-node=3 run.py --data MMBench_DEV_EN --model InternVL3-38B
176
+ ```
177
+
178
+ 注:此方式不支持 `vllm` 后端,基于 `vllm` 后端起评测任务时,请用 `python` 命令启动,默认调用所有可见的 GPU。
179
+
180
+ #### 性能差距
181
+ 在不同的运行环境中,模型的性能表现可能会有所差异。因此,在评估过程中,您可能会发现自己的评测结果与VLMEvalKit官方榜单上的结果存在差距。这种差异可能与`transformers`, `cuda`, `torch`等版本的变化有关。
182
+
183
+ 此外,对于异常的表现,我们建议您优先查看运行完成后的本地生成记录`{model}_{dataset}.xlsx`或者评估记录`{model}_{dataset}_{judge_model}.xlsx`,这可能会帮助您更好地理解评估结果并发现问题。
184
+
185
+
186
+
187
+ ### 部署本地语言模型作为评判 / 选择提取器
188
+ 上述默认设置使用 OpenAI 的 GPT 作为评判 LLM。你也可以使用 [LMDeploy](https://github.com/InternLM/lmdeploy) 部署本地评判 LLM。
189
+
190
+ 首先进行安装:
191
+ ```
192
+ pip install lmdeploy openai
193
+ ```
194
+
195
+ 然后可以通过一行代码部署本地评判 LLM。LMDeploy 将自动从 Huggingface 下载模型。假设我们使用 internlm2-chat-1_8b 作为评判,端口为 23333,密钥为 sk-123456(密钥必须以 "sk-" 开头,后跟任意数字):
196
+ ```
197
+ lmdeploy serve api_server internlm/internlm2-chat-1_8b --server-port 23333
198
+ ```
199
+
200
+ 使用以下 Python 代码获取由 LMDeploy 注册的模型名称:
201
+ ```
202
+ from openai import OpenAI
203
+ client = OpenAI(
204
+ api_key='sk-123456',
205
+ base_url="http://0.0.0.0:23333/v1"
206
+ )
207
+ model_name = client.models.list().data[0].id
208
+ ```
209
+
210
+ 配置对应环境变量,以告诉 VLMEvalKit 如何使用本地评判 LLM。正如上面提到的,也可以在 `$VLMEvalKit/.env` 文件中设置:
211
+ ```
212
+ OPENAI_API_KEY=sk-123456
213
+ OPENAI_API_BASE=http://0.0.0.0:23333/v1/chat/completions
214
+ LOCAL_LLM=<model_name you get>
215
+ ```
216
+
217
+ 最后,你可以运行第2步中的命令,使用本地评判 LLM 来评估你的 VLM。
218
+
219
+ **请注意:**
220
+
221
+ - 如果你希望将评判 LLM 部署在单独的一个 GPU 上,并且由于 GPU 内存有限而希望在其他 GPU 上评估你的 VLM,可以使用 `CUDA_VISIBLE_DEVICES=x` 这样的方法,例如:
222
+ ```
223
+ CUDA_VISIBLE_DEVICES=0 lmdeploy serve api_server internlm/internlm2-chat-1_8b --server-port 23333
224
+ CUDA_VISIBLE_DEVICES=1,2,3 torchrun --nproc-per-node=3 run.py --data HallusionBench --model qwen_chat --verbose
225
+ ```
226
+ - 如果本地评判 LLM 在遵循指令方面不够好,评估过程可能会失败。请通过 issues 报告此类失败情况。
227
+ - 可以以不同的方式部署评判 LLM,例如使用私有 LLM(而非来自 HuggingFace)或使用量化 LLM。请参考 [LMDeploy doc](https://lmdeploy.readthedocs.io/en/latest/serving/api_server.html) 文档。也可以使用其他支持 OpenAI API 框架的方法。
228
+
229
+ ### 使用 LMDeploy 加速模型推理
230
+
231
+ 可参考[文档](/docs/zh-CN/EvalByLMDeploy.md)
reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/README_zh-CN.md ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div align="center">
2
+
3
+ ![LOGO](http://opencompass.openxlab.space/utils/MMLB.jpg)
4
+
5
+ <b>VLMEvalKit: 一种多模态大模型评测工具 </b>
6
+
7
+ [![][github-contributors-shield]][github-contributors-link] • [![][github-forks-shield]][github-forks-link] • [![][github-stars-shield]][github-stars-link] • [![][github-issues-shield]][github-issues-link] • [![][github-license-shield]][github-license-link]
8
+
9
+ [English](/README.md) | 简体中文 | [日本語](/docs/ja/README_ja.md)
10
+
11
+ <a href="https://rank.opencompass.org.cn/leaderboard-multimodal">🏆 OpenCompass 排行榜 </a> •
12
+ <a href="#%EF%B8%8F-quickstart">🏗️ 快速开始 </a> •
13
+ <a href="#-datasets-models-and-evaluation-results">📊 数据集和模型 </a> •
14
+ <a href="#%EF%B8%8F-development-guide">🛠️ 开发指南 </a> •
15
+ <a href="#-the-goal-of-vlmevalkit">🎯 我们的目标 </a> •
16
+ <a href="#%EF%B8%8F-citation">🖊️ 引用 </a>
17
+
18
+ <a href="https://huggingface.co/spaces/opencompass/open_vlm_leaderboard">🤗 HuggingFace 排行榜 (存档全部性能) </a> •
19
+ <a href="https://huggingface.co/datasets/VLMEval/OpenVLMRecords">🤗 原始评测记录</a> •
20
+ <a href="https://discord.gg/evDT4GZmxN">🔊 Discord</a> •
21
+ <a href="https://www.arxiv.org/abs/2407.11691">📝 技术报告 </a>
22
+ </div>
23
+
24
+ **VLMEvalKit** (python 包名为 **vlmeval**) 是一款专为大型视觉语言模型 (Large Vision-Language Models, LVLMs) 评测而设计的开源工具包。该工具支持在各种基准测试上对大型视觉语言模型进行**一键评估**,无需进行繁重的数据准备工作,让评估过程更加简便。在 VLMEvalKit 中,我们对所有大型视觉语言模型生成的结果进行评测,并提供基于**精确匹配**与基于 **LLM 的答案提取**两种评测结果。
25
+
26
+ ## 🆕 更新
27
+
28
+ - **[2025-04-29]** 优化 `torchrun` 启动逻辑:目前 `torchrun` 启动时,若进程数为 M,机器 GPU 卡数为 N,将会自动调整每个进程分配的 GPU 数量为 `N // M`。目前此分配方式适用于 `transformers`, `lmdeploy` 推理后端,`vllm` 推理后端仅支持使用 python 启动 🔥🔥🔥
29
+ - **[2025-02-20]** 支持新模型:**InternVL2.5 series, QwenVL2.5 series, QVQ-72B, Doubao-VL, Janus-Pro-7B, MiniCPM-o-2.6, InternVL2-MPO, LLaVA-CoT, Hunyuan-Standard-Vision, Ovis2, Valley, SAIL-VL, Ross, Long-VITA, EMU3, SmolVLM**。支持新基准:**MMMU-Pro, WeMath, 3DSRBench, LogicVista, VL-RewardBench, CC-OCR, CG-Bench, CMMMU, WorldSense**。请参考[**VLMEvalKit Features**](https://aicarrier.feishu.cn/wiki/Qp7wwSzQ9iK1Y6kNUJVcr6zTnPe?table=tblsdEpLieDoCxtb)以获取更多信息。感谢社区的各位贡献者 🔥🔥🔥
30
+ - **[2024-11-21]** 集成了一个新的配置系统,以实现更灵活的评估设置。查看[文档](/docs/zh-CN/ConfigSystem.md)或运行`python run.py --help`了解更多详情 🔥🔥🔥
31
+ - **[2024-11-21]** 支持 **[QSpatial](https://andrewliao11.github.io/spatial_prompt/)**,一个用于定量空间推理的多模态基准(例如,确定大小/距离),感谢 **[andrewliao11](https://github.com/andrewliao11)** 提供官方支持 🔥🔥🔥
32
+ - **[2024-11-21]** 支持 **[MM-Math](https://github.com/kge-sun/mm-math)**,一个包含约6K初中多模态推理数学问题的新多模态数学基准。GPT-4o-20240806在该基准上达到了22.5%的准确率 🔥🔥🔥
33
+ - **[2024-11-16]** 支持 **[OlympiadBench](https://github.com/OpenBMB/OlympiadBench)**,一个多模态基准,包含奥林匹克级别的数学和物理问题 🔥🔥🔥
34
+ - **[2024-11-16]** 支持 **[WildVision](https://huggingface.co/datasets/WildVision/wildvision-bench)**,一个基于多模态竞技场数据的主观多模态基准 🔥🔥🔥
35
+ - **[2024-11-13]** 支持 **[MIA-Bench](https://arxiv.org/abs/2407.01509)**,一个多模态指令跟随基准 🔥🔥🔥
36
+ - **[2024-11-08]** 支持 **[Aria](https://arxiv.org/abs/2410.05993)**,一个多模态原生 MoE 模型,感谢 **[teowu](https://github.com/teowu)** 🔥🔥🔥
37
+ - **[2024-11-04]** 支持 **[WorldMedQA-V](https://www.arxiv.org/abs/2410.12722)**,该基准包含 1000 多个医学 VQA 问题,涵盖巴西、以色列、日本、西班牙等四个国家的语言,以及它们的英文翻译 🔥🔥🔥
38
+
39
+ ## 🏗️ 快速开始 <a id="quickstart"></a>
40
+
41
+ 请参阅[**快速开始**](/docs/zh-CN/Quickstart.md)获取入门指南。
42
+
43
+ ## 📊 评测结果,支持的数据集和模型 <a id="data-model-results"></a>
44
+
45
+ ### 评测结果
46
+
47
+ **[OpenVLM Leaderboard](https://huggingface.co/spaces/opencompass/open_vlm_leaderboard)**: **[下载全部细粒度测试结果](http://opencompass.openxlab.space/assets/OpenVLM.json)**.
48
+
49
+ 请查看[**VLMEvalKit Features**](https://aicarrier.feishu.cn/wiki/Qp7wwSzQ9iK1Y6kNUJVcr6zTnPe?table=tblsdEpLieDoCxtb)中的 **Supported Benchmarks** 标签,以查看所有支持的图像和视频基准(70+)。
50
+
51
+ 请查看[**VLMEvalKit Features**](https://aicarrier.feishu.cn/wiki/Qp7wwSzQ9iK1Y6kNUJVcr6zTnPe?table=tblsdEpLieDoCxtb)中的 **Supported LMMs** 标签,以查看所���支持的 LMMs,包括商业 API、开源模型等(200+)。
52
+
53
+ ### 其他
54
+
55
+ **Transformers 的版本推荐:**
56
+
57
+ **请注意**,某些 VLM 可能无法在某些特定的 transformers 版本下运行,我们建议使用以下设置来评估对应的VLM:
58
+
59
+ - **请用** `transformers==4.33.0` **来运行**: `Qwen series`, `Monkey series`, `InternLM-XComposer Series`, `mPLUG-Owl2`, `OpenFlamingo v2`, `IDEFICS series`, `VisualGLM`, `MMAlaya`, `ShareCaptioner`, `MiniGPT-4 series`, `InstructBLIP series`, `PandaGPT`, `VXVERSE`.
60
+ - **请用** `transformers==4.37.0 ` **来运行**: `LLaVA series`, `ShareGPT4V series`, `TransCore-M`, `LLaVA (XTuner)`, `CogVLM Series`, `EMU2 Series`, `Yi-VL Series`, `MiniCPM-[V1/V2]`, `OmniLMM-12B`, `DeepSeek-VL series`, `InternVL series`, `Cambrian Series`, `VILA Series`, `Llama-3-MixSenseV1_1`, `Parrot-7B`, `PLLaVA Series`.
61
+ - **请用** `transformers==4.40.0 ` **来运行**: `IDEFICS2`, `Bunny-Llama3`, `MiniCPM-Llama3-V2.5`, `360VL-70B`, `Phi-3-Vision`, `WeMM`.
62
+ - **请用** `transformers==4.42.0 ` **来运行**: `AKI`.
63
+ - **请用** `transformers==latest` **来运行**: `LLaVA-Next series`, `PaliGemma-3B`, `Chameleon series`, `Video-LLaVA-7B-HF`, `Ovis series`, `Mantis series`, `MiniCPM-V2.6`, `OmChat-v2.0-13B-sinlge-beta`, `Idefics-3`, `GLM-4v-9B`, `VideoChat2-HD`.
64
+
65
+ **如何测试一个 VLM 是否可以正常运行:**
66
+
67
+ ```python
68
+ from vlmeval.config import supported_VLM
69
+ model = supported_VLM['idefics_9b_instruct']()
70
+ # 前向单张图片
71
+ ret = model.generate(['assets/apple.jpg', 'What is in this image?'])
72
+ print(ret) # 这张图片上有一个带叶子的红苹果
73
+ # 前向多张图片
74
+ ret = model.generate(['assets/apple.jpg', 'assets/apple.jpg', 'How many apples are there in the provided images? '])
75
+ print(ret) # 提供的图片中有两个苹果
76
+ ```
77
+
78
+ ## 🛠️ 开发指南 <a id="development"></a>
79
+
80
+ 要开发自定义评测数据集,支持其他 VLMs,或为 VLMEvalKit 贡献代码,请参阅[**开发指南**](/docs/zh-CN/Development_zh-CN.md)。
81
+
82
+ 为激励来自社区的共享并分享相应的 credit,在下一次 report 更新中,我们将:
83
+
84
+ - 致谢所有的 contribution
85
+ - 具备三个或以上主要贡献 (支持新模型、评测集、或是主要特性) 的贡献者将可以加入技术报告的作者列表 。合条件的贡献者可以创建 issue 或是在 [VLMEvalKit Discord Channel](https://discord.com/invite/evDT4GZmxN) 私信 kennyutc,我们将进行跟进
86
+
87
+ ## 🎯 VLMEvalKit 的目标 <a id="goal-of-vlmevalkit"></a>
88
+
89
+ **该代码库的设计目标是:**
90
+
91
+ 1. 提供一个**易于使用**的**开源评估工具包**,方便研究人员和开发人员评测现有的多模态大模型,并使评测结果**易于复现**。
92
+ 2. 使 VLM 开发人员能够轻松地评测自己的模型。在多个支持的基准测试上评估 VLM,只需实现一个 `generate_inner()` 函数,所有其他工作负载(数据下载、数据预处理、预测推理、度量计算)都由代码库处理。
93
+
94
+ **该代码库的设计目标不是:**
95
+
96
+ 复现所有**第三方基准测试**原始论文中报告的准确数字。有两个相关的原因:
97
+ 1. VLMEvalKit 对所有 VLMs 使用基于生成的评估(可选使用基于 LLM 的答案提取)。同时,一些基准测试可能官方使用不同的方法(*例如,SEEDBench 使用基于 PPL 的评估*)。对于这些基准测试,我们在相应的结果中比较两个得分。我们鼓励开发人员在代码库中支持其他评估范式。
98
+ 2. 默认情况下,我们对所有多模态模型使用相同的提示模板来评估基准测试。同时,**一些多模态模型可能有他们特定的提示模板**(目前可能未在代码库中涵盖)。我们鼓励 VLM 的开发人员在 VLMEvalKit 中实现自己的提示模板,如果目前未覆盖。这将有助于提高可复现性。
99
+
100
+ ## 🖊️ 引用 <a id="citation"></a>
101
+
102
+ 如果我们的工作对您有所帮助,请考虑 **star🌟** VLMEvalKit。感谢支持!
103
+
104
+ [![Stargazers repo roster for @open-compass/VLMEvalKit](https://reporoster.com/stars/open-compass/VLMEvalKit)](https://github.com/open-compass/VLMEvalKit/stargazers)
105
+
106
+ 如果您在研究中使用了 VLMEvalKit,或希望参考已发布的开源评估结果,请使用以下 BibTeX 条目以及与您使用的特定 VLM / 基准测试相对应的 BibTex 条目。
107
+
108
+ ```bib
109
+ @misc{duan2024vlmevalkit,
110
+ title={VLMEvalKit: An Open-Source Toolkit for Evaluating Large Multi-Modality Models},
111
+ author={Haodong Duan and Junming Yang and Yuxuan Qiao and Xinyu Fang and Lin Chen and Yuan Liu and Xiaoyi Dong and Yuhang Zang and Pan Zhang and Jiaqi Wang and Dahua Lin and Kai Chen},
112
+ year={2024},
113
+ eprint={2407.11691},
114
+ archivePrefix={arXiv},
115
+ primaryClass={cs.CV},
116
+ url={https://arxiv.org/abs/2407.11691},
117
+ }
118
+ ```
119
+
120
+ <p align="right"><a href="#top">🔝回到顶部</a></p>
121
+
122
+ [github-contributors-link]: https://github.com/open-compass/VLMEvalKit/graphs/contributors
123
+ [github-contributors-shield]: https://img.shields.io/github/contributors/open-compass/VLMEvalKit?color=c4f042&labelColor=black&style=flat-square
124
+ [github-forks-link]: https://github.com/open-compass/VLMEvalKit/network/members
125
+ [github-forks-shield]: https://img.shields.io/github/forks/open-compass/VLMEvalKit?color=8ae8ff&labelColor=black&style=flat-square
126
+ [github-issues-link]: https://github.com/open-compass/VLMEvalKit/issues
127
+ [github-issues-shield]: https://img.shields.io/github/issues/open-compass/VLMEvalKit?color=ff80eb&labelColor=black&style=flat-square
128
+ [github-license-link]: https://github.com/open-compass/VLMEvalKit/blob/main/LICENSE
129
+ [github-license-shield]: https://img.shields.io/github/license/open-compass/VLMEvalKit?color=white&labelColor=black&style=flat-square
130
+ [github-stars-link]: https://github.com/open-compass/VLMEvalKit/stargazers
131
+ [github-stars-shield]: https://img.shields.io/github/stars/open-compass/VLMEvalKit?color=ffcb47&labelColor=black&style=flat-square
reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/_static/css/readthedocs.css ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .header-logo {
2
+ background-image: url("../image/logo.svg");
3
+ background-size: 275px 80px;
4
+ height: 80px;
5
+ width: 275px;
6
+ }
7
+
8
+
9
+ @media screen and (min-width: 1100px) {
10
+ .header-logo {
11
+ top: -25px;
12
+ }
13
+ }
14
+
15
+ pre {
16
+ white-space: pre;
17
+ }
18
+
19
+ @media screen and (min-width: 2000px) {
20
+ .pytorch-content-left {
21
+ width: 1200px;
22
+ margin-left: 30px;
23
+ }
24
+ article.pytorch-article {
25
+ max-width: 1200px;
26
+ }
27
+ .pytorch-breadcrumbs-wrapper {
28
+ width: 1200px;
29
+ }
30
+ .pytorch-right-menu.scrolling-fixed {
31
+ position: fixed;
32
+ top: 45px;
33
+ left: 1580px;
34
+ }
35
+ }
36
+
37
+
38
+ article.pytorch-article section code {
39
+ padding: .2em .4em;
40
+ background-color: #f3f4f7;
41
+ border-radius: 5px;
42
+ }
43
+
44
+ /* Disable the change in tables */
45
+ article.pytorch-article section table code {
46
+ padding: unset;
47
+ background-color: unset;
48
+ border-radius: unset;
49
+ }
50
+
51
+ table.autosummary td {
52
+ width: 50%
53
+ }
54
+
55
+ img.align-center {
56
+ display: block;
57
+ margin-left: auto;
58
+ margin-right: auto;
59
+ }
60
+
61
+ article.pytorch-article p.rubric {
62
+ font-weight: bold;
63
+ }
reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/_static/image/logo.svg ADDED
reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/_static/image/logo_icon.svg ADDED
reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/_static/js/custom.js ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ var collapsedSections = [];
2
+
3
+ $(document).ready(function () {
4
+ $('.model-summary').DataTable({
5
+ "stateSave": false,
6
+ "lengthChange": false,
7
+ "pageLength": 20,
8
+ "order": []
9
+ });
10
+ });
reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/_templates/404.html ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "layout.html" %}
2
+
3
+ {% block body %}
4
+
5
+ <h1>Page Not Found</h1>
6
+ <p>
7
+ The page you are looking for cannot be found.
8
+ </p>
9
+ <p>
10
+ If you just switched documentation versions, it is likely that the page you were on is moved. You can look for it in
11
+ the content table left, or go to <a href="{{ pathto(root_doc) }}">the homepage</a>.
12
+ </p>
13
+ <!-- <p>
14
+ If you cannot find documentation you want, please <a
15
+ href="">open an issue</a> to tell us!
16
+ </p> -->
17
+
18
+ {% endblock %}
reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/_templates/autosummary/class.rst ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .. role:: hidden
2
+ :class: hidden-section
3
+ .. currentmodule:: {{ module }}
4
+
5
+
6
+ {{ name | underline}}
7
+
8
+ .. autoclass:: {{ name }}
9
+ :members:
10
+
11
+ ..
12
+ autogenerated from _templates/autosummary/class.rst
13
+ note it does not have :inherited-members:
reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/_templates/callable.rst ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .. role:: hidden
2
+ :class: hidden-section
3
+ .. currentmodule:: {{ module }}
4
+
5
+
6
+ {{ name | underline}}
7
+
8
+ .. autoclass:: {{ name }}
9
+ :members:
10
+ :special-members: __call__
11
+
12
+ ..
13
+ autogenerated from _templates/callable.rst
14
+ note it does not have :inherited-members:
reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/conf.py ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # flake8: noqa
2
+ # Configuration file for the Sphinx documentation builder.
3
+ #
4
+ # This file only contains a selection of the most common options. For a full
5
+ # list see the documentation:
6
+ # https://www.sphinx-doc.org/en/master/usage/configuration.html
7
+
8
+ # -- Path setup --------------------------------------------------------------
9
+
10
+ # If extensions (or modules to document with autodoc) are in another directory,
11
+ # add these directories to sys.path here. If the directory is relative to the
12
+ # documentation root, use os.path.abspath to make it absolute, like shown here.
13
+
14
+ import ast
15
+ import os
16
+ import subprocess
17
+ import sys
18
+
19
+ import pytorch_sphinx_theme
20
+ from sphinx.builders.html import StandaloneHTMLBuilder
21
+
22
+ sys.path.insert(0, os.path.abspath('../../'))
23
+
24
+ # -- Project information -----------------------------------------------------
25
+
26
+ project = 'VLMEvalKit'
27
+ copyright = '2023, VLMEvalKit'
28
+ author = 'VLMEvalKit Authors'
29
+
30
+ # The full version, including alpha/beta/rc tags
31
+ version_file = '../../vlmeval/__init__.py'
32
+
33
+
34
+ def get_version():
35
+ with open(version_file, 'r') as f:
36
+ file_content = f.read()
37
+ # Parse the file content into an abstract syntax tree (AST)
38
+ tree = ast.parse(file_content, filename=version_file)
39
+
40
+ # Iterate through the body of the AST, looking for an assignment to __version__
41
+ for node in tree.body:
42
+ if isinstance(node, ast.Assign):
43
+ for target in node.targets:
44
+ if isinstance(target, ast.Name) and target.id == '__version__':
45
+ return node.value.s
46
+ raise ValueError('__version__ not found')
47
+
48
+
49
+ release = get_version()
50
+
51
+ # -- General configuration ---------------------------------------------------
52
+
53
+ # Add any Sphinx extension module names here, as strings. They can be
54
+ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
55
+ # ones.
56
+ extensions = [
57
+ 'sphinx.ext.autodoc',
58
+ 'sphinx.ext.autosummary',
59
+ 'sphinx.ext.intersphinx',
60
+ 'sphinx.ext.napoleon',
61
+ 'sphinx.ext.viewcode',
62
+ 'myst_parser',
63
+ 'sphinx_copybutton',
64
+ 'sphinx_tabs.tabs',
65
+ 'notfound.extension',
66
+ 'sphinxcontrib.jquery',
67
+ 'sphinx_design',
68
+ ]
69
+
70
+ # Add any paths that contain templates here, relative to this directory.
71
+ templates_path = ['_templates']
72
+
73
+ # The suffix(es) of source filenames.
74
+ # You can specify multiple suffix as a list of string:
75
+ #
76
+ source_suffix = {
77
+ '.rst': 'restructuredtext',
78
+ '.md': 'markdown',
79
+ }
80
+
81
+ language = 'cn'
82
+
83
+ # The master toctree document.
84
+ root_doc = 'index'
85
+ html_context = {
86
+ 'github_version': 'latest',
87
+ }
88
+ # List of patterns, relative to source directory, that match files and
89
+ # directories to ignore when looking for source files.
90
+ # This pattern also affects html_static_path and html_extra_path.
91
+ exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
92
+
93
+ # -- Options for HTML output -------------------------------------------------
94
+
95
+ # The theme to use for HTML and HTML Help pages. See the documentation for
96
+ # a list of builtin themes.
97
+ #
98
+ html_theme = 'pytorch_sphinx_theme'
99
+ html_theme_path = [pytorch_sphinx_theme.get_html_theme_path()]
100
+
101
+ # Theme options are theme-specific and customize the look and feel of a theme
102
+ # further. For a list of options available for each theme, see the
103
+ # documentation.
104
+ # yapf: disable
105
+ html_theme_options = {
106
+ 'menu': [
107
+ {
108
+ 'name': 'GitHub',
109
+ 'url': 'https://github.com/open-compass/VLMEvalKit'
110
+ },
111
+ ],
112
+ # Specify the language of shared menu
113
+ 'menu_lang': 'cn',
114
+ # Disable the default edit on GitHub
115
+ 'default_edit_on_github': False,
116
+ }
117
+ # yapf: enable
118
+
119
+ # Add any paths that contain custom static files (such as style sheets) here,
120
+ # relative to this directory. They are copied after the builtin static files,
121
+ # so a file named "default.css" will overwrite the builtin "default.css".
122
+ html_static_path = ['_static']
123
+ html_css_files = [
124
+ 'https://cdn.datatables.net/v/bs4/dt-1.12.1/datatables.min.css',
125
+ 'css/readthedocs.css'
126
+ ]
127
+ html_js_files = [
128
+ 'https://cdn.datatables.net/v/bs4/dt-1.12.1/datatables.min.js',
129
+ 'js/custom.js'
130
+ ]
131
+
132
+ # -- Options for HTMLHelp output ---------------------------------------------
133
+
134
+ # Output file base name for HTML help builder.
135
+ htmlhelp_basename = 'vlmevalkitdoc'
136
+
137
+ # -- Options for LaTeX output ------------------------------------------------
138
+
139
+ latex_elements = {
140
+ # The paper size ('letterpaper' or 'a4paper').
141
+ #
142
+ # 'papersize': 'letterpaper',
143
+
144
+ # The font size ('10pt', '11pt' or '12pt').
145
+ #
146
+ # 'pointsize': '10pt',
147
+
148
+ # Additional stuff for the LaTeX preamble.
149
+ #
150
+ # 'preamble': '',
151
+ }
152
+
153
+ # Grouping the document tree into LaTeX files. List of tuples
154
+ # (source start file, target name, title,
155
+ # author, documentclass [howto, manual, or own class]).
156
+ latex_documents = [
157
+ (root_doc, 'vlmevalkit.tex', 'VLMEvalKit Documentation', author,
158
+ 'manual'),
159
+ ]
160
+
161
+ # -- Options for manual page output ------------------------------------------
162
+
163
+ # One entry per manual page. List of tuples
164
+ # (source start file, name, description, authors, manual section).
165
+ man_pages = [(root_doc, 'vlmevalkit', 'VLMEvalKit Documentation', [author],
166
+ 1)]
167
+
168
+ # -- Options for Texinfo output ----------------------------------------------
169
+
170
+ # Grouping the document tree into Texinfo files. List of tuples
171
+ # (source start file, target name, title, author,
172
+ # dir menu entry, description, category)
173
+ texinfo_documents = [
174
+ (root_doc, 'vlmevalkit', 'VLMEvalKit Documentation', author,
175
+ 'VLMEvalKit Authors', 'AGI evaluation toolbox and benchmark.',
176
+ 'Miscellaneous'),
177
+ ]
178
+
179
+ # -- Options for Epub output -------------------------------------------------
180
+
181
+ # Bibliographic Dublin Core info.
182
+ epub_title = project
183
+
184
+ # The unique identifier of the text. This can be a ISBN number
185
+ # or the project homepage.
186
+ #
187
+ # epub_identifier = ''
188
+
189
+ # A unique identification for the text.
190
+ #
191
+ # epub_uid = ''
192
+
193
+ # A list of files that should not be packed into the epub file.
194
+ epub_exclude_files = ['search.html']
195
+
196
+ # set priority when building html
197
+ StandaloneHTMLBuilder.supported_image_types = [
198
+ 'image/svg+xml', 'image/gif', 'image/png', 'image/jpeg'
199
+ ]
200
+
201
+ # -- Extension configuration -------------------------------------------------
202
+ # Ignore >>> when copying code
203
+ copybutton_prompt_text = r'>>> |\.\.\. '
204
+ copybutton_prompt_is_regexp = True
205
+
206
+ # Auto-generated header anchors
207
+ myst_heading_anchors = 3
208
+ # Enable "colon_fence" extension of myst.
209
+ myst_enable_extensions = ['colon_fence', 'dollarmath']
210
+
211
+ # Configuration for intersphinx
212
+ intersphinx_mapping = {
213
+ 'python': ('https://docs.python.org/3', None),
214
+ 'numpy': ('https://numpy.org/doc/stable', None),
215
+ 'torch': ('https://pytorch.org/docs/stable/', None),
216
+ 'mmengine': ('https://mmengine.readthedocs.io/en/latest/', None),
217
+ 'transformers':
218
+ ('https://huggingface.co/docs/transformers/main/en/', None),
219
+ }
220
+ napoleon_custom_sections = [
221
+ # Custom sections for data elements.
222
+ ('Meta fields', 'params_style'),
223
+ ('Data fields', 'params_style'),
224
+ ]
225
+
226
+ # Disable docstring inheritance
227
+ autodoc_inherit_docstrings = False
228
+ # Mock some imports during generate API docs.
229
+ autodoc_mock_imports = ['rich', 'attr', 'einops']
230
+ # Disable displaying type annotations, these can be very verbose
231
+ autodoc_typehints = 'none'
232
+
233
+ # The not found page
234
+ notfound_template = '404.html'
235
+
236
+
237
+ def builder_inited_handler(app):
238
+ subprocess.run(['./cp_origin_docs.sh'])
239
+
240
+
241
+ def setup(app):
242
+ app.connect('builder-inited', builder_inited_handler)
reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/cp_origin_docs.sh ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+
3
+ # Copy *.md files from docs/ if it doesn't have a Chinese translation
4
+
5
+ for filename in $(find ../en/ -name '*.md' -printf "%P\n");
6
+ do
7
+ mkdir -p $(dirname $filename)
8
+ cp -n ../en/$filename ./$filename
9
+ done
reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/docutils.conf ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ [html writers]
2
+ table_style: colwidths-auto
reference/code/Fast-dLLM/third_party/VLMEvalKit/docs/zh-CN/index.rst ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 欢迎来到 VLMEvalKit 中文教程!
2
+ ==========================================
3
+
4
+ VLMEvalKit 上手路线
5
+ -------------------------------
6
+
7
+ 为了用户能够快速上手,我们推荐以下流程:
8
+
9
+ - 对于想要使用 VLMEvalKit 的用户,我们推荐先阅读 开始你的第一步_ 部分来设置环境,并启动一个迷你实验熟悉流程。
10
+
11
+ - 若您想进行更多模块的自定义,例如增加数据集和模型,我们提供了 进阶教程_ 。
12
+
13
+ 我们始终非常欢迎用户的 PRs 和 Issues 来完善 VLMEvalKit!
14
+
15
+ .. _快速开始:
16
+ .. toctree::
17
+ :maxdepth: 1
18
+ :caption: 快速开始
19
+
20
+ Quickstart.md
21
+
22
+
23
+ .. .. _教程:
24
+ .. .. toctree::
25
+ .. :maxdepth: 1
26
+ .. :caption: 教程
27
+
28
+ .. user_guides/framework_overview.md
29
+
30
+ .. _进阶教程:
31
+ .. toctree::
32
+ :maxdepth: 1
33
+ :caption: 进阶教程
34
+
35
+ Development.md
36
+ ConfigSystem.md
37
+
38
+ .. .. _其他说明:
39
+ .. .. toctree::
40
+ .. :maxdepth: 1
41
+ .. :caption: 其他说明
42
+
43
+ .. notes/contribution_guide.md
44
+
45
+ 索引与表格
46
+ ==================
47
+
48
+ * :ref:`genindex`
49
+ * :ref:`search`
reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/CGAVCounting/__init__.py ADDED
File without changes
reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/CGAVCounting/cg_av_counting.py ADDED
@@ -0,0 +1,415 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import os.path as osp
4
+ from pathlib import Path
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+ import portalocker
9
+ from huggingface_hub import snapshot_download
10
+ from PIL import Image
11
+
12
+ from vlmeval.smp import (dump, get_cache_path, get_file_extension, get_intermediate_file_path,
13
+ load, md5, modelscope_flag_set)
14
+ from ..utils.cgbench import post_process, unzip_hf_zip
15
+ from ..video_base import VideoBaseDataset
16
+ from .utils import get_timestampes, rating_func
17
+
18
+
19
+ class CGAVCounting(VideoBaseDataset):
20
+
21
+ dataset = "CG-AV-Counting"
22
+
23
+ TYPE = "Video-Counting"
24
+
25
+ MD5 = "d1cd8486353ab85178098d443264a7d0"
26
+
27
+ SYS = ""
28
+
29
+ def __init__(
30
+ self,
31
+ dataset="CG-AV-Counting",
32
+ use_frame_time=False,
33
+ nframe=0,
34
+ fps=-1,
35
+ ):
36
+ super().__init__(dataset=dataset, nframe=nframe, fps=fps)
37
+ self.use_frame_time = use_frame_time
38
+ self.dataset_name = dataset
39
+ self.frame_tmpl_clue = 'frame-{}.jpg'
40
+
41
+ @classmethod
42
+ def supported_datasets(cls):
43
+ return ["CGAVCounting"]
44
+
45
+ def frame_paths_clue(self, video, timestamp_list):
46
+ frame_root = osp.join(self.frame_root, video)
47
+ os.makedirs(frame_root, exist_ok=True)
48
+ return [osp.join(frame_root, self.frame_tmpl_clue.format(i)) for i in timestamp_list]
49
+
50
+ def save_video_frames_clue(self, video, uid, timestamp_list):
51
+ if type(uid) is not str:
52
+ uid = str(uid)
53
+ import decord
54
+ frame_paths = self.frame_paths_clue(uid, timestamp_list)
55
+ flag = np.all([osp.exists(p) for p in frame_paths])
56
+ if flag:
57
+ frame = Image.open(frame_paths[0])
58
+ return frame_paths, frame.width, frame.height
59
+ vid_path = osp.join(self.data_root, video)
60
+ vid = decord.VideoReader(vid_path)
61
+ frames = []
62
+ # 获取视频的帧率
63
+ fps = vid.get_avg_fps()
64
+ lock_path = osp.splitext(vid_path)[0] + '.lock'
65
+ with portalocker.Lock(lock_path, 'w', timeout=30):
66
+ for timestamp_sec in timestamp_list:
67
+ # 计算视频帧对应的索引
68
+ frame_idx = int(timestamp_sec * fps)
69
+
70
+ # 获取对应帧
71
+ frame = vid[frame_idx]
72
+
73
+ # 将帧转换为PIL图像
74
+ img = Image.fromarray(frame.asnumpy())
75
+ frames.append(img)
76
+ for im, pth in zip(frames, frame_paths):
77
+ if not osp.exists(pth):
78
+ im.save(pth)
79
+ return frame_paths, frames[0].width, frames[0].height
80
+
81
+ def format_time(self, t):
82
+ return f"{t:.2f}"
83
+
84
+ def get_output_filename(self, item):
85
+ video_id = Path(item["video"]).stem
86
+ start_str = self.format_time(item["query_interval"][0])
87
+ end_str = self.format_time(item["query_interval"][1])
88
+ return f"{video_id}_{start_str}_{end_str}.mp4"
89
+
90
+ def prepare_dataset(self, dataset_name="CG-AV-Counting", repo_id="CG-Bench/CG-AV-Counting"):
91
+
92
+ def check_integrity(pth):
93
+ data_file = osp.join(pth, f"{dataset_name}.tsv")
94
+
95
+ if not os.path.exists(data_file):
96
+ return False
97
+
98
+ if md5(data_file) != self.MD5:
99
+ return False
100
+ data = load(data_file)
101
+ for video_pth in data["video"]:
102
+ if not osp.exists(osp.join(pth, video_pth)):
103
+ return False
104
+ return True
105
+
106
+ cache_path = get_cache_path(repo_id)
107
+
108
+ if cache_path is not None and check_integrity(cache_path):
109
+ dataset_path = cache_path
110
+ else:
111
+
112
+ def generate_tsv(pth):
113
+
114
+ tsv_file = osp.join(pth, f"{dataset_name}.tsv")
115
+
116
+ task_modes = ["long_acc", "ref_acc", "clue_acc"]
117
+ all_data = []
118
+ for task_mode in task_modes:
119
+ with open(osp.join(pth, "cg-av-counting.json"), "r") as f:
120
+ data_file = pd.DataFrame(json.load(f))
121
+
122
+ data_file = data_file.assign(index=range(len(data_file)))
123
+ data_file["video_uid"] = data_file["video"].replace(".mp4", "")
124
+ data_file["video"] = data_file["video"].apply(lambda x: f"cg_videos_720p/{x}")
125
+
126
+ data_file["ref_video_path"] = ""
127
+ data_file["ref_video_uid"] = ""
128
+
129
+ if task_mode in ["ref_acc"]:
130
+ data_file["ref_video_path"] = data_file.apply(
131
+ lambda row: f"ref_videos/{self.get_output_filename(row)}", axis=1
132
+ )
133
+ data_file["ref_video_uid"] = data_file["ref_video_path"].apply(
134
+ lambda x: x.split("/")[-1].replace(".mp4", ""))
135
+
136
+ data_file["task_mode"] = task_mode
137
+
138
+ if task_mode == "clue_acc":
139
+ data_file["answer"] = data_file["clue"].apply(json.dumps)
140
+
141
+ data_file = data_file[
142
+ [
143
+ "index",
144
+ "video_uid",
145
+ "video",
146
+ "ref_video_path",
147
+ "ref_video_uid",
148
+ "question",
149
+ "answer",
150
+ "type",
151
+ "category",
152
+ "task_mode"
153
+ ]
154
+ ]
155
+
156
+ all_data.append(data_file)
157
+
158
+ final_data = pd.concat(all_data, ignore_index=True)
159
+ final_data["index"] = range(len(final_data))
160
+ final_data.to_csv(tsv_file, sep="\t", index=False)
161
+ dataset_path = cache_path
162
+
163
+ if modelscope_flag_set():
164
+ from modelscope import dataset_snapshot_download
165
+
166
+ dataset_path = dataset_snapshot_download(dataset_id=repo_id)
167
+ else:
168
+ dataset_path = snapshot_download(repo_id=repo_id, repo_type="dataset")
169
+
170
+ unzip_hf_zip(dataset_path)
171
+
172
+ generate_tsv(dataset_path)
173
+
174
+ tsv_file = osp.join(dataset_path, f"{dataset_name}.tsv")
175
+
176
+ return dict(data_file=tsv_file, root=dataset_path)
177
+
178
+ def build_prompt(self, line, video_llm):
179
+ if isinstance(line, int):
180
+ assert line < len(self)
181
+ line = self.data.iloc[line]
182
+ task_mode = line["task_mode"]
183
+ assert task_mode in ["long_acc", "clue_acc", "ref_acc"]
184
+ if task_mode == "long_acc":
185
+ user_prompt = ""
186
+ message = []
187
+ video_path = line["video"]
188
+ if video_llm:
189
+ message.append(dict(type="video", value=osp.join(self.data_root, video_path)))
190
+ else:
191
+ image_paths, frame_indices, vid_fps = self.save_video_frames(
192
+ video_path, uid=line["video_uid"], num_frames=self.nframe, fps=self.fps
193
+ )
194
+ message.extend(dict(type="image", value=im) for im in image_paths)
195
+
196
+ if self.use_frame_time:
197
+ user_prompt += get_timestampes(frame_indices, vid_fps)
198
+
199
+ user_prompt += (
200
+ f"Please answer the question '{line['question']}' with a number. Just output the number itself, "
201
+ "don't output anything else."
202
+ )
203
+ message.append(dict(type="text", value=user_prompt))
204
+ elif task_mode == "ref_acc":
205
+ user_prompt = ""
206
+ message = []
207
+ video_path = line["ref_video_path"]
208
+ if video_llm:
209
+ message.append(dict(type="video", value=osp.join(self.data_root, video_path)))
210
+ else:
211
+ image_paths, frame_indices, vid_fps = self.save_video_frames(
212
+ video_path, uid=line["ref_video_uid"], num_frames=self.nframe, fps=self.fps
213
+ )
214
+ message.extend(dict(type="image", value=im) for im in image_paths)
215
+
216
+ if self.use_frame_time:
217
+ user_prompt += get_timestampes(frame_indices, vid_fps)
218
+ user_prompt += (
219
+ f"Please answer the question '{line['question']}' with a number. Just output the number itself, "
220
+ "don't output anything else."
221
+ )
222
+ message.append(dict(type="text", value=user_prompt))
223
+ elif task_mode == "clue_acc":
224
+ if line["category"] == "event":
225
+ user_prompt = ""
226
+ message = []
227
+ video_path = line["video"]
228
+ if video_llm:
229
+ message.append(dict(type="video", value=osp.join(self.data_root, video_path)))
230
+ else:
231
+ image_paths, frame_indices, vid_fps = self.save_video_frames(
232
+ video_path, uid=line["video_uid"], num_frames=self.nframe, fps=self.fps
233
+ )
234
+ message.extend(dict(type="image", value=im) for im in image_paths)
235
+ user_prompt += get_timestampes(frame_indices, vid_fps)
236
+
237
+ user_prompt += (
238
+ f"Watch the video and provide your answer to the question '{line['question']}', "
239
+ "including the start and end timestamps for each event."
240
+ "Format your answer in JSON, enclosed in <answer> and </answer> tags. "
241
+ "The output should look like this: <answer>[[\"start_time\", \"end_time\"], ...]</answer>. "
242
+ "Ensure each timestamp is in seconds (e.g., 'xx.xx')."
243
+ )
244
+ message.append(dict(type="text", value=user_prompt))
245
+ elif line["category"] == "object":
246
+ user_prompt = ""
247
+ message = []
248
+ video_path = line["video"]
249
+ clue_timestamp_list = []
250
+ for clue in json.loads(line["answer"]):
251
+ if clue["timestamp"] not in clue_timestamp_list:
252
+ clue_timestamp_list.append(clue["timestamp"])
253
+ image_paths, width, height = self.save_video_frames_clue(
254
+ video_path, uid=line["video_uid"], timestamp_list=clue_timestamp_list
255
+ )
256
+ message.append(
257
+ dict(type="text", value=f"There are {len(image_paths)} frames in the size of {width}x{height}"))
258
+ for idx, im in enumerate(image_paths):
259
+ message.append(dict(type="text", value=f"Frame{idx + 1}:"))
260
+ message.append(dict(type="image", value=im))
261
+ user_prompt += (
262
+ f"Answer the question '{line['question']}', "
263
+ "including the bounding box for the query object in the first frame "
264
+ "where it appears. For subsequent frames where the object appears, "
265
+ "do not provide the bounding box again. "
266
+ "Format your answer in JSON, enclosed within <answer> and </answer> tags. "
267
+ "The output should look like this: "
268
+ "<answer>{\"Frame1\": [[x_min, y_min, x_max, y_max]], \"Frame2\": [...],...}</answer>. "
269
+ "In the output, each frame should either contain the bounding box of the object "
270
+ "(if it appears for the first time in that frame) or an empty list `[]` "
271
+ "(if the object does not appear or it has already been labeled in a previous frame). "
272
+ "Ensure that bounding boxes are listed as [x_min, y_min, x_max, y_max]."
273
+ )
274
+ message.append(dict(type="text", value=user_prompt))
275
+ elif line["category"] == "attribute":
276
+ user_prompt = ""
277
+ message = []
278
+ video_path = line["video"]
279
+ clue_timestamp_list = []
280
+ for clue_ in json.loads(line["answer"]):
281
+ for clue in clue_:
282
+ if clue["timestamp"] not in clue_timestamp_list:
283
+ clue_timestamp_list.append(clue["timestamp"])
284
+ image_paths, width, height = self.save_video_frames_clue(
285
+ video_path, uid=line["video_uid"], timestamp_list=clue_timestamp_list
286
+ )
287
+ message.append(dict(
288
+ type="text",
289
+ value=f"There are {len(image_paths)} frames in the size of {width}x{height}"))
290
+ for idx, im in enumerate(image_paths):
291
+ message.append(dict(type="text", value=f"Frame{idx + 1}:"))
292
+ message.append(dict(type="image", value=im))
293
+ user_prompt += (
294
+ f"Answer the question '{line['question']}', clustering the objects according to the question. "
295
+ "For each unique cluster, assign a unique label and return the bounding box for each object in "
296
+ "the first frame where it appears. For subsequent frames where the object appears, "
297
+ "do not output anything. "
298
+ "Format your answer in JSON, enclosed within <answer> and </answer> tags. "
299
+ "The output should look like this: "
300
+ "<answer>{\"Frame 1\": [{\"bbox\": [x_min, y_min, x_max, y_max], 'label': \"Label 1\"}], "
301
+ "\"Frame 2\": [...], ...}</answer>. "
302
+ "In the output, each frame should either contain the bounding box and label for the object "
303
+ "(if it appears for the first time in that frame) or an empty list `[]` "
304
+ "(if the object has already been labeled or does not appear in that frame). "
305
+ "The label should correspond to a unique object cluster according to the question."
306
+ )
307
+ message.append(dict(type="text", value=user_prompt))
308
+ print(message)
309
+ return message
310
+
311
+ def save_video_frames(self, video, uid, num_frames=8, fps=-1):
312
+
313
+ if type(uid) is not str:
314
+ uid = str(uid)
315
+ import decord
316
+ vid_path = osp.join(self.data_root, video)
317
+ vid = decord.VideoReader(vid_path)
318
+ vid_fps = vid.get_avg_fps()
319
+ n_frames = len(vid)
320
+
321
+ if num_frames > 0 and fps < 0:
322
+ step_size = len(vid) / (num_frames + 1)
323
+ indices = [int(i * step_size) for i in range(1, num_frames + 1)]
324
+
325
+ frame_paths = self.frame_paths(uid)
326
+ elif fps > 0:
327
+ total_duration = n_frames / vid_fps
328
+ required_frames = int(total_duration * fps)
329
+ step_size = vid_fps / fps
330
+ indices = [int(i * step_size) for i in range(required_frames)]
331
+ frame_paths = self.frame_paths_fps(uid, len(indices))
332
+
333
+ # Save and validate frames
334
+ valid_paths = []
335
+ valid_indices = []
336
+ lock_path = osp.splitext(vid_path)[0] + '.lock'
337
+ with portalocker.Lock(lock_path, 'w', timeout=30):
338
+ if not np.all([osp.exists(p) for p in frame_paths]):
339
+ images = [vid[i].asnumpy() for i in indices]
340
+ for i, (img_array, path) in enumerate(zip(images, frame_paths)):
341
+ if osp.exists(path):
342
+ try:
343
+ with Image.open(path) as img:
344
+ img.verify()
345
+ valid_paths.append(path)
346
+ valid_indices.append(indices[i])
347
+ except Exception:
348
+ continue
349
+ else:
350
+ try:
351
+ img = Image.fromarray(img_array)
352
+ img.save(path)
353
+ img.verify()
354
+ valid_paths.append(path)
355
+ valid_indices.append(indices[i])
356
+ except Exception:
357
+ continue
358
+ else:
359
+ for i, path in enumerate(frame_paths):
360
+ try:
361
+ with Image.open(path) as img:
362
+ img.verify()
363
+ valid_paths.append(path)
364
+ valid_indices.append(indices[i])
365
+ except Exception:
366
+ continue
367
+
368
+ return valid_paths, valid_indices, vid_fps
369
+
370
+ def evaluate(self, eval_file, **judge_kwargs):
371
+
372
+ assert get_file_extension(eval_file) in ['xlsx', 'json', 'tsv'], \
373
+ 'data file should be an supported format (xlsx/json/tsv) file'
374
+
375
+ tgt_file = get_intermediate_file_path(eval_file, '_rating', 'json')
376
+ score_file = get_intermediate_file_path(eval_file, '_score', 'csv')
377
+
378
+ data = load(eval_file)
379
+
380
+ data_un = data[~pd.isna(data["prediction"])]
381
+ data_pred_na = data[pd.isna(data["prediction"])]
382
+
383
+ data_pred_na["score"] = -1
384
+
385
+ scores_df = data_un.apply(
386
+ lambda row: post_process(
387
+ response=row["prediction"],
388
+ right_answer=row["answer"],
389
+ task_mode=row["task_mode"],
390
+ category=row["category"]
391
+ ),
392
+ axis=1,
393
+ result_type='expand'
394
+ )
395
+
396
+ data_un = pd.concat([data_un, scores_df], axis=1)
397
+
398
+ data = pd.concat([data_pred_na, data_un])
399
+
400
+ rejected_count = (data["score"] == -1).sum()
401
+
402
+ print(
403
+ f"Among {len(data)} questions, "
404
+ f"failed to obtain prediction for {len(data_pred_na)} questions, "
405
+ f"failed to obtain the score for {rejected_count - len(data_pred_na)} questions. "
406
+ f"Those questions will be counted as -1 score in ALL rating, and will not be counted in VALID rating."
407
+ )
408
+
409
+ dump(data, score_file)
410
+
411
+ rating = rating_func(score_file)
412
+
413
+ dump(rating, tgt_file)
414
+
415
+ return rating
reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/CGAVCounting/requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ scipy
2
+ word2number
reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/CGAVCounting/utils.py ADDED
@@ -0,0 +1,423 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import math
3
+ import re
4
+ import signal
5
+ import zipfile
6
+ from pathlib import Path
7
+
8
+ import numpy as np
9
+ from tqdm import tqdm
10
+
11
+ from vlmeval.smp import load
12
+
13
+
14
+ def rating_func(data_path):
15
+ df = load(data_path)
16
+
17
+ task_mode_fields = {
18
+ "long_acc": ["acc", "oboa", "mae", "rmse"],
19
+ "ref_acc": ["acc", "oboa", "mae", "rmse"],
20
+ "clue_acc": ["wcs", "ifa"],
21
+ }
22
+
23
+ rating = {}
24
+
25
+ for task_mode, fields in task_mode_fields.items():
26
+ sub_df = df[df["task_mode"] == task_mode]
27
+ for field in fields:
28
+ values = sub_df[field]
29
+ if field == "rmse":
30
+ # RMSE: sqrt(mean(x^2))
31
+ rmse_val = np.sqrt(values.mean())
32
+ rating[f"{task_mode}/rmse"] = round(rmse_val, 4)
33
+ else:
34
+ rating[f"{task_mode}/{field}"] = round(values.mean(), 4)
35
+
36
+ return rating
37
+
38
+
39
+ def get_timestampes(frame_indices, fps):
40
+ seconds = list(map(lambda x: str(round(x / fps, 4)), frame_indices))
41
+ timestamps = ", ".join(seconds)
42
+ return "A total of {frame_num} frames are sampled. Their corresponding timestamps are:\n\n{timestamps}\n\n".format(
43
+ frame_num=len(frame_indices), timestamps=timestamps
44
+ )
45
+
46
+
47
+ def time_str_to_seconds(time_str: str) -> float:
48
+ time_str = time_str.strip()
49
+ if '.' in time_str:
50
+ time_main, milliseconds = time_str.split('.')
51
+ milliseconds = float(f"0.{milliseconds}")
52
+ else:
53
+ time_main = time_str
54
+ milliseconds = 0.0
55
+
56
+ parts = list(map(int, time_main.split(":")))
57
+
58
+ if len(parts) == 2:
59
+ minutes, seconds = parts
60
+ total_seconds = minutes * 60 + seconds
61
+ elif len(parts) == 3:
62
+ hours, minutes, seconds = parts
63
+ total_seconds = hours * 3600 + minutes * 60 + seconds
64
+ else:
65
+ raise ValueError(f"Invalid time format: {time_str}")
66
+
67
+ return total_seconds + milliseconds
68
+
69
+
70
+ def extract_outer_json(text):
71
+ stack = []
72
+ start_idx = None
73
+ opening = {'{': '}', '[': ']'}
74
+ closing = {'}': '{', ']': '['}
75
+
76
+ for i, char in enumerate(text):
77
+ if char in opening:
78
+ if not stack:
79
+ start_idx = i # 最外层起点
80
+ stack.append(char)
81
+ elif char in closing:
82
+ if stack and stack[-1] == closing[char]:
83
+ stack.pop()
84
+ if not stack and start_idx is not None:
85
+ candidate = text[start_idx:i + 1]
86
+ try:
87
+ return json.dumps(json.loads(candidate))
88
+ except json.JSONDecodeError:
89
+ continue # 尝试下一个 JSON 块
90
+ return None
91
+
92
+
93
+ def compute_tiou(t1, t2):
94
+ """Temporal IoU"""
95
+ inter_start = max(t1[0], t2[0])
96
+ inter_end = min(t1[1], t2[1])
97
+ inter = max(0.0, inter_end - inter_start)
98
+ union = max(t1[1], t2[1]) - min(t1[0], t2[0])
99
+ return inter / union if union > 0 else 0.0
100
+
101
+
102
+ def compute_sIoU(box1, box2):
103
+ """
104
+ Complete IoU (sIoU) between two bounding boxes.
105
+ Args:
106
+ box1 (list or np.array): [x1, y1, x2, y2] of ground truth box
107
+ box2 (list or np.array): [x1, y1, x2, y2] of predicted box
108
+
109
+ Returns:
110
+ IoU (float): The IoU score between the two boxes.
111
+ """
112
+
113
+ # Ensure the coordinates are ordered: [min_x, min_y, max_x, max_y]
114
+ box1 = np.array([min(box1[0], box1[2]), min(box1[1], box1[3]),
115
+ max(box1[0], box1[2]), max(box1[1], box1[3])])
116
+ box2 = np.array([min(box2[0], box2[2]), min(box2[1], box2[3]),
117
+ max(box2[0], box2[2]), max(box2[1], box2[3])])
118
+
119
+ # Compute the intersection area
120
+ inter_x1 = max(box1[0], box2[0])
121
+ inter_y1 = max(box1[1], box2[1])
122
+ inter_x2 = min(box1[2], box2[2])
123
+ inter_y2 = min(box1[3], box2[3])
124
+
125
+ inter_area = max(0, inter_x2 - inter_x1) * max(0, inter_y2 - inter_y1)
126
+
127
+ # Compute areas of the individual boxes
128
+ area1 = (box1[2] - box1[0]) * (box1[3] - box1[1])
129
+ area2 = (box2[2] - box2[0]) * (box2[3] - box2[1])
130
+
131
+ # Compute union area
132
+ union = area1 + area2 - inter_area
133
+ iou = inter_area / union if union > 0 else 0.0
134
+
135
+ return iou
136
+
137
+
138
+ def greedy_matching(gt_instances, pred_instances, iou_func):
139
+ """Greedy matching based on maximum IoU"""
140
+ unmatched_gt = set(range(len(gt_instances)))
141
+ unmatched_pred = set(range(len(pred_instances)))
142
+ matches = []
143
+
144
+ while unmatched_gt and unmatched_pred:
145
+ max_iou = -1
146
+ best_match = None
147
+ for gt_idx in unmatched_gt:
148
+ for pred_idx in unmatched_pred:
149
+ iou = iou_func(gt_instances[gt_idx], pred_instances[pred_idx])
150
+ if iou > max_iou:
151
+ max_iou = iou
152
+ best_match = (gt_idx, pred_idx)
153
+
154
+ if best_match:
155
+ gt_idx, pred_idx = best_match
156
+ matches.append((gt_idx, pred_idx))
157
+ unmatched_gt.remove(gt_idx)
158
+ unmatched_pred.remove(pred_idx)
159
+
160
+ return matches
161
+
162
+
163
+ def compute_cluster_pair_wcs(gt, pred, iou_type):
164
+ if iou_type == 'tIoU':
165
+ loc_sum = 0.0
166
+ for g in gt:
167
+ loc_sum += max([compute_tiou(g, p) for p in pred] or [0.0])
168
+ loc_acc = loc_sum / len(gt) if gt else 0.0
169
+ count_penalty = 1.0 - abs(len(pred) - len(gt)) / max(len(gt), 1)
170
+ # count_penalty = 1.0
171
+ return math.sqrt(loc_acc * max(0, count_penalty))
172
+
173
+ elif iou_type == 'sIoU':
174
+ # group by frame index
175
+ from collections import defaultdict
176
+ gt_by_f = defaultdict(list)
177
+ pred_by_f = defaultdict(list)
178
+ for f, box in gt:
179
+ gt_by_f[f].append(box)
180
+ for f, box in pred:
181
+ pred_by_f[f].append(box)
182
+
183
+ all_f = set(gt_by_f) | set(pred_by_f)
184
+ wcs = 0.0
185
+ for f in all_f:
186
+ gt_f = gt_by_f.get(f, [])
187
+ pred_f = pred_by_f.get(f, [])
188
+ matches = greedy_matching(gt_f, pred_f, compute_sIoU)
189
+ loc_sum = sum([compute_sIoU(gt_f[i], pred_f[j]) for i, j in matches])
190
+ loc_acc = loc_sum / len(gt_f) if gt_f else 0.0
191
+ count_penalty = 1.0 - abs(len(pred_f) - len(gt_f)) / max(len(gt_f), 1)
192
+ # count_penalty = 1.0
193
+ wcs += math.sqrt(loc_acc * max(0, count_penalty))
194
+ return wcs / max(len(all_f), 1)
195
+
196
+ else:
197
+ raise ValueError("Unsupported iou_type")
198
+
199
+
200
+ class TimeoutException(Exception):
201
+ pass
202
+
203
+
204
+ def timeout_handler(signum, frame):
205
+ raise TimeoutException("Function execution exceeded the time limit.")
206
+
207
+
208
+ def compute_wcs_unlabeled(gt_clusters, pred_clusters, iou_type='tIoU',
209
+ timeout=10): # 主要是给attribute用的,但是object和event视作一个cluster也能用
210
+ from scipy.optimize import linear_sum_assignment
211
+
212
+ # Set the timeout signal handler
213
+ signal.signal(signal.SIGALRM, timeout_handler)
214
+ signal.alarm(timeout) # Set the alarm to go off in 'timeout' seconds
215
+
216
+ try:
217
+ # Original function logic
218
+ K = len(gt_clusters)
219
+ M = len(pred_clusters)
220
+
221
+ # Build cost matrix (we want max score → min cost)
222
+ score_matrix = np.zeros((K, M))
223
+ for i in range(K):
224
+ for j in range(M):
225
+ score_matrix[i, j] = compute_cluster_pair_wcs(gt_clusters[i], pred_clusters[j], iou_type)
226
+
227
+ cost_matrix = -score_matrix # maximize score → minimize cost
228
+
229
+ row_ind, col_ind = linear_sum_assignment(cost_matrix)
230
+
231
+ matched_scores = [score_matrix[i, j] for i, j in zip(row_ind, col_ind)]
232
+
233
+ # WCS = average over gt clusters (including unmatched = 0)
234
+ total_wcs = sum(matched_scores)
235
+ return total_wcs / K
236
+
237
+ except TimeoutException:
238
+ print(gt_clusters, pred_clusters)
239
+ print("Function execution exceeded the time limit.")
240
+ return None # or you can return some default value to indicate timeout
241
+
242
+ finally:
243
+ signal.alarm(0) # Cancel the alarm after the function completes or times out
244
+
245
+
246
+ def post_process(response, right_answer, task_mode, category):
247
+ from word2number import w2n
248
+ if task_mode in ["long_acc", "ref_acc"]:
249
+ result = {"acc": 0, "oboa": 0, "mae": 0, "rmse": 0}
250
+ if response:
251
+ try:
252
+ pred = w2n.word_to_num(response)
253
+ except Exception:
254
+ pred = 0
255
+ if abs(float(right_answer) - float(pred)) <= 1e-5:
256
+ result["acc"] = 1
257
+
258
+ if abs(float(right_answer) - float(pred)) <= 1:
259
+ result["oboa"] = 1
260
+
261
+ if abs(float(right_answer) - float(pred)) <= max(2 * float(right_answer), 100):
262
+ result["mae"] = abs(float(right_answer) - float(pred))
263
+ result["rmse"] = abs(float(right_answer) - float(pred)) ** 2
264
+ else:
265
+ result["mae"] = abs(float(right_answer) * 2)
266
+ result["rmse"] = abs(float(right_answer) * 2) ** 2
267
+ elif task_mode == "clue_acc":
268
+ result = {"wcs": 0, "ifa": 0}
269
+ if response:
270
+ clues = json.loads(right_answer)
271
+ content_match = re.search(r"<answer>(.*?)</answer>", response, re.DOTALL)
272
+ student_answer = content_match.group(1).strip() if content_match else response.strip()
273
+ j = None
274
+ try:
275
+ try:
276
+ j = json.loads(student_answer)
277
+ except Exception:
278
+ j = json.loads(extract_outer_json(student_answer))
279
+ except Exception:
280
+ pass
281
+ if j is not None:
282
+ try:
283
+ if category == "event":
284
+ pred = []
285
+ for e in j:
286
+
287
+ if isinstance(e[0], str) and isinstance(e[1], str) and ":" in e[0] and ":" in e[1]:
288
+ pred.append([time_str_to_seconds(e[0]), time_str_to_seconds(e[1])])
289
+ else:
290
+ pred.append([float(e[0].split(" ")[0]) if isinstance(e[0], str) else e[0],
291
+ float(e[1].split(" ")[0]) if isinstance(e[1], str) else e[1]])
292
+ gt = []
293
+ for e in clues:
294
+ gt.append([float(e['start']), float(e['end'])])
295
+
296
+ result["wcs"] = compute_wcs_unlabeled([gt], [pred], "tIoU")
297
+ result["ifa"] = 1
298
+ elif category == "object":
299
+ gt = []
300
+ clue_timestamp_list = []
301
+ for clue in clues:
302
+ if clue["timestamp"] not in clue_timestamp_list:
303
+ clue_timestamp_list.append(clue["timestamp"])
304
+ for clue in clues:
305
+ gt.append((clue_timestamp_list.index(clue["timestamp"]), clue['bbox']))
306
+ pred = []
307
+ for key in j.keys():
308
+ if "Frame" not in key:
309
+ continue
310
+ idx = int(key.replace("Frame", "")) - 1
311
+ if len(j[key]) == 0:
312
+ continue
313
+ if isinstance(j[key][0], list) and len(j[key][0]) == 4:
314
+ for e in j[key]:
315
+ if isinstance(e, list) and len(e) == 4:
316
+ pred.append((idx, e))
317
+ elif isinstance(j[key][0], list) and len(j[key][0]) == 2:
318
+ for ii in range(int(len(j[key]) // 2)):
319
+ if isinstance(j[key][ii * 2], list) and len(j[key][ii * 2]) == 2 and isinstance(
320
+ j[key][ii * 2 + 1], list) and len(j[key][ii * 2 + 1]) == 2:
321
+ pred.append((idx, [j[key][ii * 2][0], j[key][ii * 2][1], j[key][ii * 2 + 1][0],
322
+ j[key][ii * 2 + 1][1]]))
323
+ result["wcs"] = compute_wcs_unlabeled([gt], [pred], "sIoU")
324
+ result["ifa"] = 1
325
+ elif category == "attribute":
326
+ gt = []
327
+ clue_timestamp_list = []
328
+ for clue_ in clues:
329
+ for clue in clue_:
330
+ if clue["timestamp"] not in clue_timestamp_list:
331
+ clue_timestamp_list.append(clue["timestamp"])
332
+ for clue_ in clues:
333
+ gt_ = []
334
+ for clue in clue_:
335
+ gt_.append((clue_timestamp_list.index(clue["timestamp"]), clue['bbox']))
336
+ gt.append(gt_)
337
+ pred = {}
338
+ for key in j.keys():
339
+ if "Frame" not in key:
340
+ continue
341
+ idx = int(key.replace("Frame", "")) - 1
342
+ for e in j[key]:
343
+ if e['label'] not in pred.keys():
344
+ pred[e['label']] = []
345
+ if 'bbox' in e:
346
+ if isinstance(e['bbox'], list) and len(e['bbox']) == 4:
347
+ pred[e['label']].append((idx, e['bbox']))
348
+ if 'bbox_2d' in e:
349
+ if isinstance(e['bbox_2d'], list) and len(e['bbox_2d']) == 4:
350
+ pred[e['label']].append((idx, e['bbox_2d']))
351
+ pred_list = [pred[key] for key in pred]
352
+ result["wcs"] = compute_wcs_unlabeled(gt, pred_list, "sIoU")
353
+ result["ifa"] = 1
354
+ except Exception:
355
+ pass
356
+
357
+ return result
358
+
359
+
360
+ def get_chunk_number(filename):
361
+ try:
362
+ num = filename.split("chunk_")[1].split(".zip")[0]
363
+ return int(num)
364
+ except Exception:
365
+ return float('inf')
366
+
367
+
368
+ def auto_merge_and_unzip_parts(target_dir, extract_dir, zip_prefix=None):
369
+ target_dir = Path(target_dir)
370
+ extract_dir = Path(extract_dir)
371
+ extract_dir.mkdir(parents=True, exist_ok=True)
372
+
373
+ # 匹配 zip 分卷:例如 video_chunk_001.zip.part000
374
+ part_files = sorted(target_dir.glob("*.zip.part*"))
375
+ groups = {}
376
+
377
+ # 分组:根据前缀提取 group 名(即 zip 文件名)
378
+ for part_file in part_files:
379
+ match = re.match(r"(.*\.zip)\.part\d+$", part_file.name)
380
+ if match:
381
+ zip_name = match.group(1)
382
+ if zip_prefix is None or Path(zip_name).stem.startswith(zip_prefix):
383
+ groups.setdefault(zip_name, []).append(part_file)
384
+
385
+ if not groups:
386
+ print(f"No matching zip parts found with prefix: {zip_prefix}")
387
+ return
388
+
389
+ # 合并每一组分卷 -> 解压
390
+ for zip_name, parts in tqdm(groups.items(), desc="Merging and unzipping"):
391
+ parts = sorted(parts, key=lambda p: int(p.name.split("part")[-1]))
392
+ zip_path = target_dir / zip_name
393
+
394
+ # 合并分卷
395
+ with open(zip_path, 'wb') as outfile:
396
+ for part in parts:
397
+ with open(part, 'rb') as infile:
398
+ outfile.write(infile.read())
399
+
400
+ # 解压合并后的 zip 文件
401
+ with zipfile.ZipFile(zip_path, 'r') as zip_ref:
402
+ zip_ref.extractall(extract_dir)
403
+
404
+ # 删除合并后的 zip 文件(可注释)
405
+ zip_path.unlink()
406
+
407
+
408
+ def unzip_hf_zip(target_dir):
409
+ target_dir = Path(target_dir)
410
+
411
+ videos_dir = target_dir / "cg_videos_720p"
412
+ ref_videos_dir = target_dir / "ref_videos"
413
+
414
+ if videos_dir.exists() and ref_videos_dir.exists():
415
+ print("all target dirs exist, skip.")
416
+ return
417
+
418
+ videos_dir.mkdir(parents=True, exist_ok=True)
419
+
420
+ auto_merge_and_unzip_parts(target_dir, ref_videos_dir, zip_prefix="ref_videos")
421
+ auto_merge_and_unzip_parts(target_dir, videos_dir, zip_prefix="videos")
422
+
423
+ print("sucessfully unzip all files.")
reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/EgoExoBench/tf2023_preprocess.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+
4
+ import cv2
5
+ import numpy as np
6
+
7
+ # replace the path with your actual path
8
+ ann_file = 'EgoExoBench/MCQ/Ego-Exo-View-Transition/ego_wearer_identification.json'
9
+
10
+
11
+ def add_bbox(bbox_img_path):
12
+
13
+ bbox_dir = os.path.dirname(bbox_img_path)
14
+ os.makedirs(bbox_dir, exist_ok=True)
15
+ vid, frame_idx, person_id = bbox_img_path.split('/')[-4], bbox_img_path.split('/')[-2], bbox_img_path.split('/')[-1].split('.')[0] # noqa: E501
16
+ import os.path as osp
17
+ json_file = os.path.join(osp.dirname(osp.dirname(osp.dirname(osp.dirname(bbox_img_path)))), vid, 'Segmentation/T', frame_idx + '.json') # noqa: E501
18
+ ori_img_path = json_file.replace('.json', '.jpg')
19
+
20
+ with open(json_file, mode='r', encoding="utf-8") as f:
21
+ configs = json.load(f)
22
+ shapes = configs["shapes"]
23
+
24
+ mask = np.zeros((configs["imageHeight"], configs["imageWidth"], 1), np.uint8)
25
+
26
+ if not os.path.exists(ori_img_path):
27
+ ori_img_path = ori_img_path.replace('T/', '')
28
+
29
+ if not os.path.exists(ori_img_path):
30
+ ori_img_path = ori_img_path.replace('Segmentation/', 'frame/T/')
31
+
32
+ original_image = cv2.imread(ori_img_path)
33
+
34
+ for shape in shapes:
35
+ if shape['label'] != person_id:
36
+ continue
37
+
38
+ cv2.fillPoly(mask, [np.array(shape["points"], np.int32)], 1)
39
+
40
+ retval, labels, stats, centroids = cv2.connectedComponentsWithStats(mask, connectivity=8)
41
+ stats = stats[stats[:, 4].argsort()]
42
+ bboxs = stats[:-1]
43
+
44
+ for b in bboxs:
45
+ x0, y0 = b[0], b[1]
46
+ x1 = b[0] + b[2]
47
+ y1 = b[1] + b[3]
48
+
49
+ start_point, end_point = (x0, y0), (x1, y1)
50
+ color = (0, 0, 255)
51
+ thickness = 2
52
+ mask_bboxs = cv2.rectangle(original_image, start_point, end_point, color, thickness)
53
+ mask_bboxs = cv2.resize(mask_bboxs, (540, 360))
54
+ cv2.imwrite(bbox_img_path, mask_bboxs)
55
+ return
56
+
57
+
58
+ def rescale_img(img_path, width, height):
59
+ img = cv2.imread(img_path)
60
+ resized_img = cv2.resize(img, (width, height))
61
+ cv2.imwrite(img_path, resized_img)
62
+
63
+
64
+ with open(ann_file, 'r') as f:
65
+ ann_data = json.load(f)
66
+ for aitem in ann_data.values():
67
+ image_paths = []
68
+ for oitem in aitem['options']:
69
+ add_bbox(oitem['image_paths'][0])
70
+
71
+ for img_path in aitem['query']['image_paths']:
72
+ rescale_img(img_path, 960, 540)
reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/EgoExoBench/utils.py ADDED
@@ -0,0 +1,758 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import math
3
+ import numbers
4
+ import random
5
+ import re
6
+
7
+ import numpy as np
8
+ import pandas as pd
9
+ import torch
10
+ import torchvision
11
+ from PIL import Image, ImageOps
12
+
13
+ from vlmeval.smp import load
14
+ from ..utils.multiple_choice import extract_answer_from_item
15
+
16
+
17
+ def get_dimension_rating(data_path, category_type='subtask_type'):
18
+ data = load(data_path)
19
+ result_board = {}
20
+ for idx, item in data.iterrows():
21
+ if item[category_type] not in result_board:
22
+ result_board[item[category_type]] = [0, 0]
23
+ result_board[item[category_type]][1] += 1
24
+ if item['score']:
25
+ result_board[item[category_type]][0] += 1
26
+
27
+ correct = 0
28
+ total = 0
29
+ for key, value in result_board.items():
30
+ correct += value[0]
31
+ total += value[1]
32
+ result_board[key].append(f'{value[0] / value[1] * 100:.2f}%')
33
+
34
+ result_board['overall'] = [correct, total, f'{correct / total * 100:.2f}%']
35
+
36
+ return result_board
37
+
38
+
39
+ def extract_characters_regex(s):
40
+ s = s.strip()
41
+ answer_prefixes = [
42
+ 'The best answer is',
43
+ 'The correct answer is',
44
+ 'The answer is',
45
+ 'The answer',
46
+ 'The best option is'
47
+ 'The correct option is',
48
+ 'Best answer:'
49
+ 'Best option:',
50
+ 'Answer:',
51
+ 'Option:',
52
+ ]
53
+ for answer_prefix in answer_prefixes:
54
+ s = s.replace(answer_prefix, '')
55
+
56
+ if len(s.split()) > 10 and not re.search('[ABCD]', s):
57
+ return ''
58
+ matches = re.search(r'[ABCD]', s)
59
+ if matches is None:
60
+ return ''
61
+ return matches[0]
62
+
63
+
64
+ def extract_option(model, input_item, dataset_name):
65
+ options = input_item['question'].split('\n')[1:]
66
+ for id, option in enumerate(options):
67
+ option_id = chr(ord('A') + id) + '.'
68
+ if option.find(option_id) >= 0:
69
+ input_item[chr(ord('A') + id)] = option[option.find(option_id) + len(option_id):].strip('. \n')
70
+ return extract_answer_from_item(model, input_item, dataset_name)['opt']
71
+
72
+
73
+ def process_results(score_file, model_name):
74
+ from sklearn.metrics import (accuracy_score, confusion_matrix, f1_score, precision_score,
75
+ recall_score)
76
+ data = pd.read_excel(score_file)
77
+
78
+ # Create the prediction column based on the Score and Answer columns
79
+ data['prediction'] = data.apply(
80
+ lambda row: row['answer'] if row['score'] == 1 else ('Yes' if row['answer'] == 'No' else 'No'), axis=1
81
+ )
82
+
83
+ # Recompute metrics for tamper types including 'original' in the calculations but exclude 'original' from the output
84
+ grouped_metrics_with_original_excluding_original = {}
85
+
86
+ original_group = data[data['tamper_type'] == 'original']
87
+
88
+ for tamper_type, group in data[data['tamper_type'] != 'original'].groupby('tamper_type'):
89
+ # Combine the current group with the 'original' group
90
+ combined_group = pd.concat([group, original_group])
91
+
92
+ # Extract ground truth and predictions for the combined group
93
+ y_true_group = combined_group['answer'].map({'Yes': 1, 'No': 0})
94
+ y_pred_group = combined_group['prediction'].map({'Yes': 1, 'No': 0})
95
+
96
+ # Calculate metrics for the combined group
97
+ accuracy = accuracy_score(y_true_group, y_pred_group)
98
+ precision = precision_score(y_true_group, y_pred_group, zero_division=0)
99
+ recall = recall_score(y_true_group, y_pred_group, zero_division=0)
100
+ f1 = f1_score(y_true_group, y_pred_group, zero_division=0)
101
+ conf_matrix = confusion_matrix(y_true_group, y_pred_group)
102
+
103
+ # Store metrics for the tamper_type
104
+ grouped_metrics_with_original_excluding_original[tamper_type] = {
105
+ "Accuracy": accuracy,
106
+ "Precision": precision,
107
+ "Recall": recall,
108
+ "F1 Score": f1,
109
+ "Confusion Matrix": conf_matrix.tolist() # Convert to list for JSON compatibility
110
+ }
111
+
112
+ # Add the Macro Average row to the Dictionary
113
+ # grouped_metrics_with_original_excluding_original["overall"] = macro_averages
114
+
115
+ # Display the metrics in a dataframe for clarity
116
+ df_grouped_metrics_with_original_excluding_original = pd.DataFrame.from_dict(
117
+ grouped_metrics_with_original_excluding_original, orient='index'
118
+ )
119
+
120
+ # Compute Macro Averages for Accuracy, Precision, Recall, and F1 Score
121
+ macro_averages = {
122
+ "Accuracy": df_grouped_metrics_with_original_excluding_original["Accuracy"].mean(),
123
+ "Precision": df_grouped_metrics_with_original_excluding_original["Precision"].mean(),
124
+ "Recall": df_grouped_metrics_with_original_excluding_original["Recall"].mean(),
125
+ "F1 Score": df_grouped_metrics_with_original_excluding_original["F1 Score"].mean(),
126
+ "Confusion Matrix": "N/A" # Macro average doesn't have a meaningful confusion matrix
127
+ }
128
+
129
+ # # Add the Macro Average row to the DataFrame
130
+ df_grouped_metrics_with_original_excluding_original.loc["overall"] = macro_averages
131
+
132
+ # df_grouped_metrics_with_original_excluding_original
133
+ metrics_dict = json.loads(df_grouped_metrics_with_original_excluding_original.T.to_json())
134
+ # Process Model Level Metrics
135
+ formatted_data = []
136
+ for task, task_metrics in metrics_dict.items():
137
+ task_metrics['Model'] = model_name
138
+ task_metrics['Task'] = task
139
+ formatted_data.append(task_metrics)
140
+
141
+ df_metrics = pd.DataFrame(formatted_data)
142
+
143
+ # Reorder columns to make 'Model' and 'Task' appear first
144
+ columns_order = ['Model', 'Task'] + [col for col in df_metrics.columns if col not in ['Model', 'Task']]
145
+ df_metrics = df_metrics[columns_order]
146
+
147
+ return df_metrics
148
+
149
+
150
+ def aggregate_metrics_with_macro_average(score_file):
151
+ from sklearn.metrics import (accuracy_score, confusion_matrix, f1_score, precision_score,
152
+ recall_score)
153
+
154
+ # Load data
155
+ data = pd.read_excel(score_file)
156
+
157
+ # Create the prediction column based on the Score and Answer columns
158
+ data['prediction'] = data.apply(
159
+ lambda row: row['answer'] if row['score'] == 1 else ('Yes' if row['answer'] == 'No' else 'No'), axis=1
160
+ )
161
+
162
+ # Initialize a dictionary to store metrics
163
+ task_type_metrics = {}
164
+
165
+ # Process each task_type separately
166
+ for task_type, task_group in data.groupby('task_type'):
167
+ # Separate the 'original' group for the current task_type
168
+ original_group = task_group[task_group['tamper_type'] == 'original']
169
+
170
+ # Skip if there is no 'original' data for this task_type
171
+ if original_group.empty:
172
+ continue
173
+
174
+ # Process each tamper type for the current task_type (excluding 'original')
175
+ tamper_metrics = {}
176
+ for tamper_type, tamper_group in task_group[task_group['tamper_type'] != 'original'].groupby('tamper_type'):
177
+
178
+ # Combine the tamper group with the original group of the current task_type
179
+ combined_group = pd.concat([tamper_group, original_group])
180
+
181
+ # Map answers and predictions to binary values
182
+ y_true = combined_group['answer'].map({'Yes': 1, 'No': 0})
183
+ y_pred = combined_group['prediction'].map({'Yes': 1, 'No': 0})
184
+
185
+ # Compute metrics
186
+ accuracy = accuracy_score(y_true, y_pred)
187
+ precision = precision_score(y_true, y_pred, zero_division=0)
188
+ recall = recall_score(y_true, y_pred, zero_division=0)
189
+ f1 = f1_score(y_true, y_pred, zero_division=0)
190
+ conf_matrix = confusion_matrix(y_true, y_pred)
191
+
192
+ # Store metrics for the tamper_type
193
+ tamper_metrics[tamper_type] = {
194
+ "Accuracy": accuracy,
195
+ "Precision": precision,
196
+ "Recall": recall,
197
+ "F1 Score": f1,
198
+ "Confusion Matrix": conf_matrix.tolist() # Convert to list for JSON compatibility
199
+ }
200
+
201
+ # Compute Macro Averages for the current task_type
202
+ metrics_df = pd.DataFrame(tamper_metrics).T
203
+ macro_average = {
204
+ "Accuracy": metrics_df["Accuracy"].mean(),
205
+ "Precision": metrics_df["Precision"].mean(),
206
+ "Recall": metrics_df["Recall"].mean(),
207
+ "F1 Score": metrics_df["F1 Score"].mean(),
208
+ "Confusion Matrix": "N/A" # Macro average doesn't have a meaningful confusion matrix
209
+ }
210
+
211
+ # Add the macro average as "overall" for the task_type
212
+ tamper_metrics["overall"] = macro_average
213
+
214
+ # Add tamper metrics for the current task_type to the main dictionary
215
+ task_type_metrics[task_type] = tamper_metrics
216
+
217
+ # Transform the nested dictionary into a DataFrame
218
+ dataframes = []
219
+ for task_type, metrics in task_type_metrics.items():
220
+ task_df = pd.DataFrame.from_dict(metrics, orient='index')
221
+ task_df['task_type'] = task_type # Add the task_type as a column
222
+ dataframes.append(task_df)
223
+
224
+ # Combine all task-specific DataFrames into a single DataFrame
225
+ result_df = pd.concat(dataframes).reset_index().rename(columns={'index': 'tamper_type'})
226
+ # Reorder the columns to place task_type first, then tamper_type
227
+ result_df = result_df[['task_type', 'tamper_type', 'Accuracy', 'Precision', 'Recall',
228
+ 'F1 Score', 'Confusion Matrix']]
229
+
230
+ # Select only numeric columns for aggregation
231
+ numeric_columns = ['Accuracy', 'Precision', 'Recall', 'F1 Score']
232
+
233
+ # Group by task_type and tamper_type, and calculate the mean for numeric columns
234
+ average_metrics = result_df.groupby(['task_type', 'tamper_type'])[numeric_columns].mean().reset_index()
235
+
236
+ return average_metrics
237
+
238
+
239
+ def check_ans(pred, gt):
240
+ """
241
+ Checks if the predicted answer matches the ground truth.
242
+
243
+ Args:
244
+ pred (str): The predicted answer.
245
+ gt (str): The ground truth answer.
246
+
247
+ Returns:
248
+ bool: True if the predicted answer matches the ground truth, False otherwise.
249
+ """
250
+ # Convert both predictions and ground truths to lowercase and split them into options and contents
251
+ flag = False
252
+
253
+ # Split prediction into option and content
254
+ pred_list = pred.lower().strip().split(' ')
255
+ pred_option, _ = pred_list[0], ' '.join(pred_list[1:])
256
+
257
+ # Split ground truth into option and content
258
+ gt_list = gt.lower().strip().split(' ')
259
+ gt_option, gt_content = gt_list[0], ' '.join(gt_list[1:])
260
+
261
+ # Remove trailing period from ground truth content if present
262
+ if gt_content[-1] == '.':
263
+ gt_content = gt_content[:-1]
264
+
265
+ # Check for matching conditions
266
+ # Condition 1: If the predicted option is a substring of the ground truth option
267
+ if pred_option.replace('.', '') in gt_option:
268
+ flag = True
269
+ # Condition 2: If the ground truth option is a substring of the predicted option
270
+ elif gt_option in pred_option:
271
+ flag = True
272
+ # Condition 3: If the ground truth is a substring of the predicted answer
273
+ elif gt in pred:
274
+ flag = True
275
+
276
+ return flag
277
+
278
+
279
+ def check_ans_with_model(pred, gt, model, item, dataset_name='MVBench'):
280
+ """
281
+ Checks if the predicted answer matches the ground truth using a given model.
282
+
283
+ Args:
284
+ pred (str): The predicted answer.
285
+ gt (str): The ground truth answer.
286
+ model: A machine learning model used for additional verification.
287
+ item (dict): An item containing information about the question or task.
288
+ dataset_name (str, optional): Name of the dataset being used. Defaults to 'MVBench'.
289
+
290
+ Returns:
291
+ bool: True if the predicted answer matches the ground truth, False otherwise.
292
+ """
293
+ # Initialize flag to track match status
294
+ flag = False
295
+
296
+ # Preprocess prediction and ground truth by converting to lowercase and splitting into options and contents
297
+ pred_list = pred.lower().strip().split(' ')
298
+ pred_option, _ = pred_list[0], ' '.join(pred_list[1:])
299
+ gt_list = gt.lower().strip().split(' ')
300
+ gt_option, gt_content = gt_list[0], ' '.join(gt_list[1:])
301
+
302
+ # Remove trailing period from ground truth content if presen
303
+ if gt_content[-1] == '.':
304
+ gt_content = gt_content[:-1]
305
+
306
+ # Check for matching conditions
307
+ # Condition 1: If the predicted option is a substring of the ground truth option
308
+ if pred_option.replace('.', '') in gt_option:
309
+ flag = True
310
+ # Condition 2: If the ground truth option is a substring of the predicted option
311
+ elif gt_option in pred_option:
312
+ flag = True
313
+ # Condition 3: Use the provided model to verify the answer
314
+ elif extract_answer_from_item(model, item, dataset_name)['opt'] == item['answer']:
315
+ flag = True
316
+
317
+ return flag
318
+
319
+
320
+ def check_ans_advanced(pred, gt):
321
+ number_table = {
322
+ 0: 'zero',
323
+ 1: 'one',
324
+ 2: 'two',
325
+ 3: 'three',
326
+ 4: 'four',
327
+ 5: 'five',
328
+ 6: 'six',
329
+ 7: 'seven',
330
+ 8: 'eight',
331
+ 9: 'nine',
332
+ }
333
+ flag = False
334
+
335
+ pred_list = pred.lower().split(' ')
336
+ pred_option, _ = pred_list[0], ' '.join(pred_list[1:])
337
+ gt_list = gt.lower().split(' ')
338
+ gt_option, gt_content = gt_list[0], ' '.join(gt_list[1:])
339
+ if gt_content[-1] == '.':
340
+ gt_content = gt_content[:-1]
341
+
342
+ try:
343
+ gt_content = number_table[int(gt_content.strip('. \n'))]
344
+ print(gt_content)
345
+ except Exception:
346
+ pass
347
+
348
+ if pred_option.replace('.', '') in gt_option:
349
+ flag = True
350
+ elif gt_option in pred_option:
351
+ flag = True
352
+ elif gt_content.lower().strip('. \n') in pred.lower().strip('. \n'):
353
+ flag = True
354
+
355
+ return flag
356
+
357
+
358
+ class GroupRandomCrop(object):
359
+ def __init__(self, size):
360
+ if isinstance(size, numbers.Number):
361
+ self.size = (int(size), int(size))
362
+ else:
363
+ self.size = size
364
+
365
+ def __call__(self, img_group):
366
+
367
+ w, h = img_group[0].size
368
+ th, tw = self.size
369
+
370
+ out_images = list()
371
+
372
+ x1 = random.randint(0, w - tw)
373
+ y1 = random.randint(0, h - th)
374
+
375
+ for img in img_group:
376
+ assert (img.size[0] == w and img.size[1] == h)
377
+ if w == tw and h == th:
378
+ out_images.append(img)
379
+ else:
380
+ out_images.append(img.crop((x1, y1, x1 + tw, y1 + th)))
381
+
382
+ return out_images
383
+
384
+
385
+ class MultiGroupRandomCrop(object):
386
+ def __init__(self, size, groups=1):
387
+ if isinstance(size, numbers.Number):
388
+ self.size = (int(size), int(size))
389
+ else:
390
+ self.size = size
391
+ self.groups = groups
392
+
393
+ def __call__(self, img_group):
394
+
395
+ w, h = img_group[0].size
396
+ th, tw = self.size
397
+
398
+ out_images = list()
399
+
400
+ for i in range(self.groups):
401
+ x1 = random.randint(0, w - tw)
402
+ y1 = random.randint(0, h - th)
403
+
404
+ for img in img_group:
405
+ assert (img.size[0] == w and img.size[1] == h)
406
+ if w == tw and h == th:
407
+ out_images.append(img)
408
+ else:
409
+ out_images.append(img.crop((x1, y1, x1 + tw, y1 + th)))
410
+
411
+ return out_images
412
+
413
+
414
+ class GroupCenterCrop(object):
415
+ def __init__(self, size):
416
+ self.worker = torchvision.transforms.CenterCrop(size)
417
+
418
+ def __call__(self, img_group):
419
+ return [self.worker(img) for img in img_group]
420
+
421
+
422
+ class GroupRandomHorizontalFlip(object):
423
+ """Randomly horizontally flips the given PIL.Image with a probability of 0.5
424
+ """
425
+
426
+ def __init__(self, is_flow=False):
427
+ self.is_flow = is_flow
428
+
429
+ def __call__(self, img_group, is_flow=False):
430
+ v = random.random()
431
+ if v < 0.5:
432
+ ret = [img.transpose(Image.FLIP_LEFT_RIGHT) for img in img_group]
433
+ if self.is_flow:
434
+ for i in range(0, len(ret), 2):
435
+ # invert flow pixel values when flipping
436
+ ret[i] = ImageOps.invert(ret[i])
437
+ return ret
438
+ else:
439
+ return img_group
440
+
441
+
442
+ class GroupNormalize(object):
443
+ def __init__(self, mean, std):
444
+ self.mean = mean
445
+ self.std = std
446
+
447
+ def __call__(self, tensor):
448
+ rep_mean = self.mean * (tensor.size()[0] // len(self.mean))
449
+ rep_std = self.std * (tensor.size()[0] // len(self.std))
450
+
451
+ # TODO: make efficient
452
+ for t, m, s in zip(tensor, rep_mean, rep_std):
453
+ t.sub_(m).div_(s)
454
+
455
+ return tensor
456
+
457
+
458
+ class GroupScale(object):
459
+ """ Rescales the input PIL.Image to the given 'size'.
460
+ 'size' will be the size of the smaller edge.
461
+ For example, if height > width, then image will be
462
+ rescaled to (size * height / width, size)
463
+ size: size of the smaller edge
464
+ interpolation: Default: PIL.Image.BILINEAR
465
+ """
466
+
467
+ def __init__(self, size, interpolation=Image.BILINEAR):
468
+ self.worker = torchvision.transforms.Resize(size, interpolation)
469
+
470
+ def __call__(self, img_group):
471
+ return [self.worker(img) for img in img_group]
472
+
473
+
474
+ class GroupOverSample(object):
475
+ def __init__(self, crop_size, scale_size=None, flip=True):
476
+ self.crop_size = crop_size if not isinstance(
477
+ crop_size, int) else (crop_size, crop_size)
478
+
479
+ if scale_size is not None:
480
+ self.scale_worker = GroupScale(scale_size)
481
+ else:
482
+ self.scale_worker = None
483
+ self.flip = flip
484
+
485
+ def __call__(self, img_group):
486
+
487
+ if self.scale_worker is not None:
488
+ img_group = self.scale_worker(img_group)
489
+
490
+ image_w, image_h = img_group[0].size
491
+ crop_w, crop_h = self.crop_size
492
+
493
+ offsets = GroupMultiScaleCrop.fill_fix_offset(
494
+ False, image_w, image_h, crop_w, crop_h)
495
+ oversample_group = list()
496
+ for o_w, o_h in offsets:
497
+ normal_group = list()
498
+ flip_group = list()
499
+ for i, img in enumerate(img_group):
500
+ crop = img.crop((o_w, o_h, o_w + crop_w, o_h + crop_h))
501
+ normal_group.append(crop)
502
+ flip_crop = crop.copy().transpose(Image.FLIP_LEFT_RIGHT)
503
+
504
+ if img.mode == 'L' and i % 2 == 0:
505
+ flip_group.append(ImageOps.invert(flip_crop))
506
+ else:
507
+ flip_group.append(flip_crop)
508
+
509
+ oversample_group.extend(normal_group)
510
+ if self.flip:
511
+ oversample_group.extend(flip_group)
512
+ return oversample_group
513
+
514
+
515
+ class GroupFullResSample(object):
516
+ def __init__(self, crop_size, scale_size=None, flip=True):
517
+ self.crop_size = crop_size if not isinstance(
518
+ crop_size, int) else (crop_size, crop_size)
519
+
520
+ if scale_size is not None:
521
+ self.scale_worker = GroupScale(scale_size)
522
+ else:
523
+ self.scale_worker = None
524
+ self.flip = flip
525
+
526
+ def __call__(self, img_group):
527
+
528
+ if self.scale_worker is not None:
529
+ img_group = self.scale_worker(img_group)
530
+
531
+ image_w, image_h = img_group[0].size
532
+ crop_w, crop_h = self.crop_size
533
+
534
+ w_step = (image_w - crop_w) // 4
535
+ h_step = (image_h - crop_h) // 4
536
+
537
+ offsets = list()
538
+ offsets.append((0 * w_step, 2 * h_step)) # left
539
+ offsets.append((4 * w_step, 2 * h_step)) # right
540
+ offsets.append((2 * w_step, 2 * h_step)) # center
541
+
542
+ oversample_group = list()
543
+ for o_w, o_h in offsets:
544
+ normal_group = list()
545
+ flip_group = list()
546
+ for i, img in enumerate(img_group):
547
+ crop = img.crop((o_w, o_h, o_w + crop_w, o_h + crop_h))
548
+ normal_group.append(crop)
549
+ if self.flip:
550
+ flip_crop = crop.copy().transpose(Image.FLIP_LEFT_RIGHT)
551
+
552
+ if img.mode == 'L' and i % 2 == 0:
553
+ flip_group.append(ImageOps.invert(flip_crop))
554
+ else:
555
+ flip_group.append(flip_crop)
556
+
557
+ oversample_group.extend(normal_group)
558
+ oversample_group.extend(flip_group)
559
+ return oversample_group
560
+
561
+
562
+ class GroupMultiScaleCrop(object):
563
+
564
+ def __init__(self, input_size, scales=None, max_distort=1,
565
+ fix_crop=True, more_fix_crop=True):
566
+ self.scales = scales if scales is not None else [1, .875, .75, .66]
567
+ self.max_distort = max_distort
568
+ self.fix_crop = fix_crop
569
+ self.more_fix_crop = more_fix_crop
570
+ self.input_size = input_size if not isinstance(input_size, int) else [
571
+ input_size, input_size]
572
+ self.interpolation = Image.BILINEAR
573
+
574
+ def __call__(self, img_group):
575
+
576
+ im_size = img_group[0].size
577
+
578
+ crop_w, crop_h, offset_w, offset_h = self._sample_crop_size(im_size)
579
+ crop_img_group = [
580
+ img.crop(
581
+ (offset_w,
582
+ offset_h,
583
+ offset_w + crop_w,
584
+ offset_h + crop_h)) for img in img_group]
585
+ ret_img_group = [img.resize((self.input_size[0], self.input_size[1]), self.interpolation)
586
+ for img in crop_img_group]
587
+ return ret_img_group
588
+
589
+ def _sample_crop_size(self, im_size):
590
+ image_w, image_h = im_size[0], im_size[1]
591
+
592
+ # find a crop size
593
+ base_size = min(image_w, image_h)
594
+ crop_sizes = [int(base_size * x) for x in self.scales]
595
+ crop_h = [
596
+ self.input_size[1] if abs(
597
+ x - self.input_size[1]) < 3 else x for x in crop_sizes]
598
+ crop_w = [
599
+ self.input_size[0] if abs(
600
+ x - self.input_size[0]) < 3 else x for x in crop_sizes]
601
+
602
+ pairs = []
603
+ for i, h in enumerate(crop_h):
604
+ for j, w in enumerate(crop_w):
605
+ if abs(i - j) <= self.max_distort:
606
+ pairs.append((w, h))
607
+
608
+ crop_pair = random.choice(pairs)
609
+ if not self.fix_crop:
610
+ w_offset = random.randint(0, image_w - crop_pair[0])
611
+ h_offset = random.randint(0, image_h - crop_pair[1])
612
+ else:
613
+ w_offset, h_offset = self._sample_fix_offset(
614
+ image_w, image_h, crop_pair[0], crop_pair[1])
615
+
616
+ return crop_pair[0], crop_pair[1], w_offset, h_offset
617
+
618
+ def _sample_fix_offset(self, image_w, image_h, crop_w, crop_h):
619
+ offsets = self.fill_fix_offset(
620
+ self.more_fix_crop, image_w, image_h, crop_w, crop_h)
621
+ return random.choice(offsets)
622
+
623
+ @staticmethod
624
+ def fill_fix_offset(more_fix_crop, image_w, image_h, crop_w, crop_h):
625
+ w_step = (image_w - crop_w) // 4
626
+ h_step = (image_h - crop_h) // 4
627
+
628
+ ret = list()
629
+ ret.append((0, 0)) # upper left
630
+ ret.append((4 * w_step, 0)) # upper right
631
+ ret.append((0, 4 * h_step)) # lower left
632
+ ret.append((4 * w_step, 4 * h_step)) # lower right
633
+ ret.append((2 * w_step, 2 * h_step)) # center
634
+
635
+ if more_fix_crop:
636
+ ret.append((0, 2 * h_step)) # center left
637
+ ret.append((4 * w_step, 2 * h_step)) # center right
638
+ ret.append((2 * w_step, 4 * h_step)) # lower center
639
+ ret.append((2 * w_step, 0 * h_step)) # upper center
640
+
641
+ ret.append((1 * w_step, 1 * h_step)) # upper left quarter
642
+ ret.append((3 * w_step, 1 * h_step)) # upper right quarter
643
+ ret.append((1 * w_step, 3 * h_step)) # lower left quarter
644
+ ret.append((3 * w_step, 3 * h_step)) # lower righ quarter
645
+
646
+ return ret
647
+
648
+
649
+ class GroupRandomSizedCrop(object):
650
+ """Random crop the given PIL.Image to a random size of (0.08 to 1.0) of the original size
651
+ and and a random aspect ratio of 3/4 to 4/3 of the original aspect ratio
652
+ This is popularly used to train the Inception networks
653
+ size: size of the smaller edge
654
+ interpolation: Default: PIL.Image.BILINEAR
655
+ """
656
+
657
+ def __init__(self, size, interpolation=Image.BILINEAR):
658
+ self.size = size
659
+ self.interpolation = interpolation
660
+
661
+ def __call__(self, img_group):
662
+ for attempt in range(10):
663
+ area = img_group[0].size[0] * img_group[0].size[1]
664
+ target_area = random.uniform(0.08, 1.0) * area
665
+ aspect_ratio = random.uniform(3. / 4, 4. / 3)
666
+
667
+ w = int(round(math.sqrt(target_area * aspect_ratio)))
668
+ h = int(round(math.sqrt(target_area / aspect_ratio)))
669
+
670
+ if random.random() < 0.5:
671
+ w, h = h, w
672
+
673
+ if w <= img_group[0].size[0] and h <= img_group[0].size[1]:
674
+ x1 = random.randint(0, img_group[0].size[0] - w)
675
+ y1 = random.randint(0, img_group[0].size[1] - h)
676
+ found = True
677
+ break
678
+ else:
679
+ found = False
680
+ x1 = 0
681
+ y1 = 0
682
+
683
+ if found:
684
+ out_group = list()
685
+ for img in img_group:
686
+ img = img.crop((x1, y1, x1 + w, y1 + h))
687
+ assert (img.size == (w, h))
688
+ out_group.append(
689
+ img.resize(
690
+ (self.size, self.size), self.interpolation))
691
+ return out_group
692
+ else:
693
+ # Fallback
694
+ scale = GroupScale(self.size, interpolation=self.interpolation)
695
+ crop = GroupRandomCrop(self.size)
696
+ return crop(scale(img_group))
697
+
698
+
699
+ class ConvertDataFormat(object):
700
+ def __init__(self, model_type):
701
+ self.model_type = model_type
702
+
703
+ def __call__(self, images):
704
+ if self.model_type == '2D':
705
+ return images
706
+ tc, h, w = images.size()
707
+ t = tc // 3
708
+ images = images.view(t, 3, h, w)
709
+ images = images.permute(1, 0, 2, 3)
710
+ return images
711
+
712
+
713
+ class Stack(object):
714
+
715
+ def __init__(self, roll=False):
716
+ self.roll = roll
717
+
718
+ def __call__(self, img_group):
719
+ if img_group[0].mode == 'L':
720
+ return np.concatenate([np.expand_dims(x, 2)
721
+ for x in img_group], axis=2)
722
+ elif img_group[0].mode == 'RGB':
723
+ if self.roll:
724
+ return np.concatenate([np.array(x)[:, :, ::-1]
725
+ for x in img_group], axis=2)
726
+ else:
727
+ # print(np.concatenate(img_group, axis=2).shape)
728
+ # print(img_group[0].shape)
729
+ return np.concatenate(img_group, axis=2)
730
+
731
+
732
+ class ToTorchFormatTensor(object):
733
+ """ Converts a PIL.Image (RGB) or numpy.ndarray (H x W x C) in the range [0, 255]
734
+ to a torch.FloatTensor of shape (C x H x W) in the range [0.0, 1.0] """
735
+
736
+ def __init__(self, div=True):
737
+ self.div = div
738
+
739
+ def __call__(self, pic):
740
+ if isinstance(pic, np.ndarray):
741
+ # handle numpy array
742
+ img = torch.from_numpy(pic).permute(2, 0, 1).contiguous()
743
+ else:
744
+ # handle PIL Image
745
+ img = torch.ByteTensor(
746
+ torch.ByteStorage.from_buffer(
747
+ pic.tobytes()))
748
+ img = img.view(pic.size[1], pic.size[0], len(pic.mode))
749
+ # put it from HWC to CHW format
750
+ # yikes, this transpose takes 80% of the loading time/CPU
751
+ img = img.transpose(0, 1).transpose(0, 2).contiguous()
752
+ return img.float().div(255) if self.div else img.float()
753
+
754
+
755
+ class IdentityTransform(object):
756
+
757
+ def __call__(self, data):
758
+ return data
reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/GUI/__init__.py ADDED
File without changes
reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/GUI/osworld_g.py ADDED
@@ -0,0 +1,441 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ast
2
+ import itertools
3
+ import json
4
+ import os
5
+ import os.path as osp
6
+ import re
7
+ from collections import defaultdict
8
+
9
+ import numpy as np
10
+ import pandas as pd
11
+ from PIL import Image
12
+ from tqdm import tqdm
13
+
14
+ from vlmeval.dataset.image_base import ImageBaseDataset
15
+ from vlmeval.smp import LMUDataRoot, dump, get_intermediate_file_path, get_logger, load, toliststr
16
+
17
+ logger = get_logger(__name__)
18
+
19
+ SYSTEM_PROMPT = """You are a GUI agent. You are given a task and a screenshot of the screen. You need to perform pyautogui click/moveTo action to complete the task. The answer format is `pyautogui.click(x=?, y=?), x and y is necessary`""" # noqa: E501
20
+
21
+ USER_INSTRUCTION = """Please complete the following tasks by clicking using `pyautogui.click`:\n{instruction}""" # noqa: E501
22
+
23
+ SYSTEM_PROMPT_V2 = """You are a GUI agent. You are given a screenshot of the screen and the description of a target element. You need to click the target element using `pyautogui.click`. The answer format is `pyautogui.click(x=?, y=?), x and y is necessary`""" # noqa: E501
24
+
25
+ USER_INSTRUCTION_V2 = """Please click the following target element using `pyautogui.click`:\n{description}"""
26
+
27
+
28
+ def parse_bbox_aguvis(response):
29
+ match = re.search(r"x=([\d.]+), y=([\d.]+)", response)
30
+ if match:
31
+ click_point = [float(match.group(1)), float(match.group(2))]
32
+ else:
33
+ click_point = [0.0, 0.0]
34
+ return click_point
35
+
36
+
37
+ def compute_iou(box1, box2):
38
+ """
39
+ Compute the Intersection over Union (IoU) of two bounding boxes.
40
+
41
+ Parameters:
42
+ - box1 (list of float): Bounding box [x_min, y_min, x_max, y_max].
43
+ - box2 (list of float): Bounding box [x_min, y_min, x_max, y_max].
44
+
45
+ Returns:
46
+ - float: IoU of box1 and box2.
47
+ """
48
+ # Determine the coordinates of the intersection rectangle
49
+ x_left = max(box1[0], box2[0])
50
+ y_top = max(box1[1], box2[1])
51
+ x_right = min(box1[2], box2[2])
52
+ y_bottom = min(box1[3], box2[3])
53
+
54
+ # Compute the area of intersection
55
+ intersection_area = max(0, x_right - x_left) * max(0, y_bottom - y_top)
56
+
57
+ # Compute the area of both bounding boxes
58
+ box1_area = (box1[2] - box1[0]) * (box1[3] - box1[1])
59
+ box2_area = (box2[2] - box2[0]) * (box2[3] - box2[1])
60
+
61
+ # Compute the area of the union
62
+ union_area = box1_area + box2_area - intersection_area
63
+
64
+ # Compute the Intersection over Union
65
+ iou = intersection_area / union_area
66
+
67
+ return iou
68
+
69
+
70
+ def compute_accuracy(box1, box2, threshold=0.5):
71
+ """
72
+ Compute the accuracy of two bounding boxes based on a specified threshold.
73
+
74
+ Parameters:
75
+ - box1 (list of float): Bounding box [x_min, y_min, x_max, y_max].
76
+ - box2 (list of float): Bounding box [x_min, y_min, x_max, y_max].
77
+ - threshold (float): Threshold for the IoU to consider the prediction correct.
78
+
79
+ Returns:
80
+ - float: Accuracy of the prediction based on the IoU threshold.
81
+ """
82
+ iou = compute_iou(box1, box2)
83
+ return iou >= threshold
84
+
85
+
86
+ def compute_center_accuracy(box1, box2):
87
+ """
88
+ Compute if the center point of box 2 is within box 1.
89
+
90
+ Parameters:
91
+ - box1 (list of float): Bounding box [x_min, y_min, x_max, y_max].
92
+ - box2 (list of float): Bounding box [x_min, y_min, x_max, y_max].
93
+
94
+ Returns:
95
+ - bool: True if the center point of box 2 is within box 1, False otherwise.
96
+ """
97
+ # Compute the center point of box 2
98
+ center_x = (box2[0] + box2[2]) / 2
99
+ center_y = (box2[1] + box2[3]) / 2
100
+
101
+ # Check if the center point is within box 1
102
+ return box1[0] <= center_x <= box1[2] and box1[1] <= center_y <= box1[3]
103
+
104
+
105
+ def convert_bbox(bbox, image_path, convert_xywh_to_x1y1x2y2=True):
106
+ new_bbox = bbox if isinstance(bbox, list) else ast.literal_eval(bbox)
107
+ if convert_xywh_to_x1y1x2y2:
108
+ new_bbox = [
109
+ new_bbox[0],
110
+ new_bbox[1],
111
+ new_bbox[0] + new_bbox[2],
112
+ new_bbox[1] + new_bbox[3],
113
+ ]
114
+ image = Image.open(image_path)
115
+ img_size = image.size
116
+ new_bbox = [
117
+ new_bbox[0] / img_size[0],
118
+ new_bbox[1] / img_size[1],
119
+ new_bbox[2] / img_size[0],
120
+ new_bbox[3] / img_size[1],
121
+ ]
122
+ return new_bbox
123
+
124
+
125
+ class OSWorld_G(ImageBaseDataset):
126
+ MODALITY = "IMAGE"
127
+ TYPE = "GUI"
128
+ DATASET_URL = {
129
+ "OSWorld_G": "https://opencompass.openxlab.space/utils/VLMEval/OSWorld_G.tsv", # Optional, dummy URL
130
+ } # path
131
+ DATASET_MD5 = {
132
+ 'OSWorld_G': 'eee81b61210f580cbc98b11c6bced928'
133
+ }
134
+ EVAL_TYPE = "point" # point or rectangle
135
+ RE_TYPE = "functional" # type of referring expressions: functional or composite
136
+
137
+ def __init__(
138
+ self,
139
+ dataset="OSWorld_G",
140
+ skip_noimg=True,
141
+ skeleton=False,
142
+ re_type="functional",
143
+ ):
144
+ # st()
145
+ ROOT = LMUDataRoot()
146
+ # You can override this variable to save image files to a different directory
147
+ self.dataset_name = dataset
148
+ self.img_root = osp.join(ROOT, "images", self.dataset_name)
149
+ self.RE_TYPE = re_type
150
+ if skeleton:
151
+ return
152
+
153
+ data = self.load_data(dataset)
154
+ self.skip_noimg = skip_noimg
155
+ if skip_noimg and "image" in data:
156
+ data = data[~pd.isna(data["image"])]
157
+
158
+ data["index"] = [str(idx + 1) for idx, x in enumerate(data["bbox"])]
159
+
160
+ self.meta_only = True
161
+ self.parse_response_func = parse_bbox_aguvis # TODO: parse function can be specified through kwargs when initializing the dataset # noqa: E501
162
+
163
+ # The image field can store the base64 encoded image or another question index (for saving space) # noqa: E501
164
+ if "image" in data:
165
+ data["image"] = [str(x) for x in data["image"]]
166
+ image_map = {x: y for x, y in zip(data["index"], data["image"])}
167
+ for k in image_map:
168
+ if len(image_map[k]) <= 64:
169
+ idx = image_map[k]
170
+ assert idx in image_map and len(image_map[idx]) > 64
171
+ image_map[k] = image_map[idx]
172
+
173
+ images = [toliststr(image_map[k]) for k in data["index"]]
174
+ data["image"] = [x[0] if len(x) == 1 else x for x in images]
175
+ self.meta_only = False
176
+
177
+ self.data = data
178
+
179
+ @classmethod
180
+ def get_action_space(self):
181
+ return ""
182
+
183
+ @classmethod
184
+ def get_trajectory(self, line):
185
+ traj_dict = {}
186
+ if self.RE_TYPE == "functional":
187
+ traj_dict["task"] = line["question"]
188
+ else:
189
+ traj_dict["task"] = line["description"]
190
+ return traj_dict
191
+
192
+ def build_prompt(self, line):
193
+ if isinstance(line, int):
194
+ line = self.data.iloc[line]
195
+ tgt_path = self.dump_image(line)
196
+
197
+ if self.RE_TYPE == "functional":
198
+ user_instruction = USER_INSTRUCTION.format(instruction=line["question"])
199
+ else:
200
+ user_instruction = USER_INSTRUCTION_V2.format(
201
+ description=line["description"]
202
+ )
203
+
204
+ msgs = []
205
+ # add system prompt
206
+ if self.RE_TYPE == "functional":
207
+ msgs.append(dict(role="system", type="text", value=SYSTEM_PROMPT))
208
+ else:
209
+ msgs.append(dict(role="system", type="text", value=SYSTEM_PROMPT_V2))
210
+ if isinstance(tgt_path, list):
211
+ msgs.extend([dict(type="image", value=p) for p in tgt_path])
212
+ else:
213
+ msgs = [dict(type="image", value=tgt_path)]
214
+ msgs.append(dict(type="text", value=user_instruction))
215
+ return msgs
216
+
217
+ def evaluate(self, eval_file, **judge_kwargs):
218
+ # st()
219
+ if self.EVAL_TYPE == "point":
220
+ return self.evaluate_point(eval_file, **judge_kwargs)
221
+
222
+ elif self.EVAL_TYPE == "rectangle":
223
+ return self.evaluate_rectangle(eval_file, **judge_kwargs)
224
+
225
+ def evaluate_rectangle(self, eval_file, **judge_kwargs):
226
+ scorers = {
227
+ "IoU": compute_iou,
228
+ "ACC@0.1": lambda x, y: compute_accuracy(x, y, 0.1),
229
+ "ACC@0.3": lambda x, y: compute_accuracy(x, y, 0.3),
230
+ "ACC@0.5": lambda x, y: compute_accuracy(x, y, 0.5),
231
+ "ACC@0.7": lambda x, y: compute_accuracy(x, y, 0.7),
232
+ "ACC@0.9": lambda x, y: compute_accuracy(x, y, 0.9),
233
+ "Center_ACC": compute_center_accuracy,
234
+ }
235
+ results_dict = {}
236
+ for key in scorers.keys():
237
+ results_dict.update(
238
+ {
239
+ key: [],
240
+ key + "_text": [],
241
+ key + "_icon": [],
242
+ }
243
+ )
244
+
245
+ result = []
246
+ data = load(eval_file)
247
+ assert "bbox" in data and "prediction" in data
248
+ lt = len(data)
249
+ lines = [data.iloc[i] for i in range(lt)]
250
+ for i in tqdm(range(len(lines))):
251
+ line = lines[i]
252
+ bbox = convert_bbox(
253
+ line["bbox"], os.path.join(self.img_root, line["image_path"]), convert_xywh_to_x1y1x2y2=False
254
+ )
255
+ prediction = str(line["prediction"])
256
+ try:
257
+ click_point = parse_bbox_aguvis(prediction)
258
+
259
+ match = {}
260
+ for score_key, score_value in scorers.items():
261
+ score = score_value(bbox, click_point)
262
+ if score_key != "IoU":
263
+ match[score_key.replace("ACC", "match")] = score
264
+ results_dict[score_key].append(score)
265
+ if line["ui_type"] == "text":
266
+ results_dict[score_key + "_text"].append(score)
267
+ else:
268
+ results_dict[score_key + "_icon"].append(score)
269
+ except Exception:
270
+ click_point = None
271
+ match = {score_key: False for score_key in scorers.keys() if score_key != "IoU"}
272
+ result.append(
273
+ {
274
+ "img_path": os.path.join(self.img_root, line["image_path"]),
275
+ "text": line["question"],
276
+ "bbox": line["bbox"],
277
+ "parsed_bbox": bbox,
278
+ "type": line["ui_type"],
279
+ "source": line["application"],
280
+ "pred": click_point,
281
+ "num_matched": sum(match.values()),
282
+ **match,
283
+ }
284
+ )
285
+ for key in results_dict:
286
+ if len(results_dict[key]) == 0:
287
+ results_dict[key] = str(0)
288
+ else:
289
+ results_dict[key] = str(sum(results_dict[key]) / len(results_dict[key]))
290
+ score_pth = get_intermediate_file_path(eval_file, '_score', 'json')
291
+ dump(results_dict, score_pth)
292
+
293
+ failure_cases_path = os.environ.get("FAILURE_CASES_PATH", None)
294
+ if failure_cases_path is not None:
295
+ failure_cases = [res for res in result if not res["match"] and res["is_wrong_format"]]
296
+ failure_cases.sort(key=lambda r: r["num_matched"], reverse=True)
297
+
298
+ with open(failure_cases_path, "w") as f:
299
+ json.dump(failure_cases, f, indent=4, ensure_ascii=False)
300
+ return results_dict
301
+
302
+ def evaluate_point(self, eval_file, **judge_kwargs):
303
+ # -1: format_err, 0: wrong, 1: correct
304
+ stats = defaultdict(list)
305
+ # Will include instance-level results
306
+ result = []
307
+
308
+ data = load(eval_file)
309
+ assert "bbox" in data and "prediction" in data
310
+ lt = len(data)
311
+ lines = [data.iloc[i] for i in range(lt)]
312
+ for i in tqdm(range(len(lines))):
313
+ line = lines[i]
314
+ bbox = (
315
+ line["bbox"]
316
+ if isinstance(line["bbox"], list)
317
+ else ast.literal_eval(line["bbox"])
318
+ )
319
+ # The format of bbox is (x1, y1, x2, y2)
320
+
321
+ image = Image.open(os.path.join(self.img_root, line["image_path"]))
322
+ img_size = image.size
323
+
324
+ def make_safe(value):
325
+ if value == -1:
326
+ # we can tolerate -1 as a special value and nomalize it to 0
327
+ return 0
328
+ else:
329
+ return value
330
+
331
+ bbox = [
332
+ make_safe(bbox[0]) / img_size[0],
333
+ make_safe(bbox[1]) / img_size[1],
334
+ make_safe(bbox[0] + bbox[2]) / img_size[0],
335
+ make_safe(bbox[1] + bbox[3]) / img_size[1],
336
+ ]
337
+
338
+ key = line["category"] + ":" + line['ui_type']
339
+ prediction = str(line["prediction"])
340
+ try:
341
+ click_point = self.parse_response_func(prediction)
342
+ # Do Normalization By Default
343
+ # if click_point[0] > 1 or click_point[1] > 1:
344
+ click_point = (click_point[0] / 1000, click_point[1] / 1000)
345
+
346
+ match = (bbox[0] <= click_point[0] <= bbox[2]) and \
347
+ (bbox[1] <= click_point[1] <= bbox[3])
348
+ # draw click point and box on image
349
+ # from PIL import ImageDraw
350
+ # draw = ImageDraw.Draw(image)
351
+ # draw.rectangle([bbox[0] * img_size[0], bbox[1] * img_size[1],
352
+ # bbox[2] * img_size[0], bbox[3] * img_size[1]], outline="red", width=2)
353
+ # draw.ellipse([click_point[0] * img_size[0] - 5, click_point[1] * img_size[1] - 5,
354
+ # click_point[0] * img_size[0] + 5, click_point[1] * img_size[1] + 5],
355
+ # outline="red", width=2)
356
+ # image.save(f"debug_{i}.png")
357
+
358
+ if match:
359
+ stats[key].append(1)
360
+ else:
361
+ stats[key].append(0)
362
+ is_wrong_format = False
363
+
364
+ except Exception as e:
365
+ logger.warning(f"exception in screenspot eval:{e}")
366
+ stats[key].append(-1)
367
+ match, is_wrong_format, click_point = False, True, None
368
+
369
+ result.append(
370
+ {
371
+ "img_path": os.path.join(self.img_root, line["image_path"]),
372
+ "text": line["question"],
373
+ "bbox": line["bbox"],
374
+ "parsed_bbox": bbox,
375
+ "type": line["ui_type"],
376
+ "source": line["application"],
377
+ "match": match,
378
+ "is_wrong_format": is_wrong_format,
379
+ "pred": click_point,
380
+ }
381
+ )
382
+
383
+ final_score_dict = {}
384
+ # Record the number of each category
385
+ final_score_dict.update({k + ':cnt': len(stats[k]) for k in stats})
386
+ # Calculate the Overall stats
387
+ full_stats = []
388
+ for v in stats.values():
389
+ full_stats.extend(v)
390
+ final_score_dict['Overall_Accuracy'] = np.mean([x > 0 for x in full_stats]) * 100
391
+ final_score_dict['Format_Err_Rate'] = np.mean([x < 0 for x in full_stats]) * 100
392
+ # Calculate the Accuracy of Text / Icon
393
+ text_stats = [v for k, v in stats.items() if k.split(":")[1] == "text" for x in v]
394
+ text_stats = itertools.chain(*text_stats)
395
+ final_score_dict['Text_Accuracy'] = np.mean([x > 0 for x in text_stats]) * 100
396
+ icon_stats = [v for k, v in stats.items() if k.split(":")[1] == "icon" for x in v]
397
+ icon_stats = itertools.chain(*icon_stats)
398
+ final_score_dict['Icon_Accuracy'] = np.mean([x > 0 for x in icon_stats]) * 100
399
+ # Calculate the Accuracy of Each Category
400
+ cates = list(set(data['category']))
401
+ for c in cates:
402
+ sub_stats = [v for k, v in stats.items() if k.split(":")[0] == c for x in v]
403
+ sub_stats = itertools.chain(*sub_stats)
404
+ final_score_dict[c + '_Accuracy'] = np.mean([x > 0 for x in sub_stats]) * 100
405
+
406
+ score_pth = get_intermediate_file_path(eval_file, '_score', 'json')
407
+ dump(final_score_dict, score_pth)
408
+
409
+ failure_cases_path = os.environ.get("FAILURE_CASES_PATH", None)
410
+ if failure_cases_path is not None:
411
+ def click_distance(bbox, click_point):
412
+ x, y = click_point
413
+ x1, y1, x2, y2 = bbox
414
+ xc, yc = (x1 + x2) / 2, (y1 + y2) / 2
415
+ w, h = x2 - x1, y2 - y1
416
+ abs_shift_to_center = [abs(x - xc), abs(y - yc)] # noqa: E501
417
+ width_outside, height_outside = [max(0, abs_shift_to_center[0] - w / 2), max(0, abs_shift_to_center[1] - h / 2)] # noqa: E501
418
+ return (width_outside ** 2 + height_outside ** 2) ** 0.5 # noqa: E501
419
+
420
+ wrong_format_result = [res for res in result if res["is_wrong_format"]]
421
+ missed_result = [res for res in result if not res["match"] and not res["is_wrong_format"]]
422
+ missed_result.sort(key=lambda r: click_distance(r["parsed_bbox"], r["pred"]), reverse=True)
423
+ failure_cases = wrong_format_result + missed_result
424
+
425
+ with open(failure_cases_path, "w") as f:
426
+ json.dump(failure_cases, f, indent=4, ensure_ascii=False)
427
+
428
+ successful_cases_path = os.environ.get("SUCCESSFUL_CASES_PATH", None)
429
+ if successful_cases_path is not None:
430
+ def _click_distance(bbox, click_point):
431
+ x, y = click_point
432
+ x1, y1, x2, y2 = bbox
433
+ xc, yc = (x1 + x2) / 2, (y1 + y2) / 2
434
+ x_shift, y_shift = x - xc, y - yc
435
+ return (x_shift ** 2 + y_shift ** 2) ** 0.5
436
+
437
+ successful_cases = [res for res in result if res["match"]]
438
+ successful_cases.sort(key=lambda r: _click_distance(r["parsed_bbox"], r["pred"]), reverse=True)
439
+ with open(successful_cases_path, "w") as f:
440
+ json.dump(successful_cases, f, indent=4, ensure_ascii=False)
441
+ return final_score_dict
reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/GUI/screenspot.py ADDED
@@ -0,0 +1,462 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ast
2
+ import itertools
3
+ import json
4
+ import os
5
+ import os.path as osp
6
+ import re
7
+ from collections import defaultdict
8
+
9
+ import numpy as np
10
+ import pandas as pd
11
+ from PIL import Image
12
+ from tqdm import tqdm
13
+
14
+ from vlmeval.dataset.image_base import ImageBaseDataset
15
+ from vlmeval.smp import LMUDataRoot, dump, get_intermediate_file_path, get_logger, load, toliststr
16
+
17
+ logger = get_logger(__name__)
18
+
19
+ """
20
+ {
21
+ "img_filename": "web_3b0ad239-da6b-4f6f-8f12-f674dc90ff33.png",
22
+ "bbox": [42, 1102, 197, 70],
23
+ "question": "view the details of the item",
24
+ "data_type": "text",
25
+ "data_source": "shop"
26
+ },
27
+ {
28
+ "img_filename": "web_3b0ad239-da6b-4f6f-8f12-f674dc90ff33.png",
29
+ "bbox": [93, 74, 86, 132],
30
+ "question": "view the previous photo",
31
+ "data_type": "icon",
32
+ "data_source": "shop"
33
+ }
34
+ """
35
+
36
+ SYSTEM_PROMPT = """You are a GUI agent. You are given a task and a screenshot of the screen. You need to perform pyautogui click/moveTo action to complete the task. The answer format is `pyautogui.click(x=?, y=?), x and y is necessary`""" # noqa: E501
37
+
38
+ USER_INSTRUCTION = """Please complete the following tasks by clicking using `pyautogui.click`:\n{instruction}"""
39
+
40
+ SYSTEM_PROMPT_V2 = """You are a GUI agent. You are given a screenshot of the screen and the description of a target element. You need to click the target element using `pyautogui.click`. The answer format is `pyautogui.click(x=?, y=?), x and y is necessary`""" # noqa: E501
41
+ USER_INSTRUCTION_V2 = """Please click the following target element using `pyautogui.click`:\n{description}"""
42
+
43
+
44
+ def parse_bbox_aguvis(response):
45
+ match = re.search(r"x=([\d.]+), y=([\d.]+)", response)
46
+ if match:
47
+ click_point = [float(match.group(1)), float(match.group(2))]
48
+ else:
49
+ click_point = [0.0, 0.0]
50
+ return click_point
51
+
52
+
53
+ def compute_iou(box1, box2):
54
+ """
55
+ Compute the Intersection over Union (IoU) of two bounding boxes.
56
+
57
+ Parameters:
58
+ - box1 (list of float): Bounding box [x_min, y_min, x_max, y_max].
59
+ - box2 (list of float): Bounding box [x_min, y_min, x_max, y_max].
60
+
61
+ Returns:
62
+ - float: IoU of box1 and box2.
63
+ """
64
+ # Determine the coordinates of the intersection rectangle
65
+ x_left = max(box1[0], box2[0])
66
+ y_top = max(box1[1], box2[1])
67
+ x_right = min(box1[2], box2[2])
68
+ y_bottom = min(box1[3], box2[3])
69
+
70
+ # Compute the area of intersection
71
+ intersection_area = max(0, x_right - x_left) * max(0, y_bottom - y_top)
72
+
73
+ # Compute the area of both bounding boxes
74
+ box1_area = (box1[2] - box1[0]) * (box1[3] - box1[1])
75
+ box2_area = (box2[2] - box2[0]) * (box2[3] - box2[1])
76
+
77
+ # Compute the area of the union
78
+ union_area = box1_area + box2_area - intersection_area
79
+
80
+ # Compute the Intersection over Union
81
+ iou = intersection_area / union_area
82
+
83
+ return iou
84
+
85
+
86
+ def compute_accuracy(box1, box2, threshold=0.5):
87
+ """
88
+ Compute the accuracy of two bounding boxes based on a specified threshold.
89
+
90
+ Parameters:
91
+ - box1 (list of float): Bounding box [x_min, y_min, x_max, y_max].
92
+ - box2 (list of float): Bounding box [x_min, y_min, x_max, y_max].
93
+ - threshold (float): Threshold for the IoU to consider the prediction correct.
94
+
95
+ Returns:
96
+ - float: Accuracy of the prediction based on the IoU threshold.
97
+ """
98
+ iou = compute_iou(box1, box2)
99
+ return iou >= threshold
100
+
101
+
102
+ def compute_center_accuracy(box1, box2):
103
+ """
104
+ Compute if the center point of box 2 is within box 1.
105
+
106
+ Parameters:
107
+ - box1 (list of float): Bounding box [x_min, y_min, x_max, y_max].
108
+ - box2 (list of float): Bounding box [x_min, y_min, x_max, y_max].
109
+
110
+ Returns:
111
+ - bool: True if the center point of box 2 is within box 1, False otherwise.
112
+ """
113
+ # Compute the center point of box 2
114
+ center_x = (box2[0] + box2[2]) / 2
115
+ center_y = (box2[1] + box2[3]) / 2
116
+
117
+ # Check if the center point is within box 1
118
+ return box1[0] <= center_x <= box1[2] and box1[1] <= center_y <= box1[3]
119
+
120
+
121
+ def convert_bbox(bbox, image_path):
122
+ new_bbox = bbox if isinstance(bbox, list) else ast.literal_eval(bbox)
123
+ new_bbox = [
124
+ new_bbox[0],
125
+ new_bbox[1],
126
+ new_bbox[0] + new_bbox[2],
127
+ new_bbox[1] + new_bbox[3],
128
+ ]
129
+ image = Image.open(image_path)
130
+ img_size = image.size
131
+ new_bbox = [
132
+ new_bbox[0] / img_size[0],
133
+ new_bbox[1] / img_size[1],
134
+ new_bbox[2] / img_size[0],
135
+ new_bbox[3] / img_size[1],
136
+ ]
137
+ return new_bbox
138
+
139
+
140
+ class ScreenSpot(ImageBaseDataset):
141
+ MODALITY = "IMAGE"
142
+ TYPE = "GUI"
143
+ DATASET_URL = {
144
+ "ScreenSpot_Mobile": "https://opencompass.openxlab.space/utils/benchmarks/GUI/ScreenSpot/ScreenSpot_Mobile.tsv", # noqa
145
+ "ScreenSpot_Desktop": "https://opencompass.openxlab.space/utils/benchmarks/GUI/ScreenSpot/ScreenSpot_Desktop.tsv", # noqa
146
+ "ScreenSpot_Web": "https://opencompass.openxlab.space/utils/benchmarks/GUI/ScreenSpot/ScreenSpot_Web.tsv", # noqa
147
+ "ScreenSpot_v2_Mobile": "https://opencompass.openxlab.space/utils/benchmarks/GUI/ScreenSpot_v2/ScreenSpot_v2_Mobile.tsv", # noqa
148
+ "ScreenSpot_v2_Desktop": "https://opencompass.openxlab.space/utils/benchmarks/GUI/ScreenSpot_v2/ScreenSpot_v2_Desktop.tsv", # noqa
149
+ "ScreenSpot_v2_Web": "https://opencompass.openxlab.space/utils/benchmarks/GUI/ScreenSpot_v2/ScreenSpot_v2_Web.tsv", # noqa
150
+ } # path
151
+ DATASET_URL_V2 = {
152
+ "ScreenSpot_Mobile": "$WORK_DIR/screenspot_mobile_ug.json",
153
+ "ScreenSpot_Desktop": "$WORK_DIR/screenspot_desktop_ug.json",
154
+ "ScreenSpot_Web": "$WORK_DIR/screenspot_web_ug.json",
155
+ } # path
156
+ DATASET_MD5 = {
157
+ "ScreenSpot_Mobile": "a5b5299843a75c9b9574c47bc13b2c53",
158
+ "ScreenSpot_Desktop": "e6e7bac21b6b2475276404fce2458132",
159
+ "ScreenSpot_Web": "e51d168c14b8582427cf3107d236cfc5",
160
+ "ScreenSpot_v2_Mobile": "234c858ab4f0e787e8388a73df65a4b7",
161
+ "ScreenSpot_v2_Desktop": "5f2aa2a497327bd33b2512a0c75cf994",
162
+ "ScreenSpot_v2_Web": "01cd0877ee1b735a6d5190b053ba9482",
163
+ }
164
+ EVAL_TYPE = "point" # point or rectangle
165
+ RE_TYPE = "functional" # type of referring expressions: functional or composite
166
+
167
+ def __init__(
168
+ self,
169
+ dataset="ScreenSpot_Mobile",
170
+ skip_noimg=True,
171
+ skeleton=False,
172
+ re_type="functional",
173
+ ):
174
+ # st()
175
+ ROOT = LMUDataRoot()
176
+ # You can override this variable to save image files to a different directory
177
+ self.dataset_name = dataset
178
+ self.img_root = osp.join(ROOT, "images", self.dataset_name)
179
+ self.RE_TYPE = re_type
180
+ if skeleton:
181
+ return
182
+
183
+ data = self.load_data(dataset)
184
+ self.skip_noimg = skip_noimg
185
+ if skip_noimg and "image" in data:
186
+ data = data[~pd.isna(data["image"])]
187
+
188
+ self.meta_only = True
189
+ self.parse_response_func = parse_bbox_aguvis # TODO: parse function can be specified through kwargs when initializing the dataset # noqa: E501
190
+
191
+ # The image field can store the base64 encoded image or another question index (for saving space)
192
+ if "image" in data:
193
+ data["image"] = [str(x) for x in data["image"]]
194
+ image_map = {x: y for x, y in zip(data["index"], data["image"])}
195
+ for k in image_map:
196
+ if len(image_map[k]) <= 64:
197
+ idx = image_map[k]
198
+ assert idx in image_map and len(image_map[idx]) > 64
199
+ image_map[k] = image_map[idx]
200
+
201
+ images = [toliststr(image_map[k]) for k in data["index"]]
202
+ data["image"] = [x[0] if len(x) == 1 else x for x in images]
203
+ self.meta_only = False
204
+
205
+ self.data = data
206
+
207
+ def prepare_tsv(self, url, file_md5=None):
208
+ # st()
209
+ if self.RE_TYPE == "functional":
210
+ return super().prepare_tsv(url=url, file_md5=file_md5)
211
+ else:
212
+ data_path = self.DATASET_URL_V2[self.dataset_name]
213
+ return pd.DataFrame(load(data_path))
214
+
215
+ @classmethod
216
+ def get_action_space(self):
217
+ return ""
218
+
219
+ @classmethod
220
+ def get_trajectory(self, line):
221
+ traj_dict = {}
222
+ if self.RE_TYPE == "functional":
223
+ traj_dict["task"] = line["question"]
224
+ else:
225
+ traj_dict["task"] = line["description"]
226
+ return traj_dict
227
+
228
+ def build_prompt(self, line):
229
+ # st()
230
+ if isinstance(line, int):
231
+ line = self.data.iloc[line]
232
+ tgt_path = self.dump_image(line)
233
+
234
+ if self.RE_TYPE == "functional":
235
+ user_instruction = USER_INSTRUCTION.format(instruction=line["question"])
236
+ else:
237
+ user_instruction = USER_INSTRUCTION_V2.format(
238
+ description=line["description"]
239
+ )
240
+
241
+ msgs = []
242
+ # add system prompt
243
+ if self.RE_TYPE == "functional":
244
+ msgs.append(dict(role="system", type="text", value=SYSTEM_PROMPT))
245
+ else:
246
+ msgs.append(dict(role="system", type="text", value=SYSTEM_PROMPT_V2))
247
+ if isinstance(tgt_path, list):
248
+ msgs.extend([dict(type="image", value=p) for p in tgt_path])
249
+ else:
250
+ msgs = [dict(type="image", value=tgt_path)]
251
+ msgs.append(dict(type="text", value=user_instruction))
252
+ return msgs
253
+
254
+ def evaluate(self, eval_file, **judge_kwargs):
255
+ # st()
256
+ if self.EVAL_TYPE == "point":
257
+ return self.evaluate_point(eval_file, **judge_kwargs)
258
+
259
+ elif self.EVAL_TYPE == "rectangle":
260
+ return self.evaluate_rectangle(eval_file, **judge_kwargs)
261
+
262
+ def evaluate_rectangle(self, eval_file, **judge_kwargs):
263
+ scorers = {
264
+ "IoU": compute_iou,
265
+ "ACC@0.1": lambda x, y: compute_accuracy(x, y, 0.1),
266
+ "ACC@0.3": lambda x, y: compute_accuracy(x, y, 0.3),
267
+ "ACC@0.5": lambda x, y: compute_accuracy(x, y, 0.5),
268
+ "ACC@0.7": lambda x, y: compute_accuracy(x, y, 0.7),
269
+ "ACC@0.9": lambda x, y: compute_accuracy(x, y, 0.9),
270
+ "Center_ACC": compute_center_accuracy,
271
+ }
272
+ results_dict = {}
273
+ for key in scorers.keys():
274
+ results_dict.update(
275
+ {
276
+ key: [],
277
+ key + "_text": [],
278
+ key + "_icon": [],
279
+ }
280
+ )
281
+
282
+ result = []
283
+ data = load(eval_file)
284
+
285
+ assert "bbox" in data and "prediction" in data
286
+ lt = len(data)
287
+ lines = [data.iloc[i] for i in range(lt)]
288
+ for i in tqdm(range(len(lines))):
289
+ line = lines[i]
290
+ bbox = convert_bbox(
291
+ line["bbox"], os.path.join(self.img_root, line["image_path"])
292
+ )
293
+ prediction = str(line["prediction"])
294
+ try:
295
+ click_point = parse_bbox_aguvis(prediction)
296
+
297
+ match = {}
298
+ for score_key, score_value in scorers.items():
299
+ score = score_value(bbox, click_point)
300
+ if score_key != "IoU":
301
+ match[score_key.replace("ACC", "match")] = score
302
+ results_dict[score_key].append(score)
303
+ if line["data_type"] == "text":
304
+ results_dict[score_key + "_text"].append(score)
305
+ else:
306
+ results_dict[score_key + "_icon"].append(score)
307
+ except Exception:
308
+ click_point = None
309
+ match = {score_key: False for score_key in scorers.keys() if score_key != "IoU"}
310
+ result.append(
311
+ {
312
+ "img_path": os.path.join(self.img_root, line["image_path"]),
313
+ "text": line["question"],
314
+ "bbox": line["bbox"],
315
+ "parsed_bbox": bbox,
316
+ "type": line["data_type"],
317
+ "source": line["data_source"],
318
+ "pred": click_point,
319
+ "num_matched": sum(match.values()),
320
+ **match,
321
+ }
322
+ )
323
+ for key in results_dict:
324
+ if len(results_dict[key]) == 0:
325
+ results_dict[key] = str(0)
326
+ else:
327
+ results_dict[key] = str(sum(results_dict[key]) / len(results_dict[key]))
328
+ score_pth = get_intermediate_file_path(eval_file, '_score', 'json')
329
+ dump(results_dict, score_pth)
330
+
331
+ failure_cases_path = os.environ.get("FAILURE_CASES_PATH", None)
332
+ if failure_cases_path is not None:
333
+ failure_cases = [res for res in result if not res["match"] and res["is_wrong_format"]]
334
+ failure_cases.sort(key=lambda r: r["num_matched"], reverse=True)
335
+
336
+ with open(failure_cases_path, "w") as f:
337
+ json.dump(failure_cases, f, indent=4, ensure_ascii=False)
338
+ return results_dict
339
+
340
+ def evaluate_point(self, eval_file, **judge_kwargs):
341
+ # -1: format_err, 0: wrong, 1: correct
342
+ stats = defaultdict(list)
343
+ # Will include instance-level results
344
+ result = []
345
+
346
+ data = load(eval_file)
347
+ assert "bbox" in data and "prediction" in data
348
+ lt = len(data)
349
+ lines = [data.iloc[i] for i in range(lt)]
350
+ for i in tqdm(range(len(lines))):
351
+ line = lines[i]
352
+ bbox = (
353
+ line["bbox"]
354
+ if isinstance(line["bbox"], list)
355
+ else ast.literal_eval(line["bbox"])
356
+ )
357
+ # The format of bbox is (x1, y1, w, h)
358
+ x1, y1, w, h = bbox
359
+ bbox = (x1, y1, x1 + w - 1, y1 + h - 1)
360
+
361
+ image = Image.open(os.path.join(self.img_root, line["image_path"]))
362
+ img_size = image.size
363
+
364
+ def make_safe(value):
365
+ if value == -1:
366
+ # we can tolerate -1 as a special value and nomalize it to 0
367
+ return 0
368
+ else:
369
+ return value
370
+
371
+ bbox = [
372
+ make_safe(bbox[0]) / img_size[0],
373
+ make_safe(bbox[1]) / img_size[1],
374
+ make_safe(bbox[2]) / img_size[0],
375
+ make_safe(bbox[3]) / img_size[1],
376
+ ]
377
+
378
+ if any([x < 0 or x > 1 for x in bbox]):
379
+ raise ValueError(f"bbox out of range: {bbox} | {line['bbox']} | {img_size}")
380
+
381
+ key = line['data_type'] if 'category' not in line else line['category'] + ":" + line['data_type']
382
+ prediction = str(line["prediction"])
383
+ try:
384
+ click_point = parse_bbox_aguvis(prediction)
385
+ # Do Normalization By Default
386
+ if click_point[0] > 1 or click_point[1] > 1:
387
+ click_point = (click_point[0] / img_size[0], click_point[1] / img_size[1])
388
+
389
+ match = (bbox[0] <= click_point[0] <= bbox[2]) and \
390
+ (bbox[1] <= click_point[1] <= bbox[3])
391
+
392
+ if match:
393
+ stats[key].append(1)
394
+ else:
395
+ stats[key].append(0)
396
+ is_wrong_format = False
397
+
398
+ except Exception as e:
399
+ logger.warning(f"exception in screenspot eval:{e}")
400
+ stats[key].append(-1)
401
+ match, is_wrong_format, click_point = False, True, None
402
+
403
+ result.append(
404
+ {
405
+ "img_path": os.path.join(self.img_root, line["image_path"]),
406
+ "text": line["question"],
407
+ "bbox": line["bbox"],
408
+ "parsed_bbox": bbox,
409
+ "type": line["data_type"],
410
+ "source": line["data_source"],
411
+ "match": match,
412
+ "is_wrong_format": is_wrong_format,
413
+ "pred": click_point,
414
+ }
415
+ )
416
+
417
+ final_score_dict = {}
418
+ # Record the number of each category
419
+ final_score_dict.update({k + ':cnt': len(stats[k]) for k in stats})
420
+ # Calculate the Overall stats
421
+ full_stats = []
422
+ for v in stats.values():
423
+ full_stats.extend(v)
424
+ final_score_dict['Overall_Accuracy'] = np.mean([x > 0 for x in full_stats]) * 100
425
+ final_score_dict['Format_Err_Rate'] = np.mean([x < 0 for x in full_stats]) * 100
426
+ # Calculate the Accuracy of Text / Icon
427
+ text_stats = [v for k, v in stats.items() if k.endswith('text') for x in v]
428
+ text_stats = itertools.chain(*text_stats)
429
+ final_score_dict['Text_Accuracy'] = np.mean([x > 0 for x in text_stats]) * 100
430
+ icon_stats = [v for k, v in stats.items() if k.endswith('icon') for x in v]
431
+ icon_stats = itertools.chain(*icon_stats)
432
+ final_score_dict['Icon_Accuracy'] = np.mean([x > 0 for x in icon_stats]) * 100
433
+ # Calculate the Accuracy of Each Category
434
+ if 'category' in data:
435
+ cates = list(set(data['category']))
436
+ for c in cates:
437
+ sub_stats = [v for k, v in stats.items() if k.split(":")[0] == c for x in v]
438
+ sub_stats = itertools.chain(*sub_stats)
439
+ final_score_dict[c + '_Accuracy'] = np.mean([x > 0 for x in sub_stats]) * 100
440
+
441
+ score_pth = get_intermediate_file_path(eval_file, '_score', 'json')
442
+ dump(final_score_dict, score_pth)
443
+
444
+ failure_cases_path = os.environ.get("FAILURE_CASES_PATH", None)
445
+ if failure_cases_path is not None:
446
+ def click_distance(bbox, click_point):
447
+ x, y = click_point
448
+ x1, y1, x2, y2 = bbox
449
+ xc, yc = (x1 + x2) / 2, (y1 + y2) / 2
450
+ w, h = x2 - x1, y2 - y1
451
+ abs_shift_to_center = [abs(x - xc), abs(y - yc)] # noqa: E501
452
+ width_outside, height_outside = [max(0, abs_shift_to_center[0] - w / 2), max(0, abs_shift_to_center[1] - h / 2)] # noqa: E501
453
+ return (width_outside ** 2 + height_outside ** 2) ** 0.5 # noqa: E501
454
+
455
+ wrong_format_result = [res for res in result if res["is_wrong_format"]]
456
+ missed_result = [res for res in result if not res["match"] and not res["is_wrong_format"]]
457
+ missed_result.sort(key=lambda r: click_distance(r["parsed_bbox"], r["pred"]), reverse=True)
458
+ failure_cases = wrong_format_result + missed_result
459
+
460
+ with open(failure_cases_path, "w") as f:
461
+ json.dump(failure_cases, f, indent=4, ensure_ascii=False)
462
+ return final_score_dict
reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/GUI/screenspot_pro.py ADDED
@@ -0,0 +1,461 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ast
2
+ import itertools
3
+ import json
4
+ import os
5
+ import os.path as osp
6
+ import re
7
+ from collections import defaultdict
8
+
9
+ import numpy as np
10
+ import pandas as pd
11
+ from PIL import Image
12
+ from tqdm import tqdm
13
+
14
+ from vlmeval.dataset.image_base import ImageBaseDataset
15
+ from vlmeval.smp import LMUDataRoot, dump, get_intermediate_file_path, get_logger, load, toliststr
16
+
17
+ logger = get_logger(__name__)
18
+
19
+ """
20
+ {
21
+ "img_filename": "web_3b0ad239-da6b-4f6f-8f12-f674dc90ff33.png",
22
+ "bbox": [42, 1102, 197, 70],
23
+ "question": "view the details of the item",
24
+ "data_type": "text",
25
+ "data_source": "shop"
26
+ },
27
+ {
28
+ "img_filename": "web_3b0ad239-da6b-4f6f-8f12-f674dc90ff33.png",
29
+ "bbox": [93, 74, 86, 132],
30
+ "question": "view the previous photo",
31
+ "data_type": "icon",
32
+ "data_source": "shop"
33
+ }
34
+ """
35
+
36
+ SYSTEM_PROMPT = """You are a GUI agent. You are given a task and a screenshot of the screen. You need to perform pyautogui click/moveTo action to complete the task. The answer format is `pyautogui.click(x=?, y=?), x and y is necessary`""" # noqa: E501
37
+
38
+ USER_INSTRUCTION = """Please complete the following tasks by clicking using `pyautogui.click`:\n{instruction}""" # noqa: E501
39
+
40
+ SYSTEM_PROMPT_V2 = """You are a GUI agent. You are given a screenshot of the screen and the description of a target element. You need to click the target element using `pyautogui.click`. The answer format is `pyautogui.click(x=?, y=?), x and y is necessary`""" # noqa: E501
41
+ USER_INSTRUCTION_V2 = """Please click the following target element using `pyautogui.click`:\n{description}"""
42
+
43
+
44
+ def parse_bbox_aguvis(response):
45
+ match = re.search(r"x=([\d.]+), y=([\d.]+)", response)
46
+ if match:
47
+ click_point = [float(match.group(1)), float(match.group(2))]
48
+ else:
49
+ click_point = [0.0, 0.0]
50
+ return click_point
51
+
52
+
53
+ def compute_iou(box1, box2):
54
+ """
55
+ Compute the Intersection over Union (IoU) of two bounding boxes.
56
+
57
+ Parameters:
58
+ - box1 (list of float): Bounding box [x_min, y_min, x_max, y_max].
59
+ - box2 (list of float): Bounding box [x_min, y_min, x_max, y_max].
60
+
61
+ Returns:
62
+ - float: IoU of box1 and box2.
63
+ """
64
+ # Determine the coordinates of the intersection rectangle
65
+ x_left = max(box1[0], box2[0])
66
+ y_top = max(box1[1], box2[1])
67
+ x_right = min(box1[2], box2[2])
68
+ y_bottom = min(box1[3], box2[3])
69
+
70
+ # Compute the area of intersection
71
+ intersection_area = max(0, x_right - x_left) * max(0, y_bottom - y_top)
72
+
73
+ # Compute the area of both bounding boxes
74
+ box1_area = (box1[2] - box1[0]) * (box1[3] - box1[1])
75
+ box2_area = (box2[2] - box2[0]) * (box2[3] - box2[1])
76
+
77
+ # Compute the area of the union
78
+ union_area = box1_area + box2_area - intersection_area
79
+
80
+ # Compute the Intersection over Union
81
+ iou = intersection_area / union_area
82
+
83
+ return iou
84
+
85
+
86
+ def compute_accuracy(box1, box2, threshold=0.5):
87
+ """
88
+ Compute the accuracy of two bounding boxes based on a specified threshold.
89
+
90
+ Parameters:
91
+ - box1 (list of float): Bounding box [x_min, y_min, x_max, y_max].
92
+ - box2 (list of float): Bounding box [x_min, y_min, x_max, y_max].
93
+ - threshold (float): Threshold for the IoU to consider the prediction correct.
94
+
95
+ Returns:
96
+ - float: Accuracy of the prediction based on the IoU threshold.
97
+ """
98
+ iou = compute_iou(box1, box2)
99
+ return iou >= threshold
100
+
101
+
102
+ def compute_center_accuracy(box1, box2):
103
+ """
104
+ Compute if the center point of box 2 is within box 1.
105
+
106
+ Parameters:
107
+ - box1 (list of float): Bounding box [x_min, y_min, x_max, y_max].
108
+ - box2 (list of float): Bounding box [x_min, y_min, x_max, y_max].
109
+
110
+ Returns:
111
+ - bool: True if the center point of box 2 is within box 1, False otherwise.
112
+ """
113
+ # Compute the center point of box 2
114
+ center_x = (box2[0] + box2[2]) / 2
115
+ center_y = (box2[1] + box2[3]) / 2
116
+
117
+ # Check if the center point is within box 1
118
+ return box1[0] <= center_x <= box1[2] and box1[1] <= center_y <= box1[3]
119
+
120
+
121
+ def convert_bbox(bbox, image_path, convert_xywh_to_x1y1x2y2=True):
122
+ new_bbox = bbox if isinstance(bbox, list) else ast.literal_eval(bbox)
123
+ if convert_xywh_to_x1y1x2y2:
124
+ new_bbox = [
125
+ new_bbox[0],
126
+ new_bbox[1],
127
+ new_bbox[0] + new_bbox[2],
128
+ new_bbox[1] + new_bbox[3],
129
+ ]
130
+ image = Image.open(image_path)
131
+ img_size = image.size
132
+ new_bbox = [
133
+ new_bbox[0] / img_size[0],
134
+ new_bbox[1] / img_size[1],
135
+ new_bbox[2] / img_size[0],
136
+ new_bbox[3] / img_size[1],
137
+ ]
138
+ return new_bbox
139
+
140
+
141
+ class ScreenSpot_Pro(ImageBaseDataset):
142
+ MODALITY = "IMAGE"
143
+ TYPE = "GUI"
144
+ DATASET_URL = {
145
+ "ScreenSpot_Pro_Development": "https://opencompass.openxlab.space/utils/benchmarks/GUI/ScreenSpot_Pro/ScreenSpot_Pro_Development.tsv", # noqa
146
+ "ScreenSpot_Pro_Creative": "https://opencompass.openxlab.space/utils/benchmarks/GUI/ScreenSpot_Pro/ScreenSpot_Pro_Creative.tsv", # noqa
147
+ "ScreenSpot_Pro_CAD": "https://opencompass.openxlab.space/utils/benchmarks/GUI/ScreenSpot_Pro/ScreenSpot_Pro_CAD.tsv", # noqa
148
+ "ScreenSpot_Pro_Scientific": "https://opencompass.openxlab.space/utils/benchmarks/GUI/ScreenSpot_Pro/ScreenSpot_Pro_Scientific.tsv", # noqa
149
+ "ScreenSpot_Pro_Office": "https://opencompass.openxlab.space/utils/benchmarks/GUI/ScreenSpot_Pro/ScreenSpot_Pro_Office.tsv", # noqa
150
+ "ScreenSpot_Pro_OS": "https://opencompass.openxlab.space/utils/benchmarks/GUI/ScreenSpot_Pro/ScreenSpot_Pro_OS.tsv", # noqa
151
+ } # path
152
+ DATASET_MD5 = {
153
+ 'ScreenSpot_Pro_Development': '45b93df1d5814885011d682fe1b0f959',
154
+ 'ScreenSpot_Pro_Creative': 'a15867fee82ba8cd95581895c55f03cd',
155
+ 'ScreenSpot_Pro_CAD': '0faa3bc29eba359766c3a7ca2c4d8917',
156
+ 'ScreenSpot_Pro_Scientific': 'edc2e1f2b53af5fff6480b77c4986b81',
157
+ 'ScreenSpot_Pro_Office': '8756c128cf567274c2647423ccc4eaf0',
158
+ 'ScreenSpot_Pro_OS': '49c3eaaa7df6d22475c39120fe8f1c06'
159
+ }
160
+ EVAL_TYPE = "point" # point or rectangle
161
+ RE_TYPE = "functional" # type of referring expressions: functional or composite
162
+
163
+ def __init__(
164
+ self,
165
+ dataset="ScreenSpot_Pro_Development",
166
+ skip_noimg=True,
167
+ skeleton=False,
168
+ re_type="functional",
169
+ ):
170
+ # st()
171
+ ROOT = LMUDataRoot()
172
+ # You can override this variable to save image files to a different directory
173
+ self.dataset_name = dataset
174
+ self.img_root = osp.join(ROOT, "images", self.dataset_name)
175
+ self.RE_TYPE = re_type
176
+ if skeleton:
177
+ return
178
+
179
+ data = self.load_data(dataset)
180
+ self.skip_noimg = skip_noimg
181
+ if skip_noimg and "image" in data:
182
+ data = data[~pd.isna(data["image"])]
183
+
184
+ data["index"] = [str(idx + 1) for idx, x in enumerate(data["bbox"])]
185
+
186
+ self.meta_only = True
187
+ self.parse_response_func = parse_bbox_aguvis # TODO: parse function can be specified through kwargs when initializing the dataset # noqa: E501
188
+
189
+ # The image field can store the base64 encoded image or another question index (for saving space) # noqa: E501
190
+ if "image" in data:
191
+ data["image"] = [str(x) for x in data["image"]]
192
+ image_map = {x: y for x, y in zip(data["index"], data["image"])}
193
+ for k in image_map:
194
+ if len(image_map[k]) <= 64:
195
+ idx = image_map[k]
196
+ assert idx in image_map and len(image_map[idx]) > 64
197
+ image_map[k] = image_map[idx]
198
+
199
+ images = [toliststr(image_map[k]) for k in data["index"]]
200
+ data["image"] = [x[0] if len(x) == 1 else x for x in images]
201
+ self.meta_only = False
202
+
203
+ self.data = data
204
+
205
+ @classmethod
206
+ def get_action_space(self):
207
+ return ""
208
+
209
+ @classmethod
210
+ def get_trajectory(self, line):
211
+ traj_dict = {}
212
+ if self.RE_TYPE == "functional":
213
+ traj_dict["task"] = line["question"]
214
+ else:
215
+ traj_dict["task"] = line["description"]
216
+ return traj_dict
217
+
218
+ def build_prompt(self, line):
219
+ if isinstance(line, int):
220
+ line = self.data.iloc[line]
221
+ tgt_path = self.dump_image(line)
222
+
223
+ if self.RE_TYPE == "functional":
224
+ user_instruction = USER_INSTRUCTION.format(instruction=line["question"])
225
+ else:
226
+ user_instruction = USER_INSTRUCTION_V2.format(
227
+ description=line["description"]
228
+ )
229
+
230
+ msgs = []
231
+ # add system prompt
232
+ if self.RE_TYPE == "functional":
233
+ msgs.append(dict(role="system", type="text", value=SYSTEM_PROMPT))
234
+ else:
235
+ msgs.append(dict(role="system", type="text", value=SYSTEM_PROMPT_V2))
236
+ if isinstance(tgt_path, list):
237
+ msgs.extend([dict(type="image", value=p) for p in tgt_path])
238
+ else:
239
+ msgs = [dict(type="image", value=tgt_path)]
240
+ msgs.append(dict(type="text", value=user_instruction))
241
+ return msgs
242
+
243
+ def evaluate(self, eval_file, **judge_kwargs):
244
+ # st()
245
+ if self.EVAL_TYPE == "point":
246
+ return self.evaluate_point(eval_file, **judge_kwargs)
247
+
248
+ elif self.EVAL_TYPE == "rectangle":
249
+ return self.evaluate_rectangle(eval_file, **judge_kwargs)
250
+
251
+ def evaluate_rectangle(self, eval_file, **judge_kwargs):
252
+ scorers = {
253
+ "IoU": compute_iou,
254
+ "ACC@0.1": lambda x, y: compute_accuracy(x, y, 0.1),
255
+ "ACC@0.3": lambda x, y: compute_accuracy(x, y, 0.3),
256
+ "ACC@0.5": lambda x, y: compute_accuracy(x, y, 0.5),
257
+ "ACC@0.7": lambda x, y: compute_accuracy(x, y, 0.7),
258
+ "ACC@0.9": lambda x, y: compute_accuracy(x, y, 0.9),
259
+ "Center_ACC": compute_center_accuracy,
260
+ }
261
+ results_dict = {}
262
+ for key in scorers.keys():
263
+ results_dict.update(
264
+ {
265
+ key: [],
266
+ key + "_text": [],
267
+ key + "_icon": [],
268
+ }
269
+ )
270
+
271
+ result = []
272
+ data = load(eval_file)
273
+ assert "bbox" in data and "prediction" in data
274
+ lt = len(data)
275
+ lines = [data.iloc[i] for i in range(lt)]
276
+ for i in tqdm(range(len(lines))):
277
+ line = lines[i]
278
+ bbox = convert_bbox(
279
+ line["bbox"], os.path.join(self.img_root, line["image_path"]), convert_xywh_to_x1y1x2y2=False
280
+ )
281
+ prediction = str(line["prediction"])
282
+ try:
283
+ click_point = parse_bbox_aguvis(prediction)
284
+
285
+ match = {}
286
+ for score_key, score_value in scorers.items():
287
+ score = score_value(bbox, click_point)
288
+ if score_key != "IoU":
289
+ match[score_key.replace("ACC", "match")] = score
290
+ results_dict[score_key].append(score)
291
+ if line["ui_type"] == "text":
292
+ results_dict[score_key + "_text"].append(score)
293
+ else:
294
+ results_dict[score_key + "_icon"].append(score)
295
+ except Exception:
296
+ click_point = None
297
+ match = {score_key: False for score_key in scorers.keys() if score_key != "IoU"}
298
+ result.append(
299
+ {
300
+ "img_path": os.path.join(self.img_root, line["image_path"]),
301
+ "text": line["question"],
302
+ "bbox": line["bbox"],
303
+ "parsed_bbox": bbox,
304
+ "type": line["ui_type"],
305
+ "source": line["application"],
306
+ "pred": click_point,
307
+ "num_matched": sum(match.values()),
308
+ **match,
309
+ }
310
+ )
311
+ for key in results_dict:
312
+ if len(results_dict[key]) == 0:
313
+ results_dict[key] = str(0)
314
+ else:
315
+ results_dict[key] = str(sum(results_dict[key]) / len(results_dict[key]))
316
+ score_pth = get_intermediate_file_path(eval_file, '_score', 'json')
317
+ dump(results_dict, score_pth)
318
+
319
+ failure_cases_path = os.environ.get("FAILURE_CASES_PATH", None)
320
+ if failure_cases_path is not None:
321
+ failure_cases = [res for res in result if not res["match"] and res["is_wrong_format"]]
322
+ failure_cases.sort(key=lambda r: r["num_matched"], reverse=True)
323
+
324
+ with open(failure_cases_path, "w") as f:
325
+ json.dump(failure_cases, f, indent=4, ensure_ascii=False)
326
+ return results_dict
327
+
328
+ def evaluate_point(self, eval_file, **judge_kwargs):
329
+ # -1: format_err, 0: wrong, 1: correct
330
+ stats = defaultdict(list)
331
+ # Will include instance-level results
332
+ result = []
333
+
334
+ data = load(eval_file)
335
+ assert "bbox" in data and "prediction" in data
336
+ lt = len(data)
337
+ lines = [data.iloc[i] for i in range(lt)]
338
+ for i in tqdm(range(len(lines))):
339
+ line = lines[i]
340
+ bbox = (
341
+ line["bbox"]
342
+ if isinstance(line["bbox"], list)
343
+ else ast.literal_eval(line["bbox"])
344
+ )
345
+ # The format of bbox is (x1, y1, x2, y2)
346
+
347
+ image = Image.open(os.path.join(self.img_root, line["image_path"]))
348
+ img_size = image.size
349
+
350
+ def make_safe(value):
351
+ if value == -1:
352
+ # we can tolerate -1 as a special value and nomalize it to 0
353
+ return 0
354
+ else:
355
+ return value
356
+
357
+ bbox = [
358
+ make_safe(bbox[0]) / img_size[0],
359
+ make_safe(bbox[1]) / img_size[1],
360
+ make_safe(bbox[2]) / img_size[0],
361
+ make_safe(bbox[3]) / img_size[1],
362
+ ]
363
+
364
+ if any([x < 0 or x > 1 for x in bbox]):
365
+ raise ValueError(f"bbox out of range: {bbox} | {line['bbox']} | {img_size}")
366
+
367
+ key = line["category"] + ":" + line['ui_type']
368
+ prediction = str(line["prediction"])
369
+ try:
370
+ click_point = self.parse_response_func(prediction)
371
+ # Do Normalization By Default
372
+ if click_point[0] > 1 or click_point[1] > 1:
373
+ click_point = (click_point[0] / img_size[0], click_point[1] / img_size[1])
374
+
375
+ match = (bbox[0] <= click_point[0] <= bbox[2]) and \
376
+ (bbox[1] <= click_point[1] <= bbox[3])
377
+
378
+ if match:
379
+ stats[key].append(1)
380
+ else:
381
+ stats[key].append(0)
382
+ is_wrong_format = False
383
+
384
+ except Exception as e:
385
+ logger.warning(f"exception in screenspot eval:{e}")
386
+ stats[key].append(-1)
387
+ match, is_wrong_format, click_point = False, True, None
388
+
389
+ result.append(
390
+ {
391
+ "img_path": os.path.join(self.img_root, line["image_path"]),
392
+ "text": line["question"],
393
+ "bbox": line["bbox"],
394
+ "parsed_bbox": bbox,
395
+ "type": line["ui_type"],
396
+ "source": line["application"],
397
+ "match": match,
398
+ "is_wrong_format": is_wrong_format,
399
+ "pred": click_point,
400
+ }
401
+ )
402
+
403
+ final_score_dict = {}
404
+ # Record the number of each category
405
+ final_score_dict.update({k + ':cnt': len(stats[k]) for k in stats})
406
+ # Calculate the Overall stats
407
+ full_stats = []
408
+ for v in stats.values():
409
+ full_stats.extend(v)
410
+ final_score_dict['Overall_Accuracy'] = np.mean([x > 0 for x in full_stats]) * 100
411
+ final_score_dict['Format_Err_Rate'] = np.mean([x < 0 for x in full_stats]) * 100
412
+ # Calculate the Accuracy of Text / Icon
413
+ text_stats = [v for k, v in stats.items() if k.split(":")[1] == "text" for x in v]
414
+ text_stats = itertools.chain(*text_stats)
415
+ final_score_dict['Text_Accuracy'] = np.mean([x > 0 for x in text_stats]) * 100
416
+ icon_stats = [v for k, v in stats.items() if k.split(":")[1] == "icon" for x in v]
417
+ icon_stats = itertools.chain(*icon_stats)
418
+ final_score_dict['Icon_Accuracy'] = np.mean([x > 0 for x in icon_stats]) * 100
419
+ # Calculate the Accuracy of Each Category
420
+ cates = list(set(data['category']))
421
+ for c in cates:
422
+ sub_stats = [v for k, v in stats.items() if k.split(":")[0] == c for x in v]
423
+ sub_stats = itertools.chain(*sub_stats)
424
+ final_score_dict[c + '_Accuracy'] = np.mean([x > 0 for x in sub_stats]) * 100
425
+
426
+ score_pth = get_intermediate_file_path(eval_file, '_score', 'json')
427
+ dump(final_score_dict, score_pth)
428
+
429
+ failure_cases_path = os.environ.get("FAILURE_CASES_PATH", None)
430
+ if failure_cases_path is not None:
431
+ def click_distance(bbox, click_point):
432
+ x, y = click_point
433
+ x1, y1, x2, y2 = bbox
434
+ xc, yc = (x1 + x2) / 2, (y1 + y2) / 2
435
+ w, h = x2 - x1, y2 - y1
436
+ abs_shift_to_center = [abs(x - xc), abs(y - yc)] # noqa: E501
437
+ width_outside, height_outside = [max(0, abs_shift_to_center[0] - w / 2), max(0, abs_shift_to_center[1] - h / 2)] # noqa: E501
438
+ return (width_outside ** 2 + height_outside ** 2) ** 0.5 # noqa: E501
439
+
440
+ wrong_format_result = [res for res in result if res["is_wrong_format"]]
441
+ missed_result = [res for res in result if not res["match"] and not res["is_wrong_format"]]
442
+ missed_result.sort(key=lambda r: click_distance(r["parsed_bbox"], r["pred"]), reverse=True)
443
+ failure_cases = wrong_format_result + missed_result
444
+
445
+ with open(failure_cases_path, "w") as f:
446
+ json.dump(failure_cases, f, indent=4, ensure_ascii=False)
447
+
448
+ successful_cases_path = os.environ.get("SUCCESSFUL_CASES_PATH", None)
449
+ if successful_cases_path is not None:
450
+ def _click_distance(bbox, click_point):
451
+ x, y = click_point
452
+ x1, y1, x2, y2 = bbox
453
+ xc, yc = (x1 + x2) / 2, (y1 + y2) / 2
454
+ x_shift, y_shift = x - xc, y - yc
455
+ return (x_shift ** 2 + y_shift ** 2) ** 0.5
456
+
457
+ successful_cases = [res for res in result if res["match"]]
458
+ successful_cases.sort(key=lambda r: _click_distance(r["parsed_bbox"], r["pred"]), reverse=True)
459
+ with open(successful_cases_path, "w") as f:
460
+ json.dump(successful_cases, f, indent=4, ensure_ascii=False)
461
+ return final_score_dict
reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/GUI/screenspot_v2.py ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ast
2
+ import os.path as osp
3
+ import re
4
+
5
+ import pandas as pd
6
+ from PIL import Image
7
+
8
+ from vlmeval.smp import LMUDataRoot, get_logger, load, toliststr
9
+ from .screenspot import ScreenSpot
10
+
11
+ logger = get_logger(__name__)
12
+
13
+ """
14
+ {
15
+ "img_filename": "web_3b0ad239-da6b-4f6f-8f12-f674dc90ff33.png",
16
+ "bbox": [42, 1102, 197, 70],
17
+ "instruction": "view the details of the item",
18
+ "data_type": "text",
19
+ "data_source": "shop"
20
+ },
21
+ {
22
+ "img_filename": "web_3b0ad239-da6b-4f6f-8f12-f674dc90ff33.png",
23
+ "bbox": [93, 74, 86, 132],
24
+ "instruction": "view the previous photo",
25
+ "data_type": "icon",
26
+ "data_source": "shop"
27
+ }
28
+ """
29
+
30
+ SYSTEM_PROMPT = """You are a GUI agent. You are given a task and a screenshot of the screen. You need to perform pyautogui click/moveTo action to complete the task. The answer format is `pyautogui.click(x=?, y=?), x and y is necessary`""" # noqa: E501
31
+
32
+ USER_INSTRUCTION = """Please complete the following tasks by clicking using `pyautogui.click`:\n{instruction}""" # noqa: E501
33
+
34
+ SYSTEM_PROMPT_V2 = """You are a GUI agent. You are given a screenshot of the screen and the description of a target element. You need to click the target element using `pyautogui.click`. The answer format is `pyautogui.click(x=?, y=?), x and y is necessary`""" # noqa: E501
35
+ USER_INSTRUCTION_V2 = """Please click the following target element using `pyautogui.click`:\n{description}"""
36
+
37
+
38
+ def parse_bbox_aguvis(response):
39
+ match = re.search(r"x=([\d.]+), y=([\d.]+)", response)
40
+ if match:
41
+ click_point = [float(match.group(1)), float(match.group(2))]
42
+ else:
43
+ click_point = [0.0, 0.0]
44
+ return click_point
45
+
46
+
47
+ def compute_iou(box1, box2):
48
+ """
49
+ Compute the Intersection over Union (IoU) of two bounding boxes.
50
+
51
+ Parameters:
52
+ - box1 (list of float): Bounding box [x_min, y_min, x_max, y_max].
53
+ - box2 (list of float): Bounding box [x_min, y_min, x_max, y_max].
54
+
55
+ Returns:
56
+ - float: IoU of box1 and box2.
57
+ """
58
+ # Determine the coordinates of the intersection rectangle
59
+ x_left = max(box1[0], box2[0])
60
+ y_top = max(box1[1], box2[1])
61
+ x_right = min(box1[2], box2[2])
62
+ y_bottom = min(box1[3], box2[3])
63
+
64
+ # Compute the area of intersection
65
+ intersection_area = max(0, x_right - x_left) * max(0, y_bottom - y_top)
66
+
67
+ # Compute the area of both bounding boxes
68
+ box1_area = (box1[2] - box1[0]) * (box1[3] - box1[1])
69
+ box2_area = (box2[2] - box2[0]) * (box2[3] - box2[1])
70
+
71
+ # Compute the area of the union
72
+ union_area = box1_area + box2_area - intersection_area
73
+
74
+ # Compute the Intersection over Union
75
+ iou = intersection_area / union_area
76
+
77
+ return iou
78
+
79
+
80
+ def compute_accuracy(box1, box2, threshold=0.5):
81
+ """
82
+ Compute the accuracy of two bounding boxes based on a specified threshold.
83
+
84
+ Parameters:
85
+ - box1 (list of float): Bounding box [x_min, y_min, x_max, y_max].
86
+ - box2 (list of float): Bounding box [x_min, y_min, x_max, y_max].
87
+ - threshold (float): Threshold for the IoU to consider the prediction correct.
88
+
89
+ Returns:
90
+ - float: Accuracy of the prediction based on the IoU threshold.
91
+ """
92
+ iou = compute_iou(box1, box2)
93
+ return iou >= threshold
94
+
95
+
96
+ def compute_center_accuracy(box1, box2):
97
+ """
98
+ Compute if the center point of box 2 is within box 1.
99
+
100
+ Parameters:
101
+ - box1 (list of float): Bounding box [x_min, y_min, x_max, y_max].
102
+ - box2 (list of float): Bounding box [x_min, y_min, x_max, y_max].
103
+
104
+ Returns:
105
+ - bool: True if the center point of box 2 is within box 1, False otherwise.
106
+ """
107
+ # Compute the center point of box 2
108
+ center_x = (box2[0] + box2[2]) / 2
109
+ center_y = (box2[1] + box2[3]) / 2
110
+
111
+ # Check if the center point is within box 1
112
+ return box1[0] <= center_x <= box1[2] and box1[1] <= center_y <= box1[3]
113
+
114
+
115
+ def convert_bbox(bbox, image_path):
116
+ new_bbox = bbox if isinstance(bbox, list) else ast.literal_eval(bbox)
117
+ new_bbox = [
118
+ new_bbox[0],
119
+ new_bbox[1],
120
+ new_bbox[0] + new_bbox[2],
121
+ new_bbox[1] + new_bbox[3],
122
+ ]
123
+ image = Image.open(image_path)
124
+ img_size = image.size
125
+ new_bbox = [
126
+ new_bbox[0] / img_size[0],
127
+ new_bbox[1] / img_size[1],
128
+ new_bbox[2] / img_size[0],
129
+ new_bbox[3] / img_size[1],
130
+ ]
131
+ return new_bbox
132
+
133
+
134
+ class ScreenSpotV2(ScreenSpot):
135
+ MODALITY = "IMAGE"
136
+ TYPE = "GUI"
137
+ DATASET_URL = {
138
+ "ScreenSpot_v2_Mobile": "ScreenSpot_v2_Mobile.tsv",
139
+ "ScreenSpot_v2_Desktop": "ScreenSpot_v2_Desktop.tsv",
140
+ "ScreenSpot_v2_Web": "ScreenSpot_v2_Web.tsv",
141
+ } # path
142
+ DATASET_MD5 = {}
143
+ EVAL_TYPE = "point" # point or rectangle
144
+ RE_TYPE = "functional" # type of referring expressions: functional or composite
145
+
146
+ def __init__(
147
+ self,
148
+ dataset="ScreenSpot_Mobile",
149
+ skip_noimg=True,
150
+ skeleton=False,
151
+ re_type="functional",
152
+ ):
153
+ # st()
154
+ ROOT = LMUDataRoot()
155
+ # You can override this variable to save image files to a different directory
156
+ self.dataset_name = dataset
157
+ self.img_root = osp.join(ROOT, "ScreenSpot_v2", "screenspotv2_image")
158
+ self.RE_TYPE = re_type
159
+ if skeleton:
160
+ return
161
+
162
+ data = self.load_data(dataset)
163
+ self.skip_noimg = skip_noimg
164
+ if skip_noimg and "image" in data:
165
+ data = data[~pd.isna(data["image"])]
166
+
167
+ data["index"] = [str(idx + 1) for idx, x in enumerate(data["bbox"])]
168
+
169
+ self.meta_only = True
170
+ self.parse_response_func = parse_bbox_aguvis # TODO: parse function can be specified through kwargs when initializing the dataset # noqa: E501
171
+
172
+ # The image field can store the base64 encoded image or another question index (for saving space)
173
+ if "image" in data:
174
+ data["image"] = [str(x) for x in data["image"]]
175
+ image_map = {x: y for x, y in zip(data["index"], data["image"])}
176
+ for k in image_map:
177
+ if len(image_map[k]) <= 64:
178
+ idx = image_map[k]
179
+ assert idx in image_map and len(image_map[idx]) > 64
180
+ image_map[k] = image_map[idx]
181
+
182
+ images = [toliststr(image_map[k]) for k in data["index"]]
183
+ data["image"] = [x[0] if len(x) == 1 else x for x in images]
184
+ self.meta_only = False
185
+
186
+ if "img_filename" in data:
187
+ paths = [toliststr(x) for x in data["img_filename"]]
188
+ data["image_path"] = [x[0] if len(x) == 1 else x for x in paths]
189
+
190
+ # if np.all([istype(x, int) for x in data["index"]]):
191
+ # data["index"] = [int(x) for x in data["index"]]
192
+
193
+ self.data = data
194
+ self.post_build(dataset)
195
+
196
+ def prepare_tsv(self, url, file_md5=None):
197
+ # st()
198
+ if self.RE_TYPE == "functional":
199
+ data_root = LMUDataRoot()
200
+ data_path = osp.join(data_root, "ScreenSpot_v2", url)
201
+ else:
202
+ data_path = self.DATASET_URL_V2[self.dataset_name]
203
+ return pd.DataFrame(load(data_path))
reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/GUI/vbgd.py ADDED
@@ -0,0 +1,447 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ast
2
+ import itertools
3
+ import json
4
+ import os
5
+ import os.path as osp
6
+ import re
7
+ from collections import defaultdict
8
+
9
+ import numpy as np
10
+ import pandas as pd
11
+ from PIL import Image
12
+ from tqdm import tqdm
13
+
14
+ from vlmeval.dataset.image_base import ImageBaseDataset
15
+ from vlmeval.smp import LMUDataRoot, dump, get_intermediate_file_path, get_logger, load, toliststr
16
+
17
+ logger = get_logger(__name__)
18
+
19
+ """
20
+ {
21
+ "img_filename": "web_3b0ad239-da6b-4f6f-8f12-f674dc90ff33.png",
22
+ "bbox": [42, 1102, 197, 70],
23
+ "question": "view the details of the item",
24
+ "data_type": "text",
25
+ "data_source": "shop"
26
+ },
27
+ {
28
+ "img_filename": "web_3b0ad239-da6b-4f6f-8f12-f674dc90ff33.png",
29
+ "bbox": [93, 74, 86, 132],
30
+ "question": "view the previous photo",
31
+ "data_type": "icon",
32
+ "data_source": "shop"
33
+ }
34
+ """
35
+
36
+ SYSTEM_PROMPT = """You are a GUI agent. You are given a task and a screenshot of the screen. You need to perform pyautogui click/moveTo action to complete the task. The answer format is `pyautogui.click(x=?, y=?), x and y is necessary`""" # noqa: E501
37
+
38
+ USER_INSTRUCTION = """Please complete the following tasks by clicking using `pyautogui.click`:\n{instruction}""" # noqa: E501
39
+
40
+ SYSTEM_PROMPT_V2 = """You are a GUI agent. You are given a screenshot of the screen and the description of a target element. You need to click the target element using `pyautogui.click`. The answer format is `pyautogui.click(x=?, y=?), x and y is necessary`""" # noqa: E501
41
+ USER_INSTRUCTION_V2 = """Please click the following target element using `pyautogui.click`:\n{description}"""
42
+
43
+
44
+ def parse_bbox_aguvis(response):
45
+ match = re.search(r"x=([\d.]+), y=([\d.]+)", response)
46
+ if match:
47
+ click_point = [float(match.group(1)), float(match.group(2))]
48
+ else:
49
+ click_point = [0.0, 0.0]
50
+ return click_point
51
+
52
+
53
+ def compute_iou(box1, box2):
54
+ """
55
+ Compute the Intersection over Union (IoU) of two bounding boxes.
56
+
57
+ Parameters:
58
+ - box1 (list of float): Bounding box [x_min, y_min, x_max, y_max].
59
+ - box2 (list of float): Bounding box [x_min, y_min, x_max, y_max].
60
+
61
+ Returns:
62
+ - float: IoU of box1 and box2.
63
+ """
64
+ # Determine the coordinates of the intersection rectangle
65
+ x_left = max(box1[0], box2[0])
66
+ y_top = max(box1[1], box2[1])
67
+ x_right = min(box1[2], box2[2])
68
+ y_bottom = min(box1[3], box2[3])
69
+
70
+ # Compute the area of intersection
71
+ intersection_area = max(0, x_right - x_left) * max(0, y_bottom - y_top)
72
+
73
+ # Compute the area of both bounding boxes
74
+ box1_area = (box1[2] - box1[0]) * (box1[3] - box1[1])
75
+ box2_area = (box2[2] - box2[0]) * (box2[3] - box2[1])
76
+
77
+ # Compute the area of the union
78
+ union_area = box1_area + box2_area - intersection_area
79
+
80
+ # Compute the Intersection over Union
81
+ iou = intersection_area / union_area
82
+
83
+ return iou
84
+
85
+
86
+ def compute_accuracy(box1, box2, threshold=0.5):
87
+ """
88
+ Compute the accuracy of two bounding boxes based on a specified threshold.
89
+
90
+ Parameters:
91
+ - box1 (list of float): Bounding box [x_min, y_min, x_max, y_max].
92
+ - box2 (list of float): Bounding box [x_min, y_min, x_max, y_max].
93
+ - threshold (float): Threshold for the IoU to consider the prediction correct.
94
+
95
+ Returns:
96
+ - float: Accuracy of the prediction based on the IoU threshold.
97
+ """
98
+ iou = compute_iou(box1, box2)
99
+ return iou >= threshold
100
+
101
+
102
+ def compute_center_accuracy(box1, box2):
103
+ """
104
+ Compute if the center point of box 2 is within box 1.
105
+
106
+ Parameters:
107
+ - box1 (list of float): Bounding box [x_min, y_min, x_max, y_max].
108
+ - box2 (list of float): Bounding box [x_min, y_min, x_max, y_max].
109
+
110
+ Returns:
111
+ - bool: True if the center point of box 2 is within box 1, False otherwise.
112
+ """
113
+ # Compute the center point of box 2
114
+ center_x = (box2[0] + box2[2]) / 2
115
+ center_y = (box2[1] + box2[3]) / 2
116
+
117
+ # Check if the center point is within box 1
118
+ return box1[0] <= center_x <= box1[2] and box1[1] <= center_y <= box1[3]
119
+
120
+
121
+ def convert_bbox(bbox, image_path, convert_xywh_to_x1y1x2y2=True):
122
+ new_bbox = bbox if isinstance(bbox, list) else ast.literal_eval(bbox)
123
+ if convert_xywh_to_x1y1x2y2:
124
+ new_bbox = [
125
+ new_bbox[0],
126
+ new_bbox[1],
127
+ new_bbox[0] + new_bbox[2],
128
+ new_bbox[1] + new_bbox[3],
129
+ ]
130
+ image = Image.open(image_path)
131
+ img_size = image.size
132
+ new_bbox = [
133
+ new_bbox[0] / img_size[0],
134
+ new_bbox[1] / img_size[1],
135
+ new_bbox[2] / img_size[0],
136
+ new_bbox[3] / img_size[1],
137
+ ]
138
+ return new_bbox
139
+
140
+
141
+ class VBGD(ImageBaseDataset):
142
+ MODALITY = "IMAGE"
143
+ TYPE = "GUI"
144
+ DATASET_URL = {"VBGD": "https://huggingface.co/datasets/Zery/VBGD_Dataset/resolve/main/VBGD.tsv"} # path
145
+ DATASET_MD5 = {"VBGD": "54615d8e27a93b3be13c71ddc09a6277"}
146
+ EVAL_TYPE = "point" # point or rectangle
147
+ RE_TYPE = "functional" # type of referring expressions: functional or composite
148
+
149
+ def __init__(
150
+ self,
151
+ dataset="VBGD_Development",
152
+ skip_noimg=True,
153
+ skeleton=False,
154
+ re_type="functional",
155
+ ):
156
+ # st()
157
+ ROOT = LMUDataRoot()
158
+ # You can override this variable to save image files to a different directory
159
+ self.dataset_name = dataset
160
+ self.img_root = osp.join(ROOT, "images", self.dataset_name)
161
+ self.RE_TYPE = re_type
162
+ if skeleton:
163
+ return
164
+
165
+ data = self.load_data(dataset)
166
+ self.skip_noimg = skip_noimg
167
+ if skip_noimg and "image" in data:
168
+ data = data[~pd.isna(data["image"])]
169
+
170
+ data["index"] = [str(idx + 1) for idx, x in enumerate(data["bbox"])]
171
+
172
+ self.meta_only = True
173
+ self.parse_response_func = parse_bbox_aguvis # TODO: parse function can be specified through kwargs when initializing the dataset # noqa: E501
174
+
175
+ # The image field can store the base64 encoded image or another question index (for saving space) # noqa: E501
176
+ if "image" in data:
177
+ data["image"] = [str(x) for x in data["image"]]
178
+ image_map = {x: y for x, y in zip(data["index"], data["image"])}
179
+ for k in image_map:
180
+ if len(image_map[k]) <= 64:
181
+ idx = image_map[k]
182
+ assert idx in image_map and len(image_map[idx]) > 64
183
+ image_map[k] = image_map[idx]
184
+
185
+ images = [toliststr(image_map[k]) for k in data["index"]]
186
+ data["image"] = [x[0] if len(x) == 1 else x for x in images]
187
+ self.meta_only = False
188
+
189
+ self.data = data
190
+
191
+ @classmethod
192
+ def get_action_space(self):
193
+ return ""
194
+
195
+ @classmethod
196
+ def get_trajectory(self, line):
197
+ traj_dict = {}
198
+ if self.RE_TYPE == "functional":
199
+ traj_dict["task"] = line["question"]
200
+ else:
201
+ traj_dict["task"] = line["description"]
202
+ return traj_dict
203
+
204
+ def build_prompt(self, line):
205
+ if isinstance(line, int):
206
+ line = self.data.iloc[line]
207
+ tgt_path = self.dump_image(line)
208
+
209
+ if self.RE_TYPE == "functional":
210
+ user_instruction = USER_INSTRUCTION.format(instruction=line["question"])
211
+ else:
212
+ user_instruction = USER_INSTRUCTION_V2.format(
213
+ description=line["description"]
214
+ )
215
+
216
+ msgs = []
217
+ # add system prompt
218
+ if self.RE_TYPE == "functional":
219
+ msgs.append(dict(role="system", type="text", value=SYSTEM_PROMPT))
220
+ else:
221
+ msgs.append(dict(role="system", type="text", value=SYSTEM_PROMPT_V2))
222
+ if isinstance(tgt_path, list):
223
+ msgs.extend([dict(type="image", value=p) for p in tgt_path])
224
+ else:
225
+ msgs = [dict(type="image", value=tgt_path)]
226
+ msgs.append(dict(type="text", value=user_instruction))
227
+ return msgs
228
+
229
+ def evaluate(self, eval_file, **judge_kwargs):
230
+ # st()
231
+ if self.EVAL_TYPE == "point":
232
+ return self.evaluate_point(eval_file, **judge_kwargs)
233
+
234
+ elif self.EVAL_TYPE == "rectangle":
235
+ return self.evaluate_rectangle(eval_file, **judge_kwargs)
236
+
237
+ def evaluate_rectangle(self, eval_file, **judge_kwargs):
238
+ scorers = {
239
+ "IoU": compute_iou,
240
+ "ACC@0.1": lambda x, y: compute_accuracy(x, y, 0.1),
241
+ "ACC@0.3": lambda x, y: compute_accuracy(x, y, 0.3),
242
+ "ACC@0.5": lambda x, y: compute_accuracy(x, y, 0.5),
243
+ "ACC@0.7": lambda x, y: compute_accuracy(x, y, 0.7),
244
+ "ACC@0.9": lambda x, y: compute_accuracy(x, y, 0.9),
245
+ "Center_ACC": compute_center_accuracy,
246
+ }
247
+ results_dict = {}
248
+ for key in scorers.keys():
249
+ results_dict.update(
250
+ {
251
+ key: [],
252
+ key + "_text": [],
253
+ key + "_icon": [],
254
+ }
255
+ )
256
+
257
+ result = []
258
+ data = load(eval_file)
259
+ assert "bbox" in data and "prediction" in data
260
+ lt = len(data)
261
+ lines = [data.iloc[i] for i in range(lt)]
262
+ for i in tqdm(range(len(lines))):
263
+ line = lines[i]
264
+ bbox = convert_bbox(
265
+ line["bbox"], os.path.join(self.img_root, line["image_path"]), convert_xywh_to_x1y1x2y2=False
266
+ )
267
+ prediction = str(line["prediction"])
268
+ try:
269
+ click_point = parse_bbox_aguvis(prediction)
270
+
271
+ match = {}
272
+ for score_key, score_value in scorers.items():
273
+ score = score_value(bbox, click_point)
274
+ if score_key != "IoU":
275
+ match[score_key.replace("ACC", "match")] = score
276
+ results_dict[score_key].append(score)
277
+ if line["ui_type"] == "text":
278
+ results_dict[score_key + "_text"].append(score)
279
+ else:
280
+ results_dict[score_key + "_icon"].append(score)
281
+ except Exception:
282
+ click_point = None
283
+ match = {score_key: False for score_key in scorers.keys() if score_key != "IoU"}
284
+ result.append(
285
+ {
286
+ "img_path": os.path.join(self.img_root, line["image_path"]),
287
+ "text": line["question"],
288
+ "bbox": line["bbox"],
289
+ "parsed_bbox": bbox,
290
+ "type": line["ui_type"],
291
+ "source": line["application"],
292
+ "pred": click_point,
293
+ "num_matched": sum(match.values()),
294
+ **match,
295
+ }
296
+ )
297
+ for key in results_dict:
298
+ if len(results_dict[key]) == 0:
299
+ results_dict[key] = str(0)
300
+ else:
301
+ results_dict[key] = str(sum(results_dict[key]) / len(results_dict[key]))
302
+ score_pth = get_intermediate_file_path(eval_file, '_score', 'json')
303
+ dump(results_dict, score_pth)
304
+
305
+ failure_cases_path = os.environ.get("FAILURE_CASES_PATH", None)
306
+ if failure_cases_path is not None:
307
+ failure_cases = [res for res in result if not res["match"] and res["is_wrong_format"]]
308
+ failure_cases.sort(key=lambda r: r["num_matched"], reverse=True)
309
+
310
+ with open(failure_cases_path, "w") as f:
311
+ json.dump(failure_cases, f, indent=4, ensure_ascii=False)
312
+ return results_dict
313
+
314
+ def evaluate_point(self, eval_file, **judge_kwargs):
315
+ # -1: format_err, 0: wrong, 1: correct
316
+ stats = defaultdict(list)
317
+ # Will include instance-level results
318
+ result = []
319
+
320
+ data = load(eval_file)
321
+ assert "bbox" in data and "prediction" in data
322
+ lt = len(data)
323
+ lines = [data.iloc[i] for i in range(lt)]
324
+ for i in tqdm(range(len(lines))):
325
+ line = lines[i]
326
+ bbox = (
327
+ line["bbox"]
328
+ if isinstance(line["bbox"], list)
329
+ else ast.literal_eval(line["bbox"])
330
+ )
331
+ # The format of bbox is (x1, y1, x2, y2)
332
+
333
+ image = Image.open(os.path.join(self.img_root, line["image_path"]))
334
+ img_size = image.size
335
+
336
+ def make_safe(value):
337
+ if value == -1:
338
+ # we can tolerate -1 as a special value and nomalize it to 0
339
+ return 0
340
+ else:
341
+ return value
342
+
343
+ bbox = [
344
+ make_safe(bbox[0]) / img_size[0],
345
+ make_safe(bbox[1]) / img_size[1],
346
+ make_safe(bbox[2]) / img_size[0],
347
+ make_safe(bbox[3]) / img_size[1],
348
+ ]
349
+
350
+ if any([x < 0 or x > 1 for x in bbox]):
351
+ raise ValueError(f"bbox out of range: {bbox} | {line['bbox']} | {img_size}")
352
+
353
+ key = line["category"] + ":" + line['ui_type']
354
+ prediction = str(line["prediction"])
355
+ try:
356
+ click_point = self.parse_response_func(prediction)
357
+ # Do Normalization By Default
358
+ if click_point[0] > 1 or click_point[1] > 1:
359
+ click_point = (click_point[0] / img_size[0], click_point[1] / img_size[1])
360
+
361
+ match = (bbox[0] <= click_point[0] <= bbox[2]) and \
362
+ (bbox[1] <= click_point[1] <= bbox[3])
363
+
364
+ if match:
365
+ stats[key].append(1)
366
+ else:
367
+ stats[key].append(0)
368
+ is_wrong_format = False
369
+
370
+ except Exception as e:
371
+ logger.warning(f"exception in screenspot eval:{e}")
372
+ stats[key].append(-1)
373
+ match, is_wrong_format, click_point = False, True, None
374
+
375
+ result.append(
376
+ {
377
+ "img_path": os.path.join(self.img_root, line["image_path"]),
378
+ "text": line["question"],
379
+ "bbox": line["bbox"],
380
+ "parsed_bbox": bbox,
381
+ "type": line["ui_type"],
382
+ "source": line["application"],
383
+ "match": match,
384
+ "is_wrong_format": is_wrong_format,
385
+ "pred": click_point,
386
+ }
387
+ )
388
+
389
+ final_score_dict = {}
390
+ # Record the number of each category
391
+ final_score_dict.update({k + ':cnt': len(stats[k]) for k in stats})
392
+ # Calculate the Overall stats
393
+ full_stats = []
394
+ for v in stats.values():
395
+ full_stats.extend(v)
396
+ final_score_dict['Overall_Accuracy'] = np.mean([x > 0 for x in full_stats]) * 100
397
+ final_score_dict['Format_Err_Rate'] = np.mean([x < 0 for x in full_stats]) * 100
398
+ # Calculate the Accuracy of Text / Icon
399
+ text_stats = [v for k, v in stats.items() if k.split(":")[1] == "text" for x in v]
400
+ text_stats = itertools.chain(*text_stats)
401
+ final_score_dict['Text_Accuracy'] = np.mean([x > 0 for x in text_stats]) * 100
402
+ icon_stats = [v for k, v in stats.items() if k.split(":")[1] == "icon" for x in v]
403
+ icon_stats = itertools.chain(*icon_stats)
404
+ final_score_dict['Icon_Accuracy'] = np.mean([x > 0 for x in icon_stats]) * 100
405
+ # Calculate the Accuracy of Each Category
406
+ cates = list(set(data['category']))
407
+ for c in cates:
408
+ sub_stats = [v for k, v in stats.items() if k.split(":")[0] == c for x in v]
409
+ sub_stats = itertools.chain(*sub_stats)
410
+ final_score_dict[c + '_Accuracy'] = np.mean([x > 0 for x in sub_stats]) * 100
411
+
412
+ score_pth = get_intermediate_file_path(eval_file, '_score', 'json')
413
+ dump(final_score_dict, score_pth)
414
+
415
+ failure_cases_path = os.environ.get("FAILURE_CASES_PATH", None)
416
+ if failure_cases_path is not None:
417
+ def click_distance(bbox, click_point):
418
+ x, y = click_point
419
+ x1, y1, x2, y2 = bbox
420
+ xc, yc = (x1 + x2) / 2, (y1 + y2) / 2
421
+ w, h = x2 - x1, y2 - y1
422
+ abs_shift_to_center = [abs(x - xc), abs(y - yc)] # noqa: E501
423
+ width_outside, height_outside = [max(0, abs_shift_to_center[0] - w / 2), max(0, abs_shift_to_center[1] - h / 2)] # noqa: E501
424
+ return (width_outside ** 2 + height_outside ** 2) ** 0.5 # noqa: E501
425
+
426
+ wrong_format_result = [res for res in result if res["is_wrong_format"]]
427
+ missed_result = [res for res in result if not res["match"] and not res["is_wrong_format"]]
428
+ missed_result.sort(key=lambda r: click_distance(r["parsed_bbox"], r["pred"]), reverse=True)
429
+ failure_cases = wrong_format_result + missed_result
430
+
431
+ with open(failure_cases_path, "w") as f:
432
+ json.dump(failure_cases, f, indent=4, ensure_ascii=False)
433
+
434
+ successful_cases_path = os.environ.get("SUCCESSFUL_CASES_PATH", None)
435
+ if successful_cases_path is not None:
436
+ def _click_distance(bbox, click_point):
437
+ x, y = click_point
438
+ x1, y1, x2, y2 = bbox
439
+ xc, yc = (x1 + x2) / 2, (y1 + y2) / 2
440
+ x_shift, y_shift = x - xc, y - yc
441
+ return (x_shift ** 2 + y_shift ** 2) ** 0.5
442
+
443
+ successful_cases = [res for res in result if res["match"]]
444
+ successful_cases.sort(key=lambda r: _click_distance(r["parsed_bbox"], r["pred"]), reverse=True)
445
+ with open(successful_cases_path, "w") as f:
446
+ json.dump(successful_cases, f, indent=4, ensure_ascii=False)
447
+ return final_score_dict
reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/GUI/venusbench.py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ast
2
+ import os
3
+ import os.path as osp
4
+ import re
5
+ from collections import defaultdict
6
+
7
+ import numpy as np
8
+ import pandas as pd
9
+ from PIL import Image
10
+ from tqdm import tqdm
11
+
12
+ from vlmeval.dataset.image_base import ImageBaseDataset
13
+ from vlmeval.smp import LMUDataRoot, dump, get_intermediate_file_path, get_logger, load, toliststr
14
+
15
+ logger = get_logger(__name__)
16
+
17
+ SYSTEM_PROMPT = "You are a GUI agent. You are given a task and a screenshot of the screen. " \
18
+ "You need to perform pyautogui click/moveTo action to complete the task. " \
19
+ "The answer format is `pyautogui.click(x=?, y=?), x and y is necessary`"
20
+
21
+ USER_INSTRUCTION = "Please complete the following tasks by clicking using `pyautogui.click`:\n{instruction}"
22
+
23
+
24
+ def parse_bbox_aguvis(response):
25
+ match = re.search(r"x=([\d.]+), y=([\d.]+)", response)
26
+ if match:
27
+ click_point = [float(match.group(1)), float(match.group(2))]
28
+ else:
29
+ click_point = [0.0, 0.0]
30
+ return click_point
31
+
32
+
33
+ class VenusBench_GD(ImageBaseDataset):
34
+ MODALITY = "IMAGE"
35
+ TYPE = "GUI"
36
+ DATASET_URL = {
37
+ "VenusBench-GD": "https://huggingface.co/datasets/Zery/VBGD_Dataset/resolve/main/VenusBench.tsv",
38
+ }
39
+ DATASET_MD5 = {
40
+ 'VenusBench-GD': '6a2fe92d3ecf5a3b6503a1fe4891c5ea'
41
+ }
42
+
43
+ def __init__(
44
+ self,
45
+ dataset="VenusBench-GD",
46
+ skip_noimg=True,
47
+ skeleton=False,
48
+ ):
49
+ ROOT = LMUDataRoot()
50
+ self.dataset_name = dataset
51
+ self.img_root = osp.join(ROOT, "images", self.dataset_name)
52
+
53
+ if skeleton:
54
+ return
55
+
56
+ data = self.load_data(dataset)
57
+ self.skip_noimg = skip_noimg
58
+ if skip_noimg and "image" in data:
59
+ data = data[~pd.isna(data["image"])]
60
+
61
+ # Verify we have index properly
62
+ if "index" not in data:
63
+ data["index"] = [str(idx + 1) for idx in range(len(data))]
64
+
65
+ self.meta_only = True
66
+ self.parse_response_func = parse_bbox_aguvis
67
+
68
+ if "image" in data:
69
+ data["image"] = [str(x) for x in data["image"]]
70
+ image_map = {x: y for x, y in zip(data["index"], data["image"])}
71
+ for k in image_map:
72
+ if len(image_map[k]) <= 64:
73
+ idx = image_map[k]
74
+ assert idx in image_map and len(image_map[idx]) > 64
75
+ image_map[k] = image_map[idx]
76
+
77
+ images = [toliststr(image_map[k]) for k in data["index"]]
78
+ data["image"] = [x[0] if len(x) == 1 else x for x in images]
79
+ self.meta_only = False
80
+
81
+ self.data = data
82
+
83
+ @classmethod
84
+ def get_action_space(self):
85
+ return ""
86
+
87
+ @classmethod
88
+ def get_trajectory(self, line):
89
+ traj_dict = {}
90
+ traj_dict["task"] = line["question"]
91
+ return traj_dict
92
+
93
+ def build_prompt(self, line):
94
+ if isinstance(line, int):
95
+ line = self.data.iloc[line]
96
+ tgt_path = self.dump_image(line)
97
+ user_instruction = USER_INSTRUCTION.format(instruction=line["question"])
98
+ msgs = []
99
+ msgs.append(dict(role="system", type="text", value=SYSTEM_PROMPT))
100
+ if isinstance(tgt_path, list):
101
+ msgs.extend([dict(type="image", value=p) for p in tgt_path])
102
+ else:
103
+ msgs = [dict(type="image", value=tgt_path)]
104
+ msgs.append(dict(type="text", value=user_instruction))
105
+ return msgs
106
+
107
+ def evaluate(self, eval_file, **judge_kwargs):
108
+ stats = defaultdict(list)
109
+ result = []
110
+
111
+ data = load(eval_file)
112
+ assert "bbox" in data and "prediction" in data
113
+ lt = len(data)
114
+ lines = [data.iloc[i] for i in range(lt)]
115
+
116
+ for i in tqdm(range(len(lines))):
117
+ line = lines[i]
118
+ bbox = (
119
+ line["bbox"]
120
+ if isinstance(line["bbox"], list)
121
+ else ast.literal_eval(line["bbox"])
122
+ )
123
+ # The format of bbox in VenusBench-GD is (x_min, y_min, x_max, y_max)
124
+ image = Image.open(os.path.join(self.img_root, line["image_path"]))
125
+ img_size = image.size
126
+
127
+ # Absolute to relative
128
+ bbox = [
129
+ bbox[0] / img_size[0],
130
+ bbox[1] / img_size[1],
131
+ bbox[2] / img_size[0],
132
+ bbox[3] / img_size[1],
133
+ ]
134
+
135
+ key = line["category"] + ":" + line['ui_type']
136
+ prediction = str(line["prediction"])
137
+ try:
138
+ click_point = self.parse_response_func(prediction)
139
+ if click_point[0] > 1 or click_point[1] > 1:
140
+ click_point = (click_point[0] / img_size[0], click_point[1] / img_size[1])
141
+
142
+ match = (bbox[0] <= click_point[0] <= bbox[2]) and \
143
+ (bbox[1] <= click_point[1] <= bbox[3])
144
+
145
+ if match:
146
+ stats[key].append(1)
147
+ else:
148
+ stats[key].append(0)
149
+ is_wrong_format = False
150
+ except Exception as e:
151
+ logger.warning(f"exception in venusbench eval:{e}")
152
+ stats[key].append(-1)
153
+ match, is_wrong_format, click_point = False, True, None
154
+
155
+ result.append(
156
+ {
157
+ "img_path": os.path.join(self.img_root, line["image_path"]),
158
+ "text": line["question"],
159
+ "bbox": line["bbox"],
160
+ "parsed_bbox": bbox,
161
+ "type": line["ui_type"],
162
+ "category": line["category"],
163
+ "match": match,
164
+ "is_wrong_format": is_wrong_format,
165
+ "pred": click_point,
166
+ }
167
+ )
168
+
169
+ final_score_dict = {}
170
+ final_score_dict.update({k + ':cnt': len(stats[k]) for k in stats})
171
+
172
+ full_stats = []
173
+ for v in stats.values():
174
+ full_stats.extend(v)
175
+ final_score_dict['Overall_Accuracy'] = np.mean([x > 0 for x in full_stats]) * 100
176
+ final_score_dict['Format_Err_Rate'] = np.mean([x < 0 for x in full_stats]) * 100
177
+
178
+ cates = list(set([line["category"] for line in lines]))
179
+ for c in cates:
180
+ sub_stats = [v for k, v in stats.items() if k.split(":")[0] == c for x in v]
181
+ if len(sub_stats) > 0:
182
+ final_score_dict[c + '_Accuracy'] = np.mean([x[0] > 0 for x in [sub_stats]]) * 100
183
+
184
+ score_pth = get_intermediate_file_path(eval_file, '_score', 'json')
185
+ dump(final_score_dict, score_pth)
186
+ return final_score_dict
reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/OmniDocBench/__init__.py ADDED
File without changes
reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/OmniDocBench/data_preprocess.py ADDED
@@ -0,0 +1,449 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import html
2
+ import os
3
+ import re
4
+ import shutil
5
+ import subprocess
6
+ import unicodedata
7
+ import uuid
8
+
9
+ from bs4 import BeautifulSoup
10
+ from pylatexenc.latex2text import LatexNodes2Text
11
+
12
+
13
+ def remove_markdown_fences(content):
14
+ content = re.sub(r'^```markdown\n?', '', content, flags=re.MULTILINE)
15
+ content = re.sub(r'```\n?$', '', content, flags=re.MULTILINE)
16
+ return content
17
+
18
+ # Standardize all consecutive characters
19
+ def replace_repeated_chars(input_str):
20
+ input_str = re.sub(r'_{4,}', '____', input_str) # Replace more than 4 consecutive underscores with 4 underscores
21
+ input_str = re.sub(r' {4,}', ' ', input_str) # Replace more than 4 consecutive spaces with 4 spaces
22
+ return re.sub(r'([^a-zA-Z0-9])\1{10,}', r'\1\1\1\1', input_str) # For other consecutive symbols (except numbers and letters), replace more than 10 occurrences with 4
23
+
24
+ # Special Unicode handling
25
+ def fullwidth_to_halfwidth(s):
26
+ result = []
27
+ for char in s:
28
+ code = ord(char)
29
+ # Convert full-width space to half-width space
30
+ if code == 0x3000:
31
+ code = 0x0020
32
+ # Convert other full-width characters to half-width
33
+ elif 0xFF01 <= code <= 0xFF5E:
34
+ code -= 0xFEE0
35
+ result.append(chr(code))
36
+ return ''.join(result)
37
+
38
+ def find_special_unicode(s):
39
+ special_chars = {}
40
+ for char in s:
41
+ if ord(char) > 127: # Non-ASCII characters
42
+ # unicode_name = unicodedata.name(char, None)
43
+ unicode_name = unicodedata.category(char)
44
+ special_chars[char] = f'U+{ord(char):04X} ({unicode_name})'
45
+ return special_chars
46
+
47
+ # # Define dictionary for Unicode character replacements
48
+ # unicode_replacements = {
49
+ # "\u00A9": r"$\copyright$", # Copyright symbol © to latex
50
+ # "\u00AE": r"$^\circledR$", # Registered trademark ® to latex
51
+ # "\u2122": r"$^\text{TM}$", # Trademark ™ to latex
52
+ # "\u2018": "'", # Left single quote to straight quote
53
+ # "\u2019": "'", # Right single quote to straight quote
54
+ # "\u201C": "\"", # Left double quote to straight quote
55
+ # "\u201D": "\"", # Right double quote to straight quote
56
+ # "\u2013": "-", # En dash to hyphen
57
+ # "\u2014": "-", # Em dash to hyphen
58
+ # "\u2026": "...", # Unicode ellipsis to three dots
59
+ # "\u2103": r"$\textdegree C$", # ℃
60
+ # "\u03B1": r"$\alpha$", # α
61
+ # "\u03B2": r"$\beta$", # β
62
+ # "\u03A3": r"$\Sigma$", # Σ
63
+ # }
64
+
65
+ # # Use regex to replace Unicode characters
66
+ # def replace_unicode(match):
67
+ # char = match.group(0)
68
+ # return unicode_replacements.get(char, char)
69
+
70
+ inline_reg = re.compile(
71
+ r'\$(.*?)\$|'
72
+ r'\\\((.*?)\\\)',
73
+ )
74
+
75
+ def textblock2unicode(text):
76
+ inline_matches = inline_reg.finditer(text)
77
+ removal_positions = []
78
+ for match in inline_matches:
79
+ position = [match.start(), match.end()]
80
+ content = match.group(1) if match.group(1) is not None else match.group(2)
81
+ # print('-------- content-------', content)
82
+ # Remove escape characters \
83
+ clean_content = re.sub(r'\\([\\_&%^])', '', content)
84
+
85
+ try:
86
+ if any(char in clean_content for char in r'\^_'):
87
+ if clean_content.endswith('\\'):
88
+ clean_content += ' '
89
+ # inline_array.append(match.group(0))
90
+ unicode_content = LatexNodes2Text().latex_to_text(clean_content)
91
+ removal_positions.append((position[0], position[1], unicode_content))
92
+ except:
93
+ continue
94
+
95
+ # Remove inline formulas from original text
96
+ for start, end, unicode_content in sorted(removal_positions, reverse=True):
97
+ text = text[:start] + unicode_content.strip() + text[end:]
98
+
99
+ return text
100
+
101
+ def normalized_formula(text):
102
+ # Normalize math formulas before matching
103
+ filter_list = ['\\mathbf', '\\mathrm', '\\mathnormal', '\\mathit', '\\mathbb', '\\mathcal', '\\mathscr', '\\mathfrak', '\\mathsf', '\\mathtt',
104
+ '\\textbf', '\\text', '\\boldmath', '\\boldsymbol', '\\operatorname', '\\bm',
105
+ '\\symbfit', '\\mathbfcal', '\\symbf', '\\scriptscriptstyle', '\\notag',
106
+ '\\setlength', '\\coloneqq', '\\space', '\\thickspace', '\\thinspace', '\\medspace', '\\nobreakspace', '\\negmedspace',
107
+ '\\quad', '\\qquad', '\\enspace', '\\substackw', ' ']
108
+ # '\\left', '\\right', '{', '}', ' ']
109
+
110
+ # delimiter_filter
111
+ pattern = re.compile(r"\\\[(.+?)(?<!\\)\\\]")
112
+ match = pattern.search(text)
113
+
114
+ if match:
115
+ text = match.group(1).strip()
116
+
117
+ tag_pattern = re.compile(r"\\tag\{.*?\}")
118
+ text = tag_pattern.sub('', text)
119
+ hspace_pattern = re.compile(r"\\hspace\{.*?\}")
120
+ text = hspace_pattern.sub('', text)
121
+ begin_pattern = re.compile(r"\\begin\{.*?\}")
122
+ text = begin_pattern.sub('', text)
123
+ end_pattern = re.compile(r"\\end\{.*?\}")
124
+ text = end_pattern.sub('', text)
125
+ col_sep = re.compile(r"\\arraycolsep.*?\}")
126
+ text = col_sep.sub('', text)
127
+ text = text.strip('.')
128
+
129
+ for filter_text in filter_list:
130
+ text = text.replace(filter_text, '')
131
+
132
+ # text = normalize_text(delimiter_filter(text))
133
+ # text = delimiter_filter(text)
134
+ text = text.lower()
135
+ return text
136
+
137
+ def normalized_html_table(text):
138
+ def process_table_html(md_i):
139
+ """
140
+ pred_md format edit
141
+ """
142
+ def process_table_html(html_content):
143
+ soup = BeautifulSoup(html_content, 'html.parser')
144
+ th_tags = soup.find_all('th')
145
+ for th in th_tags:
146
+ th.name = 'td'
147
+ thead_tags = soup.find_all('thead')
148
+ for thead in thead_tags:
149
+ thead.unwrap() # unwrap()会移除标签但保留其内容
150
+ math_tags = soup.find_all('math')
151
+ for math_tag in math_tags:
152
+ alttext = math_tag.get('alttext', '')
153
+ alttext = f'${alttext}$'
154
+ if alttext:
155
+ math_tag.replace_with(alttext)
156
+ span_tags = soup.find_all('span')
157
+ for span in span_tags:
158
+ span.unwrap()
159
+ return str(soup)
160
+
161
+ table_res=''
162
+ table_res_no_space=''
163
+ if '<table' in md_i.replace(" ","").replace("'",'"'):
164
+ md_i = process_table_html(md_i)
165
+ table_res = html.unescape(md_i).replace('\n', '')
166
+ table_res = unicodedata.normalize('NFKC', table_res).strip()
167
+ pattern = r'<table\b[^>]*>(.*)</table>'
168
+ tables = re.findall(pattern, table_res, re.DOTALL | re.IGNORECASE)
169
+ table_res = ''.join(tables)
170
+ # table_res = re.sub('<table.*?>','',table_res)
171
+ table_res = re.sub('( style=".*?")', "", table_res)
172
+ table_res = re.sub('( height=".*?")', "", table_res)
173
+ table_res = re.sub('( width=".*?")', "", table_res)
174
+ table_res = re.sub('( align=".*?")', "", table_res)
175
+ table_res = re.sub('( class=".*?")', "", table_res)
176
+ table_res = re.sub('</?tbody>',"",table_res)
177
+
178
+ table_res = re.sub(r'\s+', " ", table_res)
179
+ table_res_no_space = '<html><body><table border="1" >' + table_res.replace(' ','') + '</table></body></html>'
180
+ # table_res_no_space = re.sub(' (style=".*?")',"",table_res_no_space)
181
+ # table_res_no_space = re.sub(r'[ ]', " ", table_res_no_space)
182
+ table_res_no_space = re.sub('colspan="', ' colspan="', table_res_no_space)
183
+ table_res_no_space = re.sub('rowspan="', ' rowspan="', table_res_no_space)
184
+ table_res_no_space = re.sub('border="', ' border="', table_res_no_space)
185
+
186
+ table_res = '<html><body><table border="1" >' + table_res + '</table></body></html>'
187
+ # table_flow.append(table_res)
188
+ # table_flow_no_space.append(table_res_no_space)
189
+
190
+ return table_res, table_res_no_space
191
+
192
+ def clean_table(input_str,flag=True):
193
+ if flag:
194
+ input_str = input_str.replace('<sup>', '').replace('</sup>', '')
195
+ input_str = input_str.replace('<sub>', '').replace('</sub>', '')
196
+ input_str = input_str.replace('<span>', '').replace('</span>', '')
197
+ input_str = input_str.replace('<div>', '').replace('</div>', '')
198
+ input_str = input_str.replace('<p>', '').replace('</p>', '')
199
+ input_str = input_str.replace('<spandata-span-identity="">', '')
200
+ input_str = re.sub('<colgroup>.*?</colgroup>','',input_str)
201
+ return input_str
202
+
203
+ norm_text, _ = process_table_html(text)
204
+ norm_text = clean_table(norm_text)
205
+ return norm_text
206
+
207
+ def normalized_latex_table(text):
208
+ def latex_template(latex_code):
209
+ template = r'''
210
+ \documentclass[border=20pt]{article}
211
+ \usepackage{subcaption}
212
+ \usepackage{url}
213
+ \usepackage{graphicx}
214
+ \usepackage{caption}
215
+ \usepackage{multirow}
216
+ \usepackage{booktabs}
217
+ \usepackage{color}
218
+ \usepackage{colortbl}
219
+ \usepackage{xcolor,soul,framed}
220
+ \usepackage{fontspec}
221
+ \usepackage{amsmath,amssymb,mathtools,bm,mathrsfs,textcomp}
222
+ \setlength{\parindent}{0pt}''' + \
223
+ r'''
224
+ \begin{document}
225
+ ''' + \
226
+ latex_code + \
227
+ r'''
228
+ \end{document}'''
229
+
230
+ return template
231
+
232
+ def process_table_latex(latex_code):
233
+ SPECIAL_STRINGS= [
234
+ ['\\\\vspace\\{.*?\\}', ''],
235
+ ['\\\\hspace\\{.*?\\}', ''],
236
+ ['\\\\rule\{.*?\\}\\{.*?\\}', ''],
237
+ ['\\\\addlinespace\\[.*?\\]', ''],
238
+ ['\\\\addlinespace', ''],
239
+ ['\\\\renewcommand\\{\\\\arraystretch\\}\\{.*?\\}', ''],
240
+ ['\\\\arraystretch\\{.*?\\}', ''],
241
+ ['\\\\(row|column)?colors?\\{[^}]*\\}(\\{[^}]*\\}){0,2}', ''],
242
+ ['\\\\color\\{.*?\\}', ''],
243
+ ['\\\\textcolor\\{.*?\\}', ''],
244
+ ['\\\\rowcolor(\\[.*?\\])?\\{.*?\\}', ''],
245
+ ['\\\\columncolor(\\[.*?\\])?\\{.*?\\}', ''],
246
+ ['\\\\cellcolor(\\[.*?\\])?\\{.*?\\}', ''],
247
+ ['\\\\colorbox\\{.*?\\}', ''],
248
+ ['\\\\(tiny|scriptsize|footnotesize|small|normalsize|large|Large|LARGE|huge|Huge)', ''],
249
+ [r'\s+', ' '],
250
+ ['\\\\centering', ''],
251
+ ['\\\\begin\\{table\\}\\[.*?\\]', '\\\\begin{table}'],
252
+ ['\t', ''],
253
+ ['@{}', ''],
254
+ ['\\\\toprule(\\[.*?\\])?', '\\\\hline'],
255
+ ['\\\\bottomrule(\\[.*?\\])?', '\\\\hline'],
256
+ ['\\\\midrule(\\[.*?\\])?', '\\\\hline'],
257
+ ['p\\{[^}]*\\}', 'l'],
258
+ ['m\\{[^}]*\\}', 'c'],
259
+ ['\\\\scalebox\\{[^}]*\\}\\{([^}]*)\\}', '\\1'],
260
+ ['\\\\textbf\\{([^}]*)\\}', '\\1'],
261
+ ['\\\\textit\\{([^}]*)\\}', '\\1'],
262
+ ['\\\\cmidrule(\\[.*?\\])?\\(.*?\\)\\{([0-9]-[0-9])\\}', '\\\\cline{\\2}'],
263
+ ['\\\\hline', ''],
264
+ [r'\\multicolumn\{1\}\{[^}]*\}\{((?:[^{}]|(?:\{[^{}]*\}))*)\}', r'\1']
265
+ ]
266
+ pattern = r'\\begin\{tabular\}.*\\end\{tabular\}' # 注意这里不用 .*?
267
+ matches = re.findall(pattern, latex_code, re.DOTALL)
268
+ latex_code = ' '.join(matches)
269
+
270
+ for special_str in SPECIAL_STRINGS:
271
+ latex_code = re.sub(fr'{special_str[0]}', fr'{special_str[1]}', latex_code)
272
+
273
+ return latex_code
274
+
275
+ def convert_latex_to_html(latex_content, cache_dir='./temp'):
276
+ if not os.path.exists(cache_dir):
277
+ os.makedirs(cache_dir)
278
+
279
+ uuid_str = str(uuid.uuid1())
280
+ with open(f'{cache_dir}/{uuid_str}.tex', 'w') as f:
281
+ f.write(latex_template(latex_content))
282
+
283
+ cmd = ['latexmlc', '--quiet', '--nocomments', f'--log={cache_dir}/{uuid_str}.log',
284
+ f'{cache_dir}/{uuid_str}.tex', f'--dest={cache_dir}/{uuid_str}.html']
285
+ try:
286
+ subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
287
+ with open(f'{cache_dir}/{uuid_str}.html', 'r') as f:
288
+ html_content = f.read()
289
+
290
+ pattern = r'<table\b[^>]*>(.*)</table>'
291
+ tables = re.findall(pattern, html_content, re.DOTALL | re.IGNORECASE)
292
+ tables = [f'<table>{table}</table>' for table in tables]
293
+ html_content = '\n'.join(tables)
294
+
295
+ except Exception as e:
296
+ html_content = ''
297
+
298
+ shutil.rmtree(cache_dir)
299
+ return html_content
300
+
301
+ html_text = convert_latex_to_html(text)
302
+ normlized_tables = normalized_html_table(html_text)
303
+ return normlized_tables
304
+
305
+
306
+ def normalized_table(text, format='html'):
307
+ if format not in ['html', 'latex']:
308
+ raise ValueError('Invalid format: {}'.format(format))
309
+ else:
310
+ return globals()['normalized_{}_table'.format(format)](text)
311
+
312
+
313
+ def textblock_with_norm_formula(text):
314
+ inline_matches = inline_reg.finditer(text)
315
+ removal_positions = []
316
+ for match in inline_matches:
317
+ position = [match.start(), match.end()]
318
+ content = match.group(1) if match.group(1) is not None else match.group(2)
319
+ # print('-------- content-------', content)
320
+
321
+ norm_content = normalized_formula(content)
322
+ removal_positions.append((position[0], position[1], norm_content))
323
+
324
+ # Remove inline formulas from original text
325
+ for start, end, norm_content in sorted(removal_positions, reverse=True):
326
+ text = text[:start] + norm_content.strip() + text[end:]
327
+
328
+ return text
329
+
330
+ # def inline_filter_unicode(text):
331
+ # # Ensure text is string type
332
+ # if not isinstance(text, str):
333
+ # text = str(text)
334
+
335
+ # # Convert LaTeX content to Unicode representation
336
+ # text = LatexNodes2Text().latex_to_text(text)
337
+
338
+ # inline_array = []
339
+ # inline_matches = inline_reg.finditer(text)
340
+
341
+ # for match in inline_matches:
342
+ # position = [match.start(), match.end()]
343
+ # content = match.group(1) if match.group(1) is not None else match.group(2)
344
+
345
+ # # Remove escape characters \
346
+ # clean_content = re.sub(r'\\([\\_&%^])', '', content)
347
+
348
+ # if any(char in clean_content for char in r'\^_'):
349
+ # # inline_array.append(match.group(0))
350
+ # inline_array.append({
351
+ # 'category_type': 'equation_inline',
352
+ # 'position': position,
353
+ # 'content': match.group(0),
354
+ # })
355
+ # text = text.replace(match.group(0), '')
356
+ # # print('-----Found inline formula: ', match.group(0))
357
+ # else:
358
+ # text = text.replace(match.group(0), content)
359
+ # # # Add to inline_array
360
+ # # inline_array.append({
361
+ # # 'category_type': 'equation_inline',
362
+ # # 'position': position,
363
+ # # 'content': content,
364
+ # # })
365
+
366
+ # # # Remove matched formula from original text, can choose to replace with spaces or remove directly
367
+ # # text = text[:position[0]] + ' '*(position[1]-position[0]) + text[position[1]:]
368
+
369
+ # return text, inline_array
370
+
371
+ def inline_filter_unicode(text):
372
+ # Ensure text is string type
373
+ if not isinstance(text, str):
374
+ text = str(text)
375
+
376
+ # Replace inline formula boundary markers
377
+ #print('--------text-------',text)
378
+ placeholder = '__INLINE_FORMULA_BOUNDARY__'
379
+ text_copy = text.replace('$', placeholder).replace('\\(', placeholder).replace('\\)', placeholder)
380
+ #print('--------text_copy-------',text_copy)
381
+ # Convert LaTeX content to Unicode representation
382
+ text_copy = LatexNodes2Text().latex_to_text(text_copy)
383
+ #print('--------text_copy---unicode----',text_copy)
384
+ # Restore boundary markers
385
+ text_copy = text_copy.replace(placeholder, '$')
386
+
387
+ inline_array = []
388
+ inline_matches = inline_reg.finditer(text_copy)
389
+ # Record positions of inline formulas to be removed
390
+ removal_positions = []
391
+
392
+ for match in inline_matches:
393
+ position = [match.start(), match.end()]
394
+ content = match.group(1) if match.group(1) is not None else match.group(2)
395
+ print('-------- content-------', content)
396
+ # Remove escape characters \
397
+ clean_content = re.sub(r'\\([\\_&%^])', '', content)
398
+
399
+ if any(char in clean_content for char in r'\^_'):
400
+ # inline_array.append(match.group(0))
401
+ inline_array.append({
402
+ 'category_type': 'equation_inline',
403
+ 'position': position,
404
+ 'content': content,
405
+ })
406
+ removal_positions.append((position[0], position[1]))
407
+
408
+ # Remove inline formulas from original text
409
+ for start, end in sorted(removal_positions, reverse=True):
410
+ text = text[:start] + text[end:]
411
+
412
+ return text, inline_array
413
+
414
+ def inline_filter(text):
415
+ # Ensure text is string type
416
+ if not isinstance(text, str):
417
+ text = str(text)
418
+
419
+ inline_array = []
420
+ inline_matches = inline_reg.finditer(text)
421
+
422
+ for match in inline_matches:
423
+ position = [match.start(), match.end()]
424
+ content = match.group(1) if match.group(1) is not None else match.group(2)
425
+ # print('inline_content: ', content)
426
+
427
+ # Remove escape characters \
428
+ clean_content = re.sub(r'\\([\\_&%^])', '', content)
429
+
430
+ if any(char in clean_content for char in r'\^_'):
431
+ # inline_array.append(match.group(0))
432
+ inline_array.append({
433
+ 'category_type': 'equation_inline',
434
+ 'position': position,
435
+ 'content': match.group(0),
436
+ })
437
+ text = text.replace(match.group(0), '')
438
+ # print('-----Found inline formula: ', match.group(0))
439
+ else:
440
+ text = text.replace(match.group(0), content)
441
+
442
+ return text, inline_array
443
+
444
+ # Text OCR quality check processing:
445
+ def clean_string(input_string):
446
+ # Use regex to keep Chinese characters, English letters and numbers
447
+ input_string = input_string.replace('\\t', '').replace('\\n', '').replace('\t', '').replace('\n', '').replace('/t', '').replace('/n', '')
448
+ cleaned_string = re.sub(r'[^\w\u4e00-\u9fff]', '', input_string)
449
+ return cleaned_string
reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/OmniDocBench/metrics.py ADDED
@@ -0,0 +1,486 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import copy
2
+ import json
3
+ import pdb
4
+ import random
5
+ import time
6
+ from collections import defaultdict, deque
7
+
8
+ import evaluate
9
+ import Levenshtein
10
+ import pandas as pd
11
+ from apted import APTED, Config
12
+ from apted.helpers import Tree
13
+ from lxml import etree, html
14
+ from tabulate import tabulate
15
+ from tqdm import tqdm
16
+
17
+ from .utils import normalized_table, save_paired_result
18
+
19
+
20
+ def show_result(results):
21
+ for metric_name in results.keys():
22
+ print(f'{metric_name}:')
23
+ score_table = [[k,v] for k,v in results[metric_name].items()]
24
+ print(tabulate(score_table))
25
+ print('='*100)
26
+
27
+ def sort_nested_dict(d):
28
+ # If it's a dictionary, recursively sort it
29
+ if isinstance(d, dict):
30
+ # Sort the current dictionary
31
+ sorted_dict = {k: sort_nested_dict(v) for k, v in sorted(d.items())}
32
+ return sorted_dict
33
+ # If not a dictionary, return directly
34
+ return d
35
+
36
+ def get_full_labels_results(samples:dict):
37
+ if not samples:
38
+ return {}
39
+ label_group_dict = defaultdict(lambda: defaultdict(list))
40
+ for sample in samples:
41
+ label_list = []
42
+ if not sample.get("gt_attribute"):
43
+ continue
44
+ for anno in sample["gt_attribute"]:
45
+ for k,v in anno.items():
46
+ label_list.append(k+": "+str(v))
47
+ for label_name in list(set(label_list)): # Currently if there are merged cases, calculate based on the set of all labels involved after merging
48
+ for metric, score in sample['metric'].items():
49
+ label_group_dict[label_name][metric].append(score)
50
+
51
+ print('----Anno Attribute---------------')
52
+ result = {}
53
+ result['sample_count'] = {}
54
+ for attribute in label_group_dict.keys():
55
+ for metric, scores in label_group_dict[attribute].items():
56
+ mean_score = sum(scores) / len(scores)
57
+ if not result.get(metric):
58
+ result[metric] = {}
59
+ result[metric][attribute] = mean_score
60
+ result['sample_count'][attribute] = len(scores)
61
+ result = sort_nested_dict(result)
62
+ show_result(result)
63
+ return result
64
+
65
+
66
+ def get_page_split(samples, page_info): # Page level metric
67
+ if not page_info:
68
+ return {}
69
+ result_list = defaultdict(list)
70
+
71
+
72
+ for sample in samples:
73
+ img_name = sample['img_id'] if sample['img_id'].endswith('.jpg') else '_'.join(sample['img_id'].split('_')[:-1])
74
+ page_info_s = page_info[img_name]
75
+ if not sample.get('metric'):
76
+ continue
77
+ for metric, score in sample['metric'].items():
78
+ gt = sample['norm_gt'] if sample.get('norm_gt') else sample['gt']
79
+ pred = sample['norm_pred'] if sample.get('norm_pred') else sample['pred']
80
+ result_list[metric].append({
81
+ 'image_name': img_name,
82
+ 'metric': metric,
83
+ 'attribute': 'ALL',
84
+ 'score': score,
85
+ 'upper_len': max(len(gt), len(pred))
86
+ })
87
+ for k,v in page_info_s.items():
88
+ if isinstance(v, list): # special issue
89
+ for special_issue in v:
90
+ if 'table' not in special_issue: # Table-related special fields have duplicates
91
+ result_list[metric].append({
92
+ 'image_name': img_name,
93
+ 'metric': metric,
94
+ 'attribute': special_issue,
95
+ 'score': score,
96
+ 'upper_len': max(len(gt), len(pred))
97
+ })
98
+ else:
99
+ result_list[metric].append({
100
+ 'image_name': img_name,
101
+ 'metric': metric,
102
+ 'attribute': k+": "+str(v),
103
+ 'score': score,
104
+ 'upper_len': max(len(gt), len(pred))
105
+ })
106
+
107
+ # Page level logic, accumulation is only done within pages, and mean operation is performed between pages
108
+ result = {}
109
+ if result_list.get('Edit_dist'):
110
+ df = pd.DataFrame(result_list['Edit_dist'])
111
+ up_total_avg = df.groupby(["image_name", "attribute"]).apply(lambda x: (x["score"]*x['upper_len']).sum() / x['upper_len'].sum()).groupby('attribute').mean() # At page level, accumulate edits, denominator is sum of max(gt, pred) from each sample
112
+ result['Edit_dist'] = up_total_avg.to_dict()
113
+ for metric in result_list.keys():
114
+ if metric == 'Edit_dist':
115
+ continue
116
+ df = pd.DataFrame(result_list[metric])
117
+ page_avg = df.groupby(["image_name", "attribute"]).apply(lambda x: x["score"].mean()).groupby('attribute').mean()
118
+ result[metric] = page_avg.to_dict()
119
+
120
+ result = sort_nested_dict(result)
121
+ # print('----Page Attribute---------------')
122
+ show_result(result)
123
+ return result
124
+
125
+
126
+ def get_groups(samples, group_info):
127
+ group_samples = defaultdict(list)
128
+ for sample in samples:
129
+ group_samples['all'].append(sample)
130
+ for group in group_info:
131
+ select_flag = True
132
+ for k, v in group.items():
133
+ for gt_attribute in sample['gt_attribute']: # gt_attribute is a list containing all merged gt attributes
134
+ if not gt_attribute: # if no GT attributes, don't include in calculation
135
+ select_flag = False
136
+ elif gt_attribute[k] != v: # if any gt attribute doesn't meet criteria, don't select
137
+ select_flag = False
138
+ if select_flag:
139
+ group_samples[str(group)].append(sample)
140
+ return group_samples
141
+
142
+
143
+ class Registry:
144
+ def __init__(self):
145
+ self._registry = {}
146
+ def register(self, name):
147
+ def decorator(item):
148
+ if name in self._registry:
149
+ raise ValueError(f"Item {name} already registered.")
150
+ self._registry[name] = item
151
+ return item
152
+ return decorator
153
+ def get(self, name):
154
+ if name not in self._registry:
155
+ raise ValueError(f"Item {name} not found in registry.")
156
+ return self._registry[name]
157
+ def list_items(self):
158
+ return list(self._registry.keys())
159
+
160
+ METRIC_REGISTRY = Registry()
161
+
162
+
163
+ @METRIC_REGISTRY.register("TEDS")
164
+ class call_TEDS():
165
+ def __init__(self, samples):
166
+ self.samples = samples
167
+ def evaluate(self, group_info=[], save_name='default'):
168
+ teds = TEDS(structure_only=False)
169
+ teds_structure_only = TEDS(structure_only=True)
170
+
171
+ group_scores = defaultdict(list)
172
+ group_scores_structure_only = defaultdict(list)
173
+
174
+ samples = self.samples
175
+ for sample in samples:
176
+ gt = sample['norm_gt'] if sample.get('norm_gt') else sample['gt']
177
+ pred = sample['norm_pred'] if sample.get('norm_pred') else sample['pred']
178
+
179
+ score = teds.evaluate(pred, gt)
180
+ score_structure_only = teds_structure_only.evaluate(pred, gt)
181
+ # print('TEDS score:', score)
182
+ group_scores['all'].append(score)
183
+ group_scores_structure_only['all'].append(score_structure_only)
184
+
185
+ if not sample.get('metric'):
186
+ sample['metric'] = {}
187
+ sample['metric']['TEDS'] = score
188
+ sample['metric']['TEDS_structure_only'] = score_structure_only
189
+
190
+ for group in group_info:
191
+ select_flag = True
192
+ for k, v in group.items():
193
+ for gt_attribute in sample['gt_attribute']: # gt_attribute is a list containing all merged gt attributes
194
+ if not gt_attribute: # if no GT attributes, don't include in calculation
195
+ select_flag = False
196
+ elif gt_attribute[k] != v: # if any gt attribute doesn't meet criteria, don't select
197
+ select_flag = False
198
+ if select_flag:
199
+ group_scores[str(group)].append(score)
200
+
201
+ result = {}
202
+ for group_name, scores in group_scores.items():
203
+ if len(scores) > 0:
204
+ result[group_name] = sum(scores) / len(scores) # average of normalized scores at sample level
205
+ else:
206
+ result[group_name] = 'NaN'
207
+ print(f'Warning: Empyty matched samples for {group_name}.')
208
+
209
+ structure_only_result = {}
210
+ for group_name, scores in group_scores_structure_only.items():
211
+ if len(scores) > 0:
212
+ structure_only_result[group_name] = sum(scores) / len(scores) # average of normalized scores at sample level
213
+ else:
214
+ structure_only_result[group_name] = 'NaN'
215
+ print(f'Warning: Empyty matched samples for {group_name}.')
216
+
217
+ return samples,{'TEDS': result, 'TEDS_structure_only': structure_only_result}
218
+
219
+
220
+ @METRIC_REGISTRY.register("BLEU")
221
+ class call_BLEU():
222
+ def __init__(self, samples):
223
+ self.samples = samples
224
+ def evaluate(self, group_info=[], save_name='default'):
225
+ group_samples = get_groups(self.samples, group_info)
226
+ result = {}
227
+ bleu = evaluate.load("bleu", keep_in_memory=True, experiment_id=random.randint(1,1e8))
228
+
229
+ for group_name, samples in group_samples.items():
230
+ predictions, references = [], []
231
+ for sample in samples:
232
+ gt = sample['norm_gt'] if sample.get('norm_gt') else sample['gt']
233
+ pred = sample['norm_pred'] if sample.get('norm_pred') else sample['pred']
234
+ predictions.append(pred)
235
+ references.append(gt)
236
+
237
+ if not predictions or not any(predictions) or not references or not any(references):
238
+ bleu_score = 0
239
+ else:
240
+ try:
241
+ bleu_results = bleu.compute(predictions=predictions, references=references)
242
+ bleu_score = bleu_results["bleu"]
243
+ except ZeroDivisionError:
244
+ bleu_score = 0
245
+
246
+ result[group_name] = bleu_score
247
+
248
+ return self.samples,{'BLEU': result}
249
+
250
+ @METRIC_REGISTRY.register("METEOR")
251
+ class call_METEOR():
252
+ def __init__(self, samples):
253
+ self.samples = samples
254
+ def evaluate(self, group_info=[], save_name='default'):
255
+ group_samples = get_groups(self.samples, group_info)
256
+ result = {}
257
+ for group_name, samples in group_samples.items():
258
+ predictions, references = [], []
259
+ for sample in samples:
260
+ gt = sample['norm_gt'] if sample.get('norm_gt') else sample['gt']
261
+ pred = sample['norm_pred'] if sample.get('norm_pred') else sample['pred']
262
+ predictions.append(gt)
263
+ references.append(pred)
264
+ meteor = evaluate.load('meteor', keep_in_memory=True, experiment_id=random.randint(1,1e8))
265
+ meteor_results = meteor.compute(predictions=predictions, references=references)
266
+ result[group_name] = meteor_results['meteor']
267
+
268
+ return self.samples,{'METEOR': result}
269
+
270
+
271
+ @METRIC_REGISTRY.register("Edit_dist")
272
+ class call_Edit_dist():
273
+ def __init__(self, samples):
274
+ self.samples = samples
275
+ def evaluate(self, group_info=[], save_name='default'):
276
+ samples = self.samples
277
+ for sample in samples:
278
+ img_name = sample['img_id'] if sample['img_id'].endswith('.jpg') else '_'.join(sample['img_id'].split('_')[:-1])
279
+ sample['image_name'] = img_name
280
+ gt = sample['norm_gt'] if sample.get('norm_gt') else sample['gt']
281
+ pred = sample['norm_pred'] if sample.get('norm_pred') else sample['pred']
282
+ upper_len = max(len(pred), len(gt))
283
+ sample['upper_len'] = upper_len
284
+ if len(pred) > 0 or len(gt) > 0:
285
+ edit_dist = Levenshtein.distance(pred, gt)
286
+ if not sample.get('metric'):
287
+ sample['metric'] = {}
288
+ sample['metric']['Edit_dist'] = edit_dist / upper_len
289
+ sample['Edit_num'] = edit_dist
290
+
291
+ if isinstance(samples, list):
292
+ saved_samples = samples
293
+ else:
294
+ saved_samples = samples.samples
295
+
296
+ if not saved_samples:
297
+ return {'Edit_dist': {'ALL_page_avg': 'NaN'}}
298
+
299
+ df = pd.DataFrame(saved_samples)
300
+ up_total_avg = df.groupby("image_name").apply(lambda x: x['Edit_num'].sum() / x['upper_len'].sum()) # page level, sum of edits divided by sum of max(gt,pred) lengths for each sample
301
+ per_img_score = up_total_avg.to_dict()
302
+
303
+ return samples,{'Edit_dist': {'ALL_page_avg': up_total_avg.mean()}}
304
+
305
+
306
+ @METRIC_REGISTRY.register("CDM")
307
+ class call_CDM():
308
+ def __init__(self, samples):
309
+ self.samples = samples
310
+ def evaluate(self, group_info=[], save_name='default'):
311
+ if isinstance(self.samples, list):
312
+ cdm_samples = copy.deepcopy(self.samples)
313
+ else:
314
+ cdm_samples = copy.deepcopy(self.samples.samples)
315
+ for idx, sample in enumerate(cdm_samples):
316
+ sample['img_name'] = sample['img_id']
317
+ sample['img_id'] = str(idx)
318
+ sample['gt'] = sample['gt'].lstrip("$$").rstrip("$$").strip()
319
+ sample['pred'] = sample['pred'].split("```latex")[-1].split("```")[0]
320
+ sample['pred'] = sample['pred'].lstrip("$$").rstrip("$$").strip()
321
+
322
+ return self.samples,False
323
+
324
+
325
+ class TEDS(object):
326
+ ''' Tree Edit Distance basead Similarity
327
+ '''
328
+ def __init__(self, structure_only=False, n_jobs=1, ignore_nodes=None):
329
+ assert isinstance(n_jobs, int) and (n_jobs >= 1), 'n_jobs must be an integer greather than 1'
330
+ self.structure_only = structure_only
331
+ self.n_jobs = n_jobs
332
+ self.ignore_nodes = ignore_nodes
333
+ self.__tokens__ = []
334
+
335
+ def tokenize(self, node):
336
+ ''' Tokenizes table cells
337
+ '''
338
+ self.__tokens__.append('<%s>' % node.tag)
339
+ if node.text is not None:
340
+ self.__tokens__ += list(node.text)
341
+ for n in node.getchildren():
342
+ self.tokenize(n)
343
+ if node.tag != 'unk':
344
+ self.__tokens__.append('</%s>' % node.tag)
345
+ if node.tag != 'td' and node.tail is not None:
346
+ self.__tokens__ += list(node.tail)
347
+
348
+ def load_html_tree(self, node, parent=None):
349
+ ''' Converts HTML tree to the format required by apted
350
+ '''
351
+ global __tokens__
352
+ if node.tag == 'td':
353
+ if self.structure_only:
354
+ cell = []
355
+ else:
356
+ self.__tokens__ = []
357
+ self.tokenize(node)
358
+ cell = self.__tokens__[1:-1].copy()
359
+ new_node = TableTree(node.tag,
360
+ int(node.attrib.get('colspan', '1')),
361
+ int(node.attrib.get('rowspan', '1')),
362
+ cell, *deque())
363
+ else:
364
+ new_node = TableTree(node.tag, None, None, None, *deque())
365
+ if parent is not None:
366
+ parent.children.append(new_node)
367
+ if node.tag != 'td':
368
+ for n in node.getchildren():
369
+ self.load_html_tree(n, new_node)
370
+ if parent is None:
371
+ return new_node
372
+
373
+ def evaluate(self, pred, true):
374
+ ''' Computes TEDS score between the prediction and the ground truth of a
375
+ given sample
376
+ '''
377
+ if (not pred) or (not true):
378
+ return 0.0
379
+ parser = html.HTMLParser(remove_comments=True, encoding='utf-8')
380
+ pred = html.fromstring(pred, parser=parser)
381
+ true = html.fromstring(true, parser=parser)
382
+ if pred.xpath('body/table') and true.xpath('body/table'):
383
+ pred = pred.xpath('body/table')[0]
384
+ true = true.xpath('body/table')[0]
385
+ if self.ignore_nodes:
386
+ etree.strip_tags(pred, *self.ignore_nodes)
387
+ etree.strip_tags(true, *self.ignore_nodes)
388
+ n_nodes_pred = len(pred.xpath(".//*"))
389
+ n_nodes_true = len(true.xpath(".//*"))
390
+ n_nodes = max(n_nodes_pred, n_nodes_true)
391
+ tree_pred = self.load_html_tree(pred)
392
+ tree_true = self.load_html_tree(true)
393
+ distance = APTED(tree_pred, tree_true, CustomConfig()).compute_edit_distance()
394
+ return 1.0 - (float(distance) / n_nodes)
395
+ else:
396
+ return 0.0
397
+
398
+ def batch_evaluate(self, pred_json, true_json):
399
+ ''' Computes TEDS score between the prediction and the ground truth of
400
+ a batch of samples
401
+ @params pred_json: {'FILENAME': 'HTML CODE', ...}
402
+ @params true_json: {'FILENAME': {'html': 'HTML CODE'}, ...}
403
+ @output: {'FILENAME': 'TEDS SCORE', ...}
404
+ '''
405
+ samples = true_json.keys()
406
+ # if self.n_jobs == 1:
407
+ scores = [self.evaluate(pred_json.get(filename, ''), true_json[filename]['html']) for filename in tqdm(samples)]
408
+ # else:
409
+ # inputs = [{'pred': pred_json.get(filename, ''), 'true': true_json[filename]['html']} for filename in samples]
410
+ # scores = parallel_process(inputs, self.evaluate, use_kwargs=True, n_jobs=self.n_jobs, front_num=1)
411
+ scores = dict(zip(samples, scores))
412
+ return scores
413
+
414
+
415
+ class CustomConfig(Config):
416
+ @staticmethod
417
+ def maximum(*sequences):
418
+ """Get maximum possible value
419
+ """
420
+ return max(map(len, sequences))
421
+
422
+ def normalized_distance(self, *sequences):
423
+ """Get distance from 0 to 1
424
+ """
425
+ return float(Levenshtein.distance(*sequences)) / self.maximum(*sequences)
426
+
427
+ def rename(self, node1, node2):
428
+ """Compares attributes of trees"""
429
+ if (node1.tag != node2.tag) or (node1.colspan != node2.colspan) or (node1.rowspan != node2.rowspan):
430
+ return 1.
431
+ if node1.tag == 'td':
432
+ if node1.content or node2.content:
433
+ return self.normalized_distance(node1.content, node2.content)
434
+ return 0.
435
+
436
+
437
+ class TableTree(Tree):
438
+ def __init__(self, tag, colspan=None, rowspan=None, content=None, *children):
439
+ self.tag = tag
440
+ self.colspan = colspan
441
+ self.rowspan = rowspan
442
+ self.content = content
443
+ self.children = list(children)
444
+
445
+ def bracket(self):
446
+ """Show tree using brackets notation"""
447
+ if self.tag == 'td':
448
+ result = '"tag": %s, "colspan": %d, "rowspan": %d, "text": %s' % \
449
+ (self.tag, self.colspan, self.rowspan, self.content)
450
+ else:
451
+ result = '"tag": %s' % self.tag
452
+ for child in self.children:
453
+ result += child.bracket()
454
+ return "{{{}}}".format(result)
455
+
456
+
457
+ class recogition_end2end_base_dataset():
458
+ def __init__(self, samples):
459
+ img_id = 0
460
+ for sample in samples:
461
+ if not sample.get('img_id'):
462
+ sample['img_id'] = img_id
463
+ img_id += 1
464
+ self.samples = samples
465
+ def __getitem__(self, idx):
466
+ return self.samples[idx]
467
+
468
+
469
+ class recogition_end2end_table_dataset(recogition_end2end_base_dataset):
470
+ def __init__(self, samples, table_format):
471
+ self.pred_table_format = table_format
472
+ self.samples = self.normalize_data(samples)
473
+
474
+ def normalize_data(self, samples):
475
+ img_id = 0
476
+ for sample in samples:
477
+ p = sample['pred']
478
+ r = sample['gt']
479
+ p = normalized_table(p, self.pred_table_format)
480
+ r = normalized_table(r)
481
+ sample['norm_gt'] = r
482
+ sample['norm_pred'] = p
483
+ sample['img_id'] = sample['img_id'] if sample.get('img_id') else img_id
484
+ img_id += 1
485
+
486
+ return samples
reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/OmniDocBench/omnidocbench.py ADDED
@@ -0,0 +1,557 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import copy
3
+ import json
4
+ import os
5
+ import tempfile
6
+
7
+ import numpy as np
8
+ import pandas as pd
9
+ import torch.distributed as dist
10
+ from tqdm import tqdm
11
+
12
+ from vlmeval.smp import dump, get_intermediate_file_path, load
13
+ from ..image_base import ImageBaseDataset
14
+
15
+ # from ..utils import get_intermediate_file_path, load, dump
16
+
17
+
18
+ class OmniDocBench(ImageBaseDataset):
19
+
20
+ MODALITY = 'IMAGE'
21
+ TYPE = 'QA'
22
+
23
+ DATASET_URL = {'OmniDocBench':'https://huggingface.co/datasets/ouyanglinke/OmniDocBench_tsv/resolve/main/OmniDocBench.tsv'}
24
+ DATASET_MD5 = {'OmniDocBench': '0fa5ccf31e682e219cb9ca83da741a59'}
25
+
26
+
27
+ system_prompt = r'''You are an AI assistant specialized in converting PDF images to Markdown format. Please follow these instructions for the conversion:
28
+
29
+ 1. Text Processing:
30
+ - Accurately recognize all text content in the PDF image without guessing or inferring.
31
+ - Convert the recognized text into Markdown format.
32
+ - Maintain the original document structure, including headings, paragraphs, lists, etc.
33
+
34
+ 2. Mathematical Formula Processing:
35
+ - Convert all mathematical formulas to LaTeX format.
36
+ # - Enclose inline formulas with \( \). For example: This is an inline formula \( E = mc^2 \)
37
+ - Enclose block formulas with \\[ \\]. For example: \[ \frac{-b \pm \sqrt{b^2 - 4ac}}{2a} \]
38
+
39
+ 3. Table Processing:
40
+ - Convert tables to HTML format.
41
+ - Wrap the entire table with <table> and </table>.
42
+
43
+ 4. Figure Handling:
44
+ - Ignore figures content in the PDF image. Do not attempt to describe or convert images.
45
+
46
+ 5. Output Format:
47
+ - Ensure the output Markdown document has a clear structure with appropriate line breaks between elements.
48
+ - For complex layouts, try to maintain the original document's structure and format as closely as possible.
49
+
50
+ Please strictly follow these guidelines to ensure accuracy and consistency in the conversion. Your task is to accurately convert the content of the PDF image into Markdown format without adding any extra explanations or comments.
51
+ '''
52
+
53
+ def __init__(self,dataset='OmniDocBench',**kwargs):
54
+ super().__init__(dataset,**kwargs)
55
+ print(f'self.img_root:{self.img_root}')
56
+
57
+ def build_prompt(self, line):
58
+
59
+ image_path = self.dump_image(line)[0]
60
+ msg = [
61
+ dict(type='image', value=image_path),
62
+ dict(type='text', value=self.system_prompt)
63
+ ]
64
+ return msg
65
+
66
+ def evaluate(self, eval_file, **judge_kwargs):
67
+ tsv_path=self.data_path
68
+ End2end_evaluator=end2end_evaluator(eval_file,tsv_path)
69
+ Table_evalutor=table_evalutor(eval_file,tsv_path)
70
+
71
+ metrics_all=End2end_evaluator.score()
72
+ metircs_table=Table_evalutor.score()
73
+
74
+ return metrics_all
75
+
76
+
77
+ class end2end_evaluator():
78
+ def __init__(self,
79
+ eval_file,
80
+ tsv_path,
81
+ match_method:str='quick_match',
82
+ filter_types:dict=None):
83
+ self.eval_file=eval_file
84
+ self.match_method=match_method
85
+ self.references=[]
86
+ self.predictions = load(eval_file)['prediction'].tolist()
87
+ self.dafault_metircs_dict={
88
+ 'text_block':
89
+ {'metric': ['Edit_dist', 'BLEU', 'METEOR']},
90
+ 'display_formula':
91
+ {'metric': ['Edit_dist', 'CDM']},
92
+ 'table':
93
+ {'metric': ['TEDS', 'Edit_dist']},
94
+ 'reading_order':
95
+ {'metric': ['Edit_dist']}
96
+ }
97
+
98
+ references = load(tsv_path)['answer'].tolist()
99
+
100
+ load_success,load_fail=0,0
101
+ for i,ans in tqdm(enumerate(references),desc='Loading data'):
102
+ try:
103
+ ans = json.loads(ans)
104
+ load_success+=1
105
+ self.references.append(ans) #[{},{}]
106
+ except json.JSONDecodeError as e:
107
+ load_fail+=1
108
+ continue
109
+ print(f'load_success:{load_success},load_fail:{load_fail}')
110
+
111
+ filtered_gt_samples = []
112
+ if filter_types:
113
+ for gt_sample in self.references:
114
+ select_flag = True
115
+ for k, v in filter_types.items():
116
+ if gt_sample["page_info"]["page_attribute"][k] != v:
117
+ select_flag = False
118
+ if select_flag:
119
+ filtered_gt_samples.append(gt_sample)
120
+ else:
121
+ filtered_gt_samples = self.references #[{},{},{}]
122
+ self.references=filtered_gt_samples
123
+
124
+
125
+ def score(self)->dict:
126
+ samples=self.get_matched_elements(self.references,self.predictions)
127
+ metrics=self.process_generated_metric_results(samples)
128
+ return metrics
129
+
130
+ def get_page_elements(self, selected_annos):
131
+ saved_element_dict = defaultdict(list)
132
+ related_truncated = []
133
+ truncated_all = {}
134
+ for relation in selected_annos["extra"]["relation"]: # Handle truncated text issues
135
+ if relation["relation_type"] == 'truncated':
136
+ truncated_all[relation["source_anno_id"]] = ""
137
+ truncated_all[relation["target_anno_id"]] = ""
138
+ exist_flag = False
139
+ for merge_list in related_truncated:
140
+ if relation["source_anno_id"] in merge_list or relation["target_anno_id"] in merge_list: # Consider cases where three text blocks may need to be merged
141
+ merge_list.append(relation["source_anno_id"])
142
+ merge_list.append(relation["target_anno_id"])
143
+ exist_flag = True
144
+ if not exist_flag:
145
+ related_truncated.append([relation["source_anno_id"], relation["target_anno_id"]])
146
+
147
+ for item in selected_annos['layout_dets']:
148
+ if item['anno_id'] not in truncated_all.keys():
149
+ saved_element_dict[item["category_type"]].append(item)
150
+ else:
151
+ truncated_all[item['anno_id']] = item
152
+
153
+ for merge_list in related_truncated:
154
+ text_block_list = [truncated_all[key] for key in merge_list]
155
+ sorted_block = sorted(text_block_list, key=lambda x: x['order'])
156
+ text = ""
157
+ for block in sorted_block:
158
+ text += block['text']
159
+ merged_block = {
160
+ "category_type": sorted_block[0]["category_type"], # Directly use information from the first block
161
+ "order": sorted_block[0]["order"],
162
+ "anno_id": sorted_block[0]["anno_id"],
163
+ "text": text,
164
+ "merge_list": sorted_block
165
+ }
166
+ saved_element_dict[sorted_block[0]["category_type"]].append(merged_block)
167
+
168
+ return saved_element_dict
169
+
170
+ def get_page_elements_list(self, gt_page_elements, category_list):
171
+ element_list = []
172
+ for category_type in category_list:
173
+ if gt_page_elements.get(category_type):
174
+ element_list.extend(gt_page_elements[category_type])
175
+ return element_list
176
+
177
+ def get_sorted_text_list(self, selected_annos):
178
+ # txt_type: text, latex, html
179
+ text_list = []
180
+ for item in selected_annos:
181
+ if item.get('order'):
182
+ order = item['order']
183
+ else:
184
+ order = 0
185
+ # 【txt_type,selecte_annos]
186
+ text_list.append((order, item))
187
+ sorted_text_list = sorted(text_list, key=lambda x: x[0])
188
+ return [_[1] for _ in sorted_text_list]
189
+
190
+ def filtered_out_ignore(self, items, ignore_category_list):
191
+ filted_items = []
192
+ for item in items:
193
+ if item['gt_category_type'] not in ignore_category_list:
194
+ filted_items.append(item)
195
+ return filted_items
196
+
197
+ def get_order_paired(self, order_match_s, img_name):
198
+ matched = [(item['gt_position'], item['pred_position']) for item in order_match_s if (item['gt_position'] != [""] and item['pred_position'] != "")]
199
+ gt_idx_all = [item['gt_position'] for item in order_match_s if (item['gt_position'] != [""])]
200
+ read_order_pred = [i[0] for i in sorted(matched, key=lambda x: x[1])]
201
+ read_order_gt = sum(gt_idx_all, []) # Convert to one-dimensional list
202
+ read_order_gt = [x for x in read_order_gt if x]
203
+ gt = sorted(read_order_gt)
204
+ pred = sum(read_order_pred, [])
205
+ pred = [x for x in pred if x]
206
+ if len(pred) > 0 or len(gt) > 0:
207
+ import Levenshtein
208
+ edit = Levenshtein.distance(gt, pred)/ max(len(pred), len(gt))
209
+ return {
210
+ 'gt': gt,
211
+ 'pred': pred,
212
+ 'img_id': img_name,
213
+ 'edit': edit
214
+ }
215
+ else:
216
+ return {} # If both GT and pred are empty for the page, return empty
217
+
218
+ def formula_format(self, formula_matches, img_name):
219
+ # formated_list = []
220
+ for i, item in enumerate(formula_matches):
221
+ item["img_id"] = img_name + '_' + str(i)
222
+ return formula_matches
223
+
224
+ def get_matched_elements(self,references:list,predictions:list)->dict:
225
+ from .metrics import recogition_end2end_base_dataset, recogition_end2end_table_dataset
226
+
227
+ plain_text_match = []
228
+ display_formula_match = []
229
+ html_table_match = []
230
+ latex_table_match = []
231
+ order_match = []
232
+
233
+
234
+ for i,sample in enumerate(references):
235
+ img_name = os.path.basename(sample["page_info"]["image_path"])
236
+ pred_content = predictions[i]
237
+ result = self.process_get_matched_elements(sample, pred_content, img_name)
238
+ [plain_text_match_clean, formated_display_formula, latex_table_match_s, html_table_match_s, order_match_single] = result
239
+
240
+ if order_match_single:
241
+ order_match.append(order_match_single)
242
+ if plain_text_match_clean:
243
+ plain_text_match.extend(plain_text_match_clean)
244
+ if formated_display_formula:
245
+ display_formula_match.extend(formated_display_formula)
246
+ if latex_table_match_s:
247
+ latex_table_match.extend(latex_table_match_s)
248
+ if html_table_match_s:
249
+ html_table_match.extend(html_table_match_s)
250
+
251
+ if len(latex_table_match) > len(html_table_match):
252
+ table_match = latex_table_match
253
+ table_format = 'latex'
254
+ else:
255
+ table_match = html_table_match
256
+ table_format = 'html'
257
+
258
+ matched_samples_all = {
259
+ "text_block": recogition_end2end_base_dataset(plain_text_match),
260
+ "display_formula": recogition_end2end_base_dataset(display_formula_match),
261
+ "table": recogition_end2end_table_dataset(table_match, table_format),
262
+ "reading_order": recogition_end2end_base_dataset(order_match)
263
+ }
264
+
265
+ return matched_samples_all
266
+
267
+ def process_get_matched_elements(self, sample, pred_content, img_name):
268
+ from func_timeout import FunctionTimedOut, func_timeout
269
+
270
+ from .utils import (match_gt2pred_no_split, match_gt2pred_quick, match_gt2pred_simple,
271
+ md_tex_filter)
272
+
273
+ if self.match_method == 'simple_match': # add match choice
274
+ match_gt2pred = match_gt2pred_simple
275
+ elif self.match_method == 'quick_match':
276
+ match_gt2pred = match_gt2pred_quick
277
+ elif self.match_method == 'no_split':
278
+ match_gt2pred = match_gt2pred_no_split
279
+ else:
280
+ # print('Invalid match method name. The quick_match will be used.')
281
+ match_gt2pred = match_gt2pred_quick
282
+
283
+ pred_dataset = md_tex_filter(pred_content)
284
+ gt_page_elements = self.get_page_elements(sample)
285
+
286
+ text_all = self.get_page_elements_list(gt_page_elements, ['text_block', 'title', 'code_txt', 'code_txt_caption', 'reference', 'equation_caption',
287
+ 'figure_caption', 'figure_footnote', 'table_caption', 'table_footnote', 'code_algorithm', 'code_algorithm_caption',
288
+ 'header', 'footer', 'page_footnote', 'page_number'])
289
+
290
+
291
+ display_formula_match_s = []
292
+ plain_text_match_clean = []
293
+ latex_table_match_s = []
294
+ html_table_match_s = []
295
+ order_match_single = []
296
+ if text_all:
297
+ gt_text_list = self.get_sorted_text_list(text_all)
298
+ try:
299
+ plain_text_match_s = func_timeout(
300
+ 30, match_gt2pred, args=(gt_text_list, pred_dataset['text_all'], 'text', img_name)
301
+ )
302
+ except FunctionTimedOut as e1:
303
+ print(f'Time out for plain text match of {img_name}, match_gt2pred_simple will be used.')
304
+ plain_text_match_s = match_gt2pred_simple(gt_text_list, pred_dataset['text_all'], 'text', img_name)
305
+ except Exception as e:
306
+ print(str(e))
307
+ sys.exit()
308
+
309
+ if not plain_text_match_s:
310
+ print(f'No text match of {img_name}. The plain text match will be empty.')
311
+ else:
312
+ plain_text_match_clean = self.filtered_out_ignore(plain_text_match_s, ['figure_caption', 'figure_footnote', 'table_caption', 'table_footnote', 'code_algorithm', 'code_algorithm_caption', 'header', 'footer', 'page_footnote', 'page_number', 'equation_caption'])
313
+
314
+
315
+ if gt_page_elements.get('equation_isolated'):
316
+ gt_display_list = self.get_sorted_text_list(gt_page_elements['equation_isolated'])
317
+ display_formula_match_s = match_gt2pred(gt_display_list, pred_dataset['equation_isolated'], 'formula', img_name)
318
+ display_formula_match_s = [x for x in display_formula_match_s if x['gt_idx'] != [""]]
319
+ if not display_formula_match_s:
320
+ print(f'No display_formula_match of {img_name}. The display_formula_match will be empty.')
321
+
322
+ if gt_page_elements.get('table'):
323
+ gt_table_list = self.get_sorted_text_list(gt_page_elements['table'])
324
+ if pred_dataset['latex_table']:
325
+ latex_table_match_s = match_gt2pred_simple(gt_table_list, pred_dataset['latex_table'], 'latex_table', img_name)
326
+ latex_table_match_s = [x for x in latex_table_match_s if x['gt_idx'] != [""]]
327
+ if pred_dataset['html_table']:
328
+ html_table_match_s = match_gt2pred_simple(gt_table_list, pred_dataset['html_table'], 'html_table', img_name)
329
+ html_table_match_s = [x for x in html_table_match_s if x['gt_idx'] != [""]]
330
+ else:
331
+ html_table_match_s = match_gt2pred_simple(gt_table_list, [], 'html_table', img_name)
332
+ html_table_match_s = [x for x in html_table_match_s if x['gt_idx'] != [""]]
333
+
334
+
335
+ order_match_s = plain_text_match_clean
336
+ if order_match_s:
337
+ order_match_single = self.get_order_paired(order_match_s, img_name)
338
+
339
+ return [plain_text_match_clean, display_formula_match_s, latex_table_match_s, html_table_match_s, order_match_single]
340
+
341
+ def process_generated_metric_results(self,samples,save_name:str='end2end_quick_match'):
342
+ from .metrics import METRIC_REGISTRY, get_full_labels_results, get_page_split, show_result
343
+
344
+ result_all={}
345
+ page_info={}
346
+ metircs_dict=self.dafault_metircs_dict
347
+ pages=self.references #gt_samples list
348
+
349
+ for page in pages:
350
+ img_path=os.path.basename(page['page_info']['image_path'])
351
+ page_info[img_path]=page['page_info']['page_attribute']
352
+
353
+ for element in metircs_dict.keys():
354
+
355
+ result={}
356
+ group_info=metircs_dict[element].get('group',[])
357
+ # samples = samples.get(element) ##
358
+ cur_samples = samples[element]
359
+
360
+ for metric in metircs_dict[element]['metric']:
361
+ metric_val = METRIC_REGISTRY.get(metric)
362
+
363
+ cur_samples,result_s = metric_val(cur_samples).evaluate(group_info, f"{save_name}_{element}")
364
+ if result_s:
365
+ result.update(result_s)
366
+
367
+ if result:
368
+ print(f"{element}")
369
+ show_result(result)
370
+ result_all[element]={}
371
+
372
+
373
+ group_result=get_full_labels_results(cur_samples)
374
+ page_result=get_page_split(cur_samples,page_info)
375
+
376
+ result_all[element]={
377
+ 'all':result,
378
+ 'group':group_result,
379
+ 'page':page_result
380
+ }
381
+ if isinstance(cur_samples,list):
382
+ saved_samples=cur_samples
383
+ else:
384
+ saved_samples=cur_samples.samples
385
+ # NOTE: The original code has a bug here, it will overwrite the result file in each iteration.
386
+ # I will fix it by adding element to the filename.
387
+ # NOTE: Fixed typo .josn -> .json
388
+ result_file = get_intermediate_file_path(self.eval_file, f'_{save_name}_{element}_result', 'json')
389
+ dump(saved_samples, result_file)
390
+
391
+ metric_result_file = get_intermediate_file_path(self.eval_file, f'_{save_name}_metric_result', 'json')
392
+ dump(result_all, metric_result_file)
393
+
394
+ dict_list = []
395
+ save_dict={}
396
+ en_overall=[]
397
+ ch_overall=[]
398
+ for category_type, metric in [("text_block", "Edit_dist"), ("display_formula", "Edit_dist"), ("display_formula", "CDM"), ("table", "TEDS"), ("table", "Edit_dist"), ("reading_order", "Edit_dist")]:
399
+ if metric == 'CDM':
400
+ save_dict[category_type+'_'+metric+'_EN'] = '-'
401
+ save_dict[category_type+'_'+metric+'_CH'] = '-'
402
+ elif metric == "TEDS":
403
+ save_dict[category_type+'_'+metric+'_EN'] = result_all[category_type]["page"][metric]["language: english"] * 100
404
+ save_dict[category_type+'_'+metric+'_CH'] = result_all[category_type]["page"][metric]["language: simplified_chinese"] * 100
405
+ else:
406
+ save_dict[category_type+'_'+metric+'_EN'] = result_all[category_type]["page"][metric].get("language: english", np.nan)
407
+ save_dict[category_type+'_'+metric+'_CH'] = result_all[category_type]["page"][metric].get("language: simplified_chinese",np.nan)
408
+ if metric == "Edit_dist":
409
+ en_overall.append(result_all[category_type]["page"][metric].get("language: english", np.nan))
410
+ ch_overall.append(result_all[category_type]["page"][metric].get("language: simplified_chinese",np.nan))
411
+
412
+ save_dict['overall_EN'] = sum(en_overall) / len(en_overall)
413
+ save_dict['overall_CH'] = sum(ch_overall) / len(ch_overall)
414
+ dict_list.append(save_dict)
415
+ df = pd.DataFrame(dict_list,index=['end2end',]).round(3)
416
+
417
+ e2e_eval_file = get_intermediate_file_path(self.eval_file, '_End2End_Evaluation', 'json')
418
+ dump(result_all, e2e_eval_file)
419
+
420
+ overall_file = get_intermediate_file_path(self.eval_file, '_overall')
421
+ dump(df, overall_file)
422
+
423
+ print(f"The save path of End2End_Evaluation is: {e2e_eval_file}")
424
+ print(f"The save path of overall metrics is: {overall_file}")
425
+ return df
426
+
427
+
428
+ class table_evalutor():
429
+ def __init__(self,eval_file,tsv_path):
430
+ self.eval_file = eval_file
431
+ gt_key='html'
432
+ pred_key='pred'
433
+ self.category_filter='table'
434
+ self.category_type='table'
435
+ self.metircs_list=['TEDS','Edit_dist']
436
+ self.gt_samples,self.table_samples=self.load_data(eval_file,tsv_path,pred_key,gt_key)
437
+
438
+ def load_data(self,eval_file,gt_file,pred_key,gt_key):
439
+ from .data_preprocess import (clean_string, normalized_formula, normalized_table,
440
+ textblock2unicode)
441
+ samples=[]
442
+ preds=[]
443
+ predictions=load(eval_file)['prediction'].tolist()
444
+ gt_samples=load(gt_file)['answer'].tolist()
445
+ load_success,load_fail=0,0
446
+ for i,gt_sample in tqdm(enumerate(gt_samples),desc='Loading data'):
447
+ try:
448
+ ans=json.loads(gt_sample)
449
+ for item in ans['layout_dets']:
450
+ if item['category_type']=="table":
451
+ item['pred']=predictions[i]
452
+ load_success+=1
453
+ preds.append(ans)
454
+
455
+ except json.JSONDecodeError as e:
456
+ load_fail+=1
457
+ continue
458
+ print(f'load_table_success:{load_success},load_table_fail:{load_fail}')
459
+
460
+ count=0
461
+ for pred in preds:
462
+ img_name = os.path.basename(pred['page_info']['image_path'])
463
+ for i, ann in enumerate(pred['layout_dets']):
464
+ if not ann.get(gt_key):
465
+ continue
466
+ if self.category_filter:
467
+ if ann['category_type'] not in self.category_filter:
468
+ continue
469
+ if not ann.get(pred_key):
470
+ # print(f'Cannot find pred for {img_name}. ann is {ann}')
471
+ # pdb.set_trace()
472
+ count += 1
473
+ continue
474
+ else:
475
+ gt_text = ann[gt_key]
476
+ norm_gt = gt_text
477
+ pred_text = ann[pred_key]
478
+ norm_pred = pred_text
479
+ if self.category_type:
480
+ if self.category_type == 'text':
481
+ norm_gt = clean_string(textblock2unicode(ann[gt_key]))
482
+ norm_pred = clean_string(textblock2unicode(ann[pred_key]))
483
+ elif self.category_type == 'formula':
484
+ norm_gt = normalized_formula(ann[gt_key])
485
+ norm_pred = normalized_formula(ann[pred_key])
486
+ elif self.category_type == 'table':
487
+ norm_gt = normalized_table(ann[gt_key], gt_key)
488
+ norm_pred = normalized_table(ann[pred_key], gt_key)
489
+ else:
490
+ raise ValueError(f'Invalid category type: {self.category_type}')
491
+
492
+ samples.append({
493
+ "gt": gt_text,
494
+ "norm_gt": norm_gt,
495
+ "gt_attribute": [ann['attribute']],
496
+ 'pred': pred_text,
497
+ "norm_pred": norm_pred,
498
+ 'img_id': img_name
499
+ })
500
+
501
+ print(f'Cannot find pred for {count} samples.')
502
+ return preds,samples
503
+
504
+ def score(self)->dict:
505
+ metrics=self.process_generated_metric_results()
506
+ return metrics
507
+
508
+ def process_generated_metric_results(self,save_name:str='OmniDocBench_table'):
509
+ from .metrics import METRIC_REGISTRY, get_full_labels_results, get_page_split, show_result
510
+
511
+ p_scores={}
512
+ page_info={}
513
+ no_page_flag=False
514
+ samples=self.table_samples
515
+ pages=self.gt_samples
516
+
517
+ for page in pages:
518
+ if 'page_info' not in page:
519
+ no_page_flag=True
520
+ break
521
+ img_path=os.path.basename(page['page_info']['image_path'])
522
+ page_info[img_path]=page['page_info']['page_attribute']
523
+
524
+ for metric in self.metircs_list:
525
+ metric_val=METRIC_REGISTRY.get(metric)
526
+ samples, result = metric_val(samples).evaluate({}, save_name)
527
+ if result:
528
+ p_scores.update(result)
529
+ show_result(p_scores)
530
+ group_result=get_full_labels_results(samples)
531
+ if no_page_flag:
532
+ page_result={}
533
+ else:
534
+ page_result=get_page_split(samples,page_info)
535
+
536
+ result_all={
537
+ 'all':p_scores,
538
+ 'group':group_result,
539
+ 'page':page_result
540
+ }
541
+
542
+ metric_result_file = get_intermediate_file_path(self.eval_file, f'_{save_name}_metric_result', 'json')
543
+ dump(result_all, metric_result_file)
544
+
545
+ dict_list=[]
546
+ dict_list.append(result_all["group"]["TEDS"])
547
+
548
+ df4 = pd.DataFrame(dict_list, index=['OmniDocBench_table'])
549
+ df4 = df4 * 100
550
+ df4 = df4.round(1)
551
+ selected_columns = df4[["language: table_en", "language: table_simplified_chinese", "language: table_en_ch_mixed", "line: full_line", "line: less_line", "line: fewer_line", "line: wireless_line",
552
+ "with_span: True", "with_span: False", "include_equation: True", "include_equation: False", "include_background: True", "include_background: False", "table_layout: vertical", "table_layout: horizontal"]]
553
+
554
+ table_attr_file = get_intermediate_file_path(self.eval_file, '_table_attribute')
555
+ dump(selected_columns, table_attr_file)
556
+ print(f'The save path of table_attribute is :{table_attr_file}')
557
+ return selected_columns
reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/OmniDocBench/requirements.txt ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ accelerate>=0.26.0
2
+ apted
3
+ BeautifulSoup4
4
+ evaluate
5
+ func_timeout
6
+ jmespath
7
+ Levenshtein
8
+ lxml
9
+ nltk
10
+ pylatexenc
11
+ qwen_vl_utils
12
+ scipy
13
+ torchvision
reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/OmniDocBench/utils.py ADDED
@@ -0,0 +1,1918 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import copy
2
+ import html
3
+ import json
4
+ import os
5
+ import pdb
6
+ import re
7
+ import shutil
8
+ import subprocess
9
+ import sys
10
+ import unicodedata
11
+ import uuid
12
+ from collections import defaultdict
13
+
14
+ import Levenshtein
15
+ import numpy as np
16
+ from bs4 import BeautifulSoup
17
+ from pylatexenc.latex2text import LatexNodes2Text
18
+ from pylatexenc.latexencode import unicode_to_latex
19
+ from pylatexenc.latexwalker import (LatexCharsNode, LatexEnvironmentNode, LatexGroupNode,
20
+ LatexMacroNode, LatexSpecialsNode, LatexWalker)
21
+ from scipy.optimize import linear_sum_assignment
22
+
23
+
24
+ def read_md_file(filepath):
25
+ with open(filepath, 'r', encoding='utf-8') as file:
26
+ content = file.read()
27
+
28
+ return content
29
+
30
+ def save_paired_result(preds, gts, save_path):
31
+ save_result = []
32
+ formula_id = 0
33
+ for gt, pred in zip(gts, preds):
34
+ save_result.append({
35
+ "gt": gt,
36
+ "pred": pred,
37
+ "img_id": formula_id
38
+ })
39
+ formula_id += 1
40
+ with open(save_path, 'w', encoding='utf-8') as f:
41
+ json.dump(save_result, f, indent=4, ensure_ascii=False)
42
+
43
+ def remove_markdown_fences(content):
44
+ content = re.sub(r'^```markdown\n?', '', content, flags=re.MULTILINE)
45
+ content = re.sub(r'```\n?$', '', content, flags=re.MULTILINE)
46
+ return content
47
+
48
+ # Standardize all consecutive characters
49
+ def replace_repeated_chars(input_str):
50
+ input_str = re.sub(r'_{4,}', '____', input_str) # Replace more than 4 consecutive underscores with 4 underscores
51
+ input_str = re.sub(r' {4,}', ' ', input_str) # Replace more than 4 consecutive spaces with 4 spaces
52
+ return re.sub(r'([^a-zA-Z0-9])\1{10,}', r'\1\1\1\1', input_str) # For other consecutive symbols (except numbers and letters), replace more than 10 occurrences with 4
53
+
54
+ # Special Unicode handling
55
+ def fullwidth_to_halfwidth(s):
56
+ result = []
57
+ for char in s:
58
+ code = ord(char)
59
+ # Convert full-width space to half-width space
60
+ if code == 0x3000:
61
+ code = 0x0020
62
+ # Convert other full-width characters to half-width
63
+ elif 0xFF01 <= code <= 0xFF5E:
64
+ code -= 0xFEE0
65
+ result.append(chr(code))
66
+ return ''.join(result)
67
+
68
+ def find_special_unicode(s):
69
+ special_chars = {}
70
+ for char in s:
71
+ if ord(char) > 127: # Non-ASCII characters
72
+ # unicode_name = unicodedata.name(char, None)
73
+ unicode_name = unicodedata.category(char)
74
+ special_chars[char] = f'U+{ord(char):04X} ({unicode_name})'
75
+ return special_chars
76
+
77
+
78
+ inline_reg = re.compile(
79
+ r'\$(.*?)\$|'
80
+ r'\\\((.*?)\\\)',
81
+ )
82
+
83
+ def textblock2unicode(text):
84
+ inline_matches = inline_reg.finditer(text)
85
+ removal_positions = []
86
+ for match in inline_matches:
87
+ position = [match.start(), match.end()]
88
+ content = match.group(1) if match.group(1) is not None else match.group(2)
89
+ # print('-------- content-------', content)
90
+ # Remove escape characters \
91
+ clean_content = re.sub(r'\\([\\_&%^])', '', content)
92
+
93
+ try:
94
+ if any(char in clean_content for char in r'\^_'):
95
+ if clean_content.endswith('\\'):
96
+ clean_content += ' '
97
+ # inline_array.append(match.group(0))
98
+ unicode_content = LatexNodes2Text().latex_to_text(clean_content)
99
+ removal_positions.append((position[0], position[1], unicode_content))
100
+ except:
101
+ continue
102
+
103
+ # Remove inline formulas from original text
104
+ for start, end, unicode_content in sorted(removal_positions, reverse=True):
105
+ text = text[:start] + unicode_content.strip() + text[end:]
106
+
107
+ return text
108
+
109
+ def normalized_formula(text):
110
+ # Normalize math formulas before matching
111
+ filter_list = ['\\mathbf', '\\mathrm', '\\mathnormal', '\\mathit', '\\mathbb', '\\mathcal', '\\mathscr', '\\mathfrak', '\\mathsf', '\\mathtt',
112
+ '\\textbf', '\\text', '\\boldmath', '\\boldsymbol', '\\operatorname', '\\bm',
113
+ '\\symbfit', '\\mathbfcal', '\\symbf', '\\scriptscriptstyle', '\\notag',
114
+ '\\setlength', '\\coloneqq', '\\space', '\\thickspace', '\\thinspace', '\\medspace', '\\nobreakspace', '\\negmedspace',
115
+ '\\quad', '\\qquad', '\\enspace', '\\substackw', ' ']
116
+ # '\\left', '\\right', '{', '}', ' ']
117
+
118
+ # delimiter_filter
119
+ pattern = re.compile(r"\\\[(.+?)(?<!\\)\\\]")
120
+ match = pattern.search(text)
121
+
122
+ if match:
123
+ text = match.group(1).strip()
124
+
125
+ tag_pattern = re.compile(r"\\tag\{.*?\}")
126
+ text = tag_pattern.sub('', text)
127
+ hspace_pattern = re.compile(r"\\hspace\{.*?\}")
128
+ text = hspace_pattern.sub('', text)
129
+ begin_pattern = re.compile(r"\\begin\{.*?\}")
130
+ text = begin_pattern.sub('', text)
131
+ end_pattern = re.compile(r"\\end\{.*?\}")
132
+ text = end_pattern.sub('', text)
133
+ col_sep = re.compile(r"\\arraycolsep.*?\}")
134
+ text = col_sep.sub('', text)
135
+ text = text.strip('.')
136
+
137
+ for filter_text in filter_list:
138
+ text = text.replace(filter_text, '')
139
+
140
+ # text = normalize_text(delimiter_filter(text))
141
+ # text = delimiter_filter(text)
142
+ text = text.lower()
143
+ return text
144
+
145
+ def normalized_html_table(text):
146
+ def process_table_html(md_i):
147
+ """
148
+ pred_md format edit
149
+ """
150
+ def process_table_html(html_content):
151
+ soup = BeautifulSoup(html_content, 'html.parser')
152
+ th_tags = soup.find_all('th')
153
+ for th in th_tags:
154
+ th.name = 'td'
155
+ thead_tags = soup.find_all('thead')
156
+ for thead in thead_tags:
157
+ thead.unwrap() # unwrap()会移除标签但保留其内容
158
+ math_tags = soup.find_all('math')
159
+ for math_tag in math_tags:
160
+ alttext = math_tag.get('alttext', '')
161
+ alttext = f'${alttext}$'
162
+ if alttext:
163
+ math_tag.replace_with(alttext)
164
+ span_tags = soup.find_all('span')
165
+ for span in span_tags:
166
+ span.unwrap()
167
+ return str(soup)
168
+
169
+ table_res=''
170
+ table_res_no_space=''
171
+ if '<table' in md_i.replace(" ","").replace("'",'"'):
172
+ md_i = process_table_html(md_i)
173
+ table_res = html.unescape(md_i).replace('\n', '')
174
+ table_res = unicodedata.normalize('NFKC', table_res).strip()
175
+ pattern = r'<table\b[^>]*>(.*)</table>'
176
+ tables = re.findall(pattern, table_res, re.DOTALL | re.IGNORECASE)
177
+ table_res = ''.join(tables)
178
+ # table_res = re.sub('<table.*?>','',table_res)
179
+ table_res = re.sub('( style=".*?")', "", table_res)
180
+ table_res = re.sub('( height=".*?")', "", table_res)
181
+ table_res = re.sub('( width=".*?")', "", table_res)
182
+ table_res = re.sub('( align=".*?")', "", table_res)
183
+ table_res = re.sub('( class=".*?")', "", table_res)
184
+ table_res = re.sub('</?tbody>',"",table_res)
185
+
186
+ table_res = re.sub(r'\s+', " ", table_res)
187
+ table_res_no_space = '<html><body><table border="1" >' + table_res.replace(' ','') + '</table></body></html>'
188
+ # table_res_no_space = re.sub(' (style=".*?")',"",table_res_no_space)
189
+ # table_res_no_space = re.sub(r'[ ]', " ", table_res_no_space)
190
+ table_res_no_space = re.sub('colspan="', ' colspan="', table_res_no_space)
191
+ table_res_no_space = re.sub('rowspan="', ' rowspan="', table_res_no_space)
192
+ table_res_no_space = re.sub('border="', ' border="', table_res_no_space)
193
+
194
+ table_res = '<html><body><table border="1" >' + table_res + '</table></body></html>'
195
+ # table_flow.append(table_res)
196
+ # table_flow_no_space.append(table_res_no_space)
197
+
198
+ return table_res, table_res_no_space
199
+
200
+ def clean_table(input_str,flag=True):
201
+ if flag:
202
+ input_str = input_str.replace('<sup>', '').replace('</sup>', '')
203
+ input_str = input_str.replace('<sub>', '').replace('</sub>', '')
204
+ input_str = input_str.replace('<span>', '').replace('</span>', '')
205
+ input_str = input_str.replace('<div>', '').replace('</div>', '')
206
+ input_str = input_str.replace('<p>', '').replace('</p>', '')
207
+ input_str = input_str.replace('<spandata-span-identity="">', '')
208
+ input_str = re.sub('<colgroup>.*?</colgroup>','',input_str)
209
+ return input_str
210
+
211
+ norm_text, _ = process_table_html(text)
212
+ norm_text = clean_table(norm_text)
213
+ return norm_text
214
+
215
+ def normalized_latex_table(text):
216
+ def latex_template(latex_code):
217
+ template = r'''
218
+ \documentclass[border=20pt]{article}
219
+ \usepackage{subcaption}
220
+ \usepackage{url}
221
+ \usepackage{graphicx}
222
+ \usepackage{caption}
223
+ \usepackage{multirow}
224
+ \usepackage{booktabs}
225
+ \usepackage{color}
226
+ \usepackage{colortbl}
227
+ \usepackage{xcolor,soul,framed}
228
+ \usepackage{fontspec}
229
+ \usepackage{amsmath,amssymb,mathtools,bm,mathrsfs,textcomp}
230
+ \setlength{\parindent}{0pt}''' + \
231
+ r'''
232
+ \begin{document}
233
+ ''' + \
234
+ latex_code + \
235
+ r'''
236
+ \end{document}'''
237
+
238
+ return template
239
+
240
+ def process_table_latex(latex_code):
241
+ SPECIAL_STRINGS= [
242
+ ['\\\\vspace\\{.*?\\}', ''],
243
+ ['\\\\hspace\\{.*?\\}', ''],
244
+ ['\\\\rule\{.*?\\}\\{.*?\\}', ''],
245
+ ['\\\\addlinespace\\[.*?\\]', ''],
246
+ ['\\\\addlinespace', ''],
247
+ ['\\\\renewcommand\\{\\\\arraystretch\\}\\{.*?\\}', ''],
248
+ ['\\\\arraystretch\\{.*?\\}', ''],
249
+ ['\\\\(row|column)?colors?\\{[^}]*\\}(\\{[^}]*\\}){0,2}', ''],
250
+ ['\\\\color\\{.*?\\}', ''],
251
+ ['\\\\textcolor\\{.*?\\}', ''],
252
+ ['\\\\rowcolor(\\[.*?\\])?\\{.*?\\}', ''],
253
+ ['\\\\columncolor(\\[.*?\\])?\\{.*?\\}', ''],
254
+ ['\\\\cellcolor(\\[.*?\\])?\\{.*?\\}', ''],
255
+ ['\\\\colorbox\\{.*?\\}', ''],
256
+ ['\\\\(tiny|scriptsize|footnotesize|small|normalsize|large|Large|LARGE|huge|Huge)', ''],
257
+ [r'\s+', ' '],
258
+ ['\\\\centering', ''],
259
+ ['\\\\begin\\{table\\}\\[.*?\\]', '\\\\begin{table}'],
260
+ ['\t', ''],
261
+ ['@{}', ''],
262
+ ['\\\\toprule(\\[.*?\\])?', '\\\\hline'],
263
+ ['\\\\bottomrule(\\[.*?\\])?', '\\\\hline'],
264
+ ['\\\\midrule(\\[.*?\\])?', '\\\\hline'],
265
+ ['p\\{[^}]*\\}', 'l'],
266
+ ['m\\{[^}]*\\}', 'c'],
267
+ ['\\\\scalebox\\{[^}]*\\}\\{([^}]*)\\}', '\\1'],
268
+ ['\\\\textbf\\{([^}]*)\\}', '\\1'],
269
+ ['\\\\textit\\{([^}]*)\\}', '\\1'],
270
+ ['\\\\cmidrule(\\[.*?\\])?\\(.*?\\)\\{([0-9]-[0-9])\\}', '\\\\cline{\\2}'],
271
+ ['\\\\hline', ''],
272
+ [r'\\multicolumn\{1\}\{[^}]*\}\{((?:[^{}]|(?:\{[^{}]*\}))*)\}', r'\1']
273
+ ]
274
+ pattern = r'\\begin\{tabular\}.*\\end\{tabular\}' # 注意这里不用 .*?
275
+ matches = re.findall(pattern, latex_code, re.DOTALL)
276
+ latex_code = ' '.join(matches)
277
+
278
+ for special_str in SPECIAL_STRINGS:
279
+ latex_code = re.sub(fr'{special_str[0]}', fr'{special_str[1]}', latex_code)
280
+
281
+ return latex_code
282
+
283
+ def convert_latex_to_html(latex_content, cache_dir='./temp'):
284
+ if not os.path.exists(cache_dir):
285
+ os.makedirs(cache_dir)
286
+
287
+ uuid_str = str(uuid.uuid1())
288
+ with open(f'{cache_dir}/{uuid_str}.tex', 'w') as f:
289
+ f.write(latex_template(latex_content))
290
+
291
+ cmd = ['latexmlc', '--quiet', '--nocomments', f'--log={cache_dir}/{uuid_str}.log',
292
+ f'{cache_dir}/{uuid_str}.tex', f'--dest={cache_dir}/{uuid_str}.html']
293
+ try:
294
+ subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
295
+ with open(f'{cache_dir}/{uuid_str}.html', 'r') as f:
296
+ html_content = f.read()
297
+
298
+ pattern = r'<table\b[^>]*>(.*)</table>'
299
+ tables = re.findall(pattern, html_content, re.DOTALL | re.IGNORECASE)
300
+ tables = [f'<table>{table}</table>' for table in tables]
301
+ html_content = '\n'.join(tables)
302
+
303
+ except Exception as e:
304
+ html_content = ''
305
+
306
+ shutil.rmtree(cache_dir)
307
+ return html_content
308
+
309
+ html_text = convert_latex_to_html(text)
310
+ normlized_tables = normalized_html_table(html_text)
311
+ return normlized_tables
312
+
313
+
314
+ def normalized_table(text, format='html'):
315
+ if format not in ['html', 'latex']:
316
+ raise ValueError('Invalid format: {}'.format(format))
317
+ else:
318
+ return globals()['normalized_{}_table'.format(format)](text)
319
+
320
+
321
+ def textblock_with_norm_formula(text):
322
+ inline_matches = inline_reg.finditer(text)
323
+ removal_positions = []
324
+ for match in inline_matches:
325
+ position = [match.start(), match.end()]
326
+ content = match.group(1) if match.group(1) is not None else match.group(2)
327
+ # print('-------- content-------', content)
328
+
329
+ norm_content = normalized_formula(content)
330
+ removal_positions.append((position[0], position[1], norm_content))
331
+
332
+ # Remove inline formulas from original text
333
+ for start, end, norm_content in sorted(removal_positions, reverse=True):
334
+ text = text[:start] + norm_content.strip() + text[end:]
335
+
336
+ return text
337
+
338
+
339
+ def inline_filter_unicode(text):
340
+ # Ensure text is string type
341
+ if not isinstance(text, str):
342
+ text = str(text)
343
+
344
+ # Replace inline formula boundary markers
345
+ #print('--------text-------',text)
346
+ placeholder = '__INLINE_FORMULA_BOUNDARY__'
347
+ text_copy = text.replace('$', placeholder).replace('\\(', placeholder).replace('\\)', placeholder)
348
+ #print('--------text_copy-------',text_copy)
349
+ # Convert LaTeX content to Unicode representation
350
+ text_copy = LatexNodes2Text().latex_to_text(text_copy)
351
+ #print('--------text_copy---unicode----',text_copy)
352
+ # Restore boundary markers
353
+ text_copy = text_copy.replace(placeholder, '$')
354
+
355
+ inline_array = []
356
+ inline_matches = inline_reg.finditer(text_copy)
357
+ # Record positions of inline formulas to be removed
358
+ removal_positions = []
359
+
360
+ for match in inline_matches:
361
+ position = [match.start(), match.end()]
362
+ content = match.group(1) if match.group(1) is not None else match.group(2)
363
+ print('-------- content-------', content)
364
+ # Remove escape characters \
365
+ clean_content = re.sub(r'\\([\\_&%^])', '', content)
366
+
367
+ if any(char in clean_content for char in r'\^_'):
368
+ # inline_array.append(match.group(0))
369
+ inline_array.append({
370
+ 'category_type': 'equation_inline',
371
+ 'position': position,
372
+ 'content': content,
373
+ })
374
+ removal_positions.append((position[0], position[1]))
375
+
376
+ # Remove inline formulas from original text
377
+ for start, end in sorted(removal_positions, reverse=True):
378
+ text = text[:start] + text[end:]
379
+
380
+ return text, inline_array
381
+
382
+ def inline_filter(text):
383
+ # Ensure text is string type
384
+ if not isinstance(text, str):
385
+ text = str(text)
386
+
387
+ inline_array = []
388
+ inline_matches = inline_reg.finditer(text)
389
+
390
+ for match in inline_matches:
391
+ position = [match.start(), match.end()]
392
+ content = match.group(1) if match.group(1) is not None else match.group(2)
393
+ # print('inline_content: ', content)
394
+
395
+ # Remove escape characters \
396
+ clean_content = re.sub(r'\\([\\_&%^])', '', content)
397
+
398
+ if any(char in clean_content for char in r'\^_'):
399
+ # inline_array.append(match.group(0))
400
+ inline_array.append({
401
+ 'category_type': 'equation_inline',
402
+ 'position': position,
403
+ 'content': match.group(0),
404
+ })
405
+ text = text.replace(match.group(0), '')
406
+ # print('-----Found inline formula: ', match.group(0))
407
+ else:
408
+ text = text.replace(match.group(0), content)
409
+
410
+ return text, inline_array
411
+
412
+ # Text OCR quality check processing:
413
+ def clean_string(input_string):
414
+ # Use regex to keep Chinese characters, English letters and numbers
415
+ input_string = input_string.replace('\\t', '').replace('\\n', '').replace('\t', '').replace('\n', '').replace('/t', '').replace('/n', '')
416
+ cleaned_string = re.sub(r'[^\w\u4e00-\u9fff]', '', input_string)
417
+ return cleaned_string
418
+
419
+ def extract_tabular(text):
420
+ begin_pattern = r'\\begin{tabular}'
421
+ end_pattern = r'\\end{tabular}'
422
+
423
+ tabulars = []
424
+ positions = []
425
+ current_pos = 0
426
+ stack = []
427
+
428
+ while current_pos < len(text):
429
+ begin_match = re.search(begin_pattern, text[current_pos:])
430
+ end_match = re.search(end_pattern, text[current_pos:])
431
+
432
+ if not begin_match and not end_match:
433
+ break
434
+
435
+ if begin_match and (not end_match or begin_match.start() < end_match.start()):
436
+ stack.append(current_pos + begin_match.start())
437
+ current_pos += begin_match.start() + len(end_pattern)
438
+ elif end_match:
439
+ if stack:
440
+ start_pos = stack.pop()
441
+ if not stack:
442
+ end_pos = current_pos + end_match.start() + len(end_pattern)
443
+ tabular_code = text[start_pos:end_pos]
444
+ tabulars.append(tabular_code)
445
+ positions.append((start_pos, end_pos))
446
+ current_pos += end_match.start() + len(end_pattern)
447
+ else:
448
+ current_pos += 1
449
+
450
+ if stack:
451
+ new_start = stack[0] + len(begin_pattern)
452
+ new_tabulars, new_positions = extract_tabular(text[new_start:])
453
+ new_positions = [(start + new_start, end + new_start) for start, end in new_positions]
454
+ tabulars.extend(new_tabulars)
455
+ positions.extend(new_positions)
456
+
457
+ return tabulars, positions
458
+
459
+ # math reg
460
+ # r'\\begin{equation\*?}(.*?)\\end{equation\*?}|'
461
+ # r'\\begin{align\*?}(.*?)\\end{align\*?}|'
462
+ # r'\\begin{gather\*?}(.*?)\\end{gather\*?}|'
463
+ display_reg = re.compile(
464
+ r'\$\$(.*?)\$\$|'
465
+ r'\\\[(.*?)\\\]|'
466
+ r'\$(.*?)\$|'
467
+ r'\\\((.*?)\\\)',
468
+ re.DOTALL
469
+ )
470
+
471
+ # inline_reg = re.compile(
472
+ # r'(?<!\$)\$(?!\$)(.*?)(?<!\$)\$(?!\$)|'
473
+ # r'\\\((.*?)\\\)',
474
+ # )
475
+ inline_reg = re.compile(
476
+ r'\$(.*?)\$|'
477
+ r'\\\((.*?)\\\)',
478
+ )
479
+
480
+ # table
481
+ table_reg = re.compile(
482
+ r'\\begin{table\*?}(.*?)\\end{table\*?}|'
483
+ r'\\begin{tabular\*?}(.*?)\\end{tabular\*?}',
484
+ re.DOTALL
485
+ )
486
+ md_table_reg = re.compile(
487
+ r'\|\s*.*?\s*\|\n',
488
+ re.DOTALL)
489
+ html_table_reg = re.compile(
490
+ r'(<table.*?</table>)',
491
+ re.DOTALL
492
+ )
493
+
494
+ # title
495
+ title_reg = re.compile(
496
+ r'^\s*#.*$',
497
+ re.MULTILINE)
498
+
499
+ # img
500
+ img_pattern = r'!\[.*?\]\(.*?\)'
501
+
502
+ # code block
503
+ code_block_reg = re.compile(
504
+ r'```(\w+)\n(.*?)```',
505
+ re.DOTALL
506
+ )
507
+
508
+
509
+ def md_tex_filter(content):
510
+ '''
511
+ Input: 1 page md or tex content - String
512
+ Output: text, display, inline, table, title, code - list
513
+ '''
514
+ content = re.sub(img_pattern, '', content) # remove image
515
+ content = remove_markdown_fences(content) # remove markdown fences
516
+ content = replace_repeated_chars(content) # replace all consecutive characters
517
+
518
+
519
+
520
+ pred_all = []
521
+ latex_table_array, table_positions = extract_tex_table(content)
522
+ for latex_table, position in zip(latex_table_array, table_positions):
523
+ position = [position[0], position[0]+len(latex_table)] # !!!
524
+ pred_all.append({
525
+ 'category_type': 'latex_table',
526
+ 'position': position,
527
+ 'content': latex_table
528
+ })
529
+ content = content[:position[0]] + ' '*(position[1]-position[0]) + content[position[1]:] # replace latex table with space
530
+
531
+
532
+ # extract html table
533
+ html_table_array, table_positions = extract_html_table(content)
534
+ for html_table, position in zip(html_table_array, table_positions):
535
+ position = [position[0], position[0]+len(html_table)]
536
+ pred_all.append({
537
+ 'category_type': 'html_table',
538
+ 'position': position,
539
+ 'content': html_table
540
+ })
541
+ content = content[:position[0]] + ' '*(position[1]-position[0]) + content[position[1]:] # replace html table with space
542
+
543
+ # extract interline formula
544
+ display_matches = display_reg.finditer(content)
545
+ for match in display_matches:
546
+ matched = match.group(0)
547
+ if matched:
548
+ single_line = ''.join(matched.split())
549
+ position = [match.start(), match.end()]
550
+ # replace $$ with \[\]
551
+ dollar_pattern = re.compile(r'\$\$(.*?)\$\$|\$(.*?)\$|\\\((.*?)\\\)', re.DOTALL)
552
+ sub_match = dollar_pattern.search(single_line)
553
+ if sub_match is None:
554
+ # pass
555
+ content = content[:position[0]] + ' '*(position[1]-position[0]) + content[position[1]:]
556
+ pred_all.append({
557
+ 'category_type': 'equation_isolated',
558
+ 'position': position,
559
+ 'content': single_line
560
+ })
561
+ elif sub_match.group(1):
562
+ single_line = re.sub(dollar_pattern, r'\\[\1\\]', single_line)
563
+ content = content[:position[0]] + ' '*(position[1]-position[0]) + content[position[1]:] # replace equation with space
564
+ pred_all.append({
565
+ 'category_type': 'equation_isolated',
566
+ 'position': position,
567
+ 'content': single_line
568
+ })
569
+ else:
570
+ single_line = re.sub(dollar_pattern, r'\\[\2\3\\]', single_line)
571
+ pred_all.append({
572
+ 'category_type': 'equation_isolated',
573
+ 'position': position,
574
+ 'content': single_line,
575
+ 'fine_category_type': 'equation_inline'
576
+ })
577
+
578
+
579
+ # extract md table with ||
580
+ md_table_mathces = md_table_reg.findall(content+'\n')
581
+ if len(md_table_mathces) >= 2:
582
+ # print("md table found!")
583
+ # print("content:", content)
584
+ content = convert_markdown_to_html(content)
585
+ # print('----------content after converting md table to html:', content)
586
+ html_table_matches = html_table_reg.finditer(content)
587
+ if html_table_matches:
588
+ for match in html_table_matches:
589
+ matched = match.group(0)
590
+ position = [match.start(), match.end()]
591
+ # content = content.replace(match, '')
592
+ # print('content after removing the md table:', content)
593
+ content = content[:position[0]] + ' '*(position[1]-position[0]) + content[position[1]:] # replace md table with space
594
+ pred_all.append({
595
+ 'category_type': 'html_table',
596
+ 'position': position,
597
+ 'content': matched.strip(),
598
+ 'fine_category_type': 'md2html_table'
599
+ })
600
+ # print('---------After md table: \n', content)
601
+
602
+ # extract code blocks
603
+ code_matches = code_block_reg.finditer(content)
604
+ if code_matches:
605
+ for match in code_matches:
606
+ position = [match.start(), match.end()]
607
+ language = match.group(1)
608
+ code = match.group(2).strip()
609
+ # content = content.replace(match.group(0), '')
610
+ content = content[:position[0]] + ' '*(position[1]-position[0]) + content[position[1]:] # replace code block with space
611
+ pred_all.append({
612
+ 'category_type': 'text_all',
613
+ 'position': position,
614
+ 'content': code,
615
+ 'language': language,
616
+ 'fine_category_type': 'code'
617
+ })
618
+
619
+
620
+ # Remove latex style
621
+ content = re.sub(r'\\title\{(.*?)\}', r'\1', content)
622
+ content = re.sub(r'\\title\s*\{\s*(.*?)\s*\}', r'\1', content, flags=re.DOTALL)
623
+ content = re.sub(r'\\text\s*\{\s*(.*?)\s*\}', r'\1', content, flags=re.DOTALL)
624
+ content = re.sub(r'\\section\*?\{(.*?)\}', r'\1', content)
625
+ content = re.sub(r'\\section\*?\{\s*(.*?)\s*\}', r'\1', content, flags=re.DOTALL)
626
+
627
+ # extract texts
628
+ res = content.split('\n\n')
629
+ if len(res) == 1:
630
+ res = content.split('\n') # some models do not use double newlines, so use single newlines to split
631
+
632
+ content_position = 0
633
+ for text in res:
634
+ position = [content_position, content_position+len(text)]
635
+ content_position += len(text)
636
+ text = text.strip()
637
+ text = text.strip('\n')
638
+ # print('ori_text: ', text)
639
+ text = '\n'.join([_.strip() for _ in text.split('\n') if _.strip()]) # avoid some single newline content with many spaces
640
+ # print('after strip text: ', text)
641
+
642
+ if text: # Check if the stripped text is not empty
643
+ if text.startswith('<table') and text.endswith('</table>'):
644
+ pred_all.append({
645
+ 'category_type': 'html_table',
646
+ 'position': position,
647
+ 'content': text,
648
+ })
649
+
650
+ elif text.startswith('$') and text.endswith('$'):
651
+ if text.replace('$', '').strip():
652
+ pred_all.append({
653
+ 'category_type': 'equation_isolated',
654
+ 'position': position,
655
+ 'content': text.strip(),
656
+ })
657
+ else:
658
+ text = text.strip()
659
+ if text:
660
+ pred_all.append({
661
+ 'category_type': 'text_all',
662
+ 'position': position,
663
+ 'content': text,
664
+ 'fine_category_type': 'text_block'
665
+ })
666
+
667
+ pred_dataset = defaultdict(list)
668
+ pred_all = sorted(pred_all, key=lambda x: x['position'][0])
669
+ for item in pred_all:
670
+ pred_dataset[item['category_type']].append(item)
671
+ # pdb.set_trace()
672
+ return pred_dataset
673
+
674
+
675
+ def extract_tex_table(content):
676
+ tables = []
677
+ tables_positions = []
678
+
679
+ pattern = r'\\begin{table}(.*?)\\end{table}'
680
+ for match in re.finditer(pattern, content, re.DOTALL):
681
+ start_pos = match.start()
682
+ end_pos = match.end()
683
+ table_content = match.group(0)
684
+ tables.append(table_content)
685
+ tables_positions.append((start_pos, end_pos))
686
+ content = content[:start_pos] + ' '*(end_pos-start_pos) + content[end_pos:]
687
+
688
+ tabulars, tabular_positions = extract_tabular(content)
689
+ all_tables = tables + tabulars
690
+ all_positions = tables_positions + tabular_positions
691
+
692
+ all_result = sorted([[pos, table]for pos, table in zip(all_positions, all_tables)], key=lambda x: x[0][0])
693
+ all_tables = [x[1] for x in all_result]
694
+ all_positions = [x[0] for x in all_result]
695
+
696
+ return all_tables, all_positions
697
+
698
+
699
+ def extract_html_table(text):
700
+ begin_pattern = r'<table(?:[^>]*)>'
701
+ end_pattern = r'</table>'
702
+
703
+ tabulars = []
704
+ positions = []
705
+ current_pos = 0
706
+ stack = []
707
+
708
+ while current_pos < len(text):
709
+ begin_match = re.search(begin_pattern, text[current_pos:])
710
+ end_match = re.search(end_pattern, text[current_pos:])
711
+
712
+ if not begin_match and not end_match:
713
+ break
714
+
715
+ if begin_match and (not end_match or begin_match.start() < end_match.start()):
716
+ stack.append(current_pos + begin_match.start())
717
+ current_pos += begin_match.start() + len(end_pattern)
718
+ elif end_match:
719
+ if stack:
720
+ start_pos = stack.pop()
721
+ if not stack:
722
+ end_pos = current_pos + end_match.start() + len(end_pattern)
723
+ tabular_code = text[start_pos:end_pos]
724
+ tabulars.append(tabular_code)
725
+ positions.append((start_pos, end_pos))
726
+ current_pos += end_match.start() + len(end_pattern)
727
+ else:
728
+ current_pos += 1
729
+
730
+ if stack:
731
+ new_start = stack[0] + len(begin_pattern)
732
+ new_tabulars, new_positions = extract_html_table(text[new_start:])
733
+ new_positions = [(start + new_start, end + new_start) for start, end in new_positions]
734
+ tabulars.extend(new_tabulars)
735
+ positions.extend(new_positions)
736
+
737
+ return tabulars, positions
738
+
739
+
740
+ def extract_node_content(node):
741
+ """ Recursively extract content from LatexEnvironmentNode and rebuild LaTeX table representation """
742
+ if isinstance(node, LatexCharsNode):
743
+ return node.chars # Use chars attribute
744
+ elif isinstance(node, LatexGroupNode):
745
+ return "{" + "".join(extract_node_content(n) for n in node.nodelist) + "}"
746
+ elif isinstance(node, LatexMacroNode):
747
+ # Extract macro command and its arguments
748
+ macro_content = "\\" + node.macroname
749
+ if node.nodeargs:
750
+ macro_content += "".join([extract_node_content(arg) for arg in node.nodeargs])
751
+ return macro_content
752
+ elif isinstance(node, LatexEnvironmentNode):
753
+ # Extract environment, preserve environment name and arguments
754
+ content = "\\begin{" + node.environmentname + "}"
755
+ if node.nodeargd and node.nodeargd.argnlist:
756
+ # content += "".join("{" + extract_node_content(arg) + "}" for arg in node.nodeargd)
757
+ # content += "".join("{" + extract_node_content(node.nodeargd) + "}")
758
+ content += "{" + extract_node_content(node.nodeargd.argnlist[0]) + "}"
759
+ if node.nodelist:
760
+ content += "".join(extract_node_content(n) for n in node.nodelist)
761
+ content += "\\end{" + node.environmentname + "}"
762
+ return content
763
+ elif isinstance(node, LatexSpecialsNode): # Changed to LatexSpecialsNode
764
+ return node.specials_chars
765
+ else:
766
+ return ""
767
+
768
+ def get_node_end_pos(node):
769
+ """Recursively determine the end position of a node"""
770
+ if hasattr(node, 'nodelist') and node.nodelist:
771
+ # If the node has child nodes, recursively find the end position of the last child node
772
+ return get_node_end_pos(node.nodelist[-1])
773
+ elif hasattr(node, 'pos_end'):
774
+ # If the node has pos_end attribute, return it directly
775
+ return node.pos_end
776
+ else:
777
+ # If there are no child nodes, assume the node ends at the last character of its content
778
+ return node.pos + len(str(node))
779
+
780
+ def remove_tex_table(content):
781
+ tables, positions = extract_tex_table(content)
782
+
783
+ # Delete in reverse order by position to avoid affecting unprocessed start positions
784
+ for start, end in sorted(positions, reverse=True):
785
+ content = content[:start] + content[end:] # Remove table content
786
+
787
+ return content
788
+
789
+
790
+
791
+ def get_pred_category_type(pred_idx, pred_items):
792
+ # if pred_idx:
793
+ if pred_items[pred_idx].get('fine_category_type'):
794
+ pred_pred_category_type = pred_items[pred_idx]['fine_category_type']
795
+ else:
796
+ pred_pred_category_type = pred_items[pred_idx]['category_type']
797
+ # else:
798
+ # pred_pred_category_type = ""
799
+ return pred_pred_category_type
800
+
801
+
802
+ def compute_edit_distance_matrix_new(gt_lines, matched_lines):
803
+ try:
804
+ distance_matrix = np.zeros((len(gt_lines), len(matched_lines)))
805
+ for i, gt_line in enumerate(gt_lines):
806
+ for j, matched_line in enumerate(matched_lines):
807
+ if len(gt_line) == 0 and len(matched_line) == 0:
808
+ distance_matrix[i][j] = 0
809
+ else:
810
+ distance_matrix[i][j] = Levenshtein.distance(gt_line, matched_line) / max(len(matched_line), len(gt_line))
811
+ return distance_matrix
812
+ except ZeroDivisionError:
813
+ #print("ZeroDivisionError occurred. Outputting norm_gt_lines and norm_pred_lines:")
814
+ # print("norm_gt_lines:", gt_lines)
815
+ # print("norm_pred_lines:", matched_lines)
816
+ raise
817
+
818
+ def get_gt_pred_lines(gt_items, pred_items, line_type):
819
+ norm_html_lines = []
820
+ gt_lines = []
821
+ gt_cat_list = []
822
+ for item in gt_items:
823
+ if item.get('fine_category_type'):
824
+ gt_cat_list.append(item['fine_category_type'])
825
+ else:
826
+ gt_cat_list.append(item['category_type'])
827
+ if item.get('content'):
828
+ gt_lines.append(str(item['content']))
829
+ norm_html_lines.append(str(item['content']))
830
+ elif line_type == 'text':
831
+ gt_lines.append(str(item['text']))
832
+ elif line_type == 'html_table':
833
+ gt_lines.append(str(item['html']))
834
+ elif line_type == 'formula':
835
+ gt_lines.append(str(item['latex']))
836
+ elif line_type == 'latex_table':
837
+ gt_lines.append(str(item['latex']))
838
+ norm_html_lines.append(str(item['html']))
839
+
840
+ pred_lines = [str(item['content']) for item in pred_items]
841
+
842
+
843
+ if line_type == 'formula':
844
+ norm_gt_lines = [normalized_formula(_) for _ in gt_lines]
845
+ norm_pred_lines = [normalized_formula(_) for _ in pred_lines]
846
+ elif line_type == 'text':
847
+ # norm_gt_lines = [textblock_with_norm_formula(_) for _ in gt_lines]
848
+ # norm_pred_lines = [textblock_with_norm_formula(_) for _ in pred_lines]
849
+ norm_gt_lines = [clean_string(textblock2unicode(_)) for _ in gt_lines]
850
+ norm_pred_lines = [clean_string(textblock2unicode(_)) for _ in pred_lines]
851
+ # norm_gt_lines = get_norm_text_lines(gt_lines)
852
+ # norm_pred_lines = get_norm_text_lines(pred_lines)
853
+ else:
854
+ norm_gt_lines = gt_lines
855
+ norm_pred_lines = pred_lines
856
+
857
+ if line_type == 'latex_table':
858
+ gt_lines = norm_html_lines
859
+
860
+
861
+ filtered_lists = [(a, b, c) for a, b, c in zip(gt_lines, norm_gt_lines, gt_cat_list) if a and b]
862
+
863
+ # decompress to three lists
864
+ if filtered_lists:
865
+ gt_lines_c, norm_gt_lines_c, gt_cat_list_c = zip(*filtered_lists)
866
+
867
+ # convert to lists
868
+ gt_lines_c = list(gt_lines_c)
869
+ norm_gt_lines_c = list(norm_gt_lines_c)
870
+ gt_cat_list_c = list(gt_cat_list_c)
871
+ else:
872
+ gt_lines_c = []
873
+ norm_gt_lines_c = []
874
+ gt_cat_list_c = []
875
+
876
+ # pred's empty values
877
+ filtered_lists = [(a, b) for a, b in zip(pred_lines, norm_pred_lines) if a and b]
878
+
879
+ # decompress to two lists
880
+ if filtered_lists:
881
+ pred_lines_c, norm_pred_lines_c = zip(*filtered_lists)
882
+
883
+ # convert to lists
884
+ pred_lines_c = list(pred_lines_c)
885
+ norm_pred_lines_c = list(norm_pred_lines_c)
886
+ else:
887
+ pred_lines_c = []
888
+ norm_pred_lines_c = []
889
+
890
+ return gt_lines_c, norm_gt_lines_c, gt_cat_list_c, pred_lines_c, norm_pred_lines_c
891
+ # return gt_lines, norm_gt_lines, gt_cat_list, pred_lines, norm_pred_lines
892
+
893
+
894
+ def match_gt2pred_simple(gt_items, pred_items, line_type, img_name):
895
+
896
+ gt_lines, norm_gt_lines, gt_cat_list, pred_lines, norm_pred_lines = get_gt_pred_lines(gt_items, pred_items, line_type)
897
+
898
+ match_list = []
899
+ if not norm_gt_lines: # not matched pred should be concatenated
900
+ # print("One of the lists is empty. Returning an empty gt result.")
901
+ # for pred_idx in range(len(norm_pred_lines)):
902
+ pred_idx_list = range(len(norm_pred_lines))
903
+ match_list.append({
904
+ 'gt_idx': [""],
905
+ 'gt': "",
906
+ 'pred_idx': pred_idx_list,
907
+ 'pred': ''.join(pred_lines[_] for _ in pred_idx_list),
908
+ 'gt_position': [""],
909
+ 'pred_position': pred_items[pred_idx_list[0]]['position'][0], # get the first pred's position
910
+ 'norm_gt': "",
911
+ 'norm_pred': ''.join(norm_pred_lines[_] for _ in pred_idx_list),
912
+ 'gt_category_type': "",
913
+ 'pred_category_type': get_pred_category_type(pred_idx_list[0], pred_items), # get the first pred's category
914
+ 'gt_attribute': [{}],
915
+ 'edit': 1,
916
+ 'img_id': img_name
917
+ })
918
+ return match_list
919
+ elif not norm_pred_lines: # not matched gt should be separated
920
+ # print("One of the lists is empty. Returning an empty pred result.")
921
+ for gt_idx in range(len(norm_gt_lines)):
922
+ match_list.append({
923
+ 'gt_idx': [gt_idx],
924
+ 'gt': gt_lines[gt_idx],
925
+ 'pred_idx': [""],
926
+ 'pred': "",
927
+ 'gt_position': [gt_items[gt_idx].get('order') if gt_items[gt_idx].get('order') else gt_items[gt_idx].get('position', [""])[0]],
928
+ 'pred_position': "",
929
+ 'norm_gt': norm_gt_lines[gt_idx],
930
+ 'norm_pred': "",
931
+ 'gt_category_type': gt_cat_list[gt_idx],
932
+ 'pred_category_type': "",
933
+ 'gt_attribute': [gt_items[gt_idx].get("attribute", {})],
934
+ 'edit': 1,
935
+ 'img_id': img_name
936
+ })
937
+ return match_list
938
+
939
+ cost_matrix = compute_edit_distance_matrix_new(norm_gt_lines, norm_pred_lines)
940
+
941
+ row_ind, col_ind = linear_sum_assignment(cost_matrix)
942
+
943
+
944
+ for gt_idx in range(len(norm_gt_lines)):
945
+ if gt_idx in row_ind:
946
+ row_i = list(row_ind).index(gt_idx)
947
+ pred_idx = int(col_ind[row_i])
948
+ pred_line = pred_lines[pred_idx]
949
+ norm_pred_line = norm_pred_lines[pred_idx]
950
+ edit = cost_matrix[gt_idx][pred_idx]
951
+ # print('edit_dist', edit)
952
+ # if edit > 0.7:
953
+ # print('! Not match')
954
+ else:
955
+ # print('No match pred')
956
+ pred_idx = ""
957
+ pred_line = ""
958
+ norm_pred_line = ""
959
+ edit = 1
960
+
961
+ match_list.append({
962
+ 'gt_idx': [gt_idx],
963
+ 'gt': gt_lines[gt_idx],
964
+ 'norm_gt': norm_gt_lines[gt_idx],
965
+ 'gt_category_type': gt_cat_list[gt_idx],
966
+ 'gt_position': [gt_items[gt_idx].get('order') if gt_items[gt_idx].get('order') else gt_items[gt_idx].get('position', [""])[0]],
967
+ 'gt_attribute': [gt_items[gt_idx].get("attribute", {})],
968
+ 'pred_idx': [pred_idx],
969
+ 'pred': pred_line,
970
+ 'norm_pred': norm_pred_line,
971
+ 'pred_category_type': get_pred_category_type(pred_idx, pred_items) if pred_idx else "",
972
+ 'pred_position': pred_items[pred_idx]['position'][0] if pred_idx else "",
973
+ 'edit': edit,
974
+ 'img_id': img_name
975
+ })
976
+ # print('-'*10)
977
+ # [([0,1], 0),(2, 1), (1,2)] --> [0,2,1]/[0,1,2]
978
+
979
+ pred_idx_list = [pred_idx for pred_idx in range(len(norm_pred_lines)) if pred_idx not in col_ind] # get not matched preds
980
+ if pred_idx_list: # if there are still remaining pred_idx, concatenate all preds
981
+ match_list.append({
982
+ 'gt_idx': [""],
983
+ 'gt': "",
984
+ 'pred_idx': pred_idx_list,
985
+ 'pred': ''.join(pred_lines[_] for _ in pred_idx_list),
986
+ 'gt_position': [""],
987
+ 'pred_position': pred_items[pred_idx_list[0]]['position'][0], # get the first pred's position
988
+ 'norm_gt': "",
989
+ 'norm_pred': ''.join(norm_pred_lines[_] for _ in pred_idx_list),
990
+ 'gt_category_type': "",
991
+ 'pred_category_type': get_pred_category_type(pred_idx_list[0], pred_items), # get the first pred's category
992
+ 'gt_attribute': [{}],
993
+ 'edit': 1,
994
+ 'img_id': img_name
995
+ })
996
+ return match_list
997
+
998
+
999
+ def match_gt2pred_no_split(gt_items, pred_items, line_type, img_name):
1000
+ # directly concatenate gt and pred by position
1001
+ gt_lines, norm_gt_lines, gt_cat_list, pred_lines, norm_pred_lines = get_gt_pred_lines(gt_items, pred_items, line_type)
1002
+ gt_line_with_position = []
1003
+ for gt_line, norm_gt_line, gt_item in zip(gt_lines, norm_gt_lines, gt_items):
1004
+ gt_position = gt_item['order'] if gt_item.get('order') else gt_item.get('position', [""])[0]
1005
+ if gt_position:
1006
+ gt_line_with_position.append((gt_position, gt_line, norm_gt_line))
1007
+ sorted_gt_lines = sorted(gt_line_with_position, key=lambda x: x[0])
1008
+ gt = '\n\n'.join([_[1] for _ in sorted_gt_lines])
1009
+ norm_gt = '\n\n'.join([_[2] for _ in sorted_gt_lines])
1010
+ pred_line_with_position = [(pred_item['position'], pred_line, pred_norm_line) for pred_line, pred_norm_line, pred_item in zip(pred_lines, norm_pred_lines, pred_items)]
1011
+ sorted_pred_lines = sorted(pred_line_with_position, key=lambda x: x[0])
1012
+ pred = '\n\n'.join([_[1] for _ in sorted_pred_lines])
1013
+ norm_pred = '\n\n'.join([_[2] for _ in sorted_pred_lines])
1014
+ # edit = Levenshtein.distance(norm_gt, norm_pred)/max(len(norm_gt), len(norm_pred))
1015
+ if norm_gt or norm_pred:
1016
+ return [{
1017
+ 'gt_idx': [0],
1018
+ 'gt': gt,
1019
+ 'norm_gt': norm_gt,
1020
+ 'gt_category_type': "text_merge",
1021
+ 'gt_position': [""],
1022
+ 'gt_attribute': [{}],
1023
+ 'pred_idx': [0],
1024
+ 'pred': pred,
1025
+ 'norm_pred': norm_pred,
1026
+ 'pred_category_type': "text_merge",
1027
+ 'pred_position': "",
1028
+ # 'edit': edit,
1029
+ 'img_id': img_name
1030
+ }]
1031
+ else:
1032
+ return []
1033
+
1034
+
1035
+ import copy
1036
+ import pdb
1037
+ from collections import Counter, defaultdict
1038
+
1039
+ import evaluate
1040
+ # from rapidfuzz.distance import Levenshtein
1041
+ import Levenshtein
1042
+ import numpy as np
1043
+ from Levenshtein import distance as Levenshtein_distance
1044
+ from scipy.optimize import linear_sum_assignment
1045
+
1046
+
1047
+ def match_gt2pred_quick(gt_items, pred_items, line_type, img_name):
1048
+
1049
+ gt_lines, norm_gt_lines, gt_cat_list, pred_lines, norm_pred_lines= get_gt_pred_lines(gt_items, pred_items, line_type)
1050
+ all_gt_indices = set(range(len(norm_gt_lines)))
1051
+ all_pred_indices = set(range(len(norm_pred_lines)))
1052
+
1053
+ if not norm_gt_lines:
1054
+ match_list = []
1055
+ for pred_idx in range(len(norm_pred_lines)):
1056
+ match_list.append({
1057
+ 'gt_idx': [""],
1058
+ 'gt': "",
1059
+ 'pred_idx': [pred_idx],
1060
+ 'pred': pred_lines[pred_idx],
1061
+ 'gt_position': "",
1062
+ 'pred_position': pred_items[pred_idx]['position'][0],
1063
+ 'norm_gt': "",
1064
+ 'norm_pred': norm_pred_lines[pred_idx],
1065
+ 'gt_category_type': "",
1066
+ 'pred_category_type': get_pred_category_type(pred_idx, pred_items),
1067
+ 'gt_attribute': [{}],
1068
+ 'edit': 1,
1069
+ 'img_id': img_name
1070
+ })
1071
+ return match_list
1072
+ elif not norm_pred_lines:
1073
+ match_list = []
1074
+ for gt_idx in range(len(norm_gt_lines)):
1075
+ match_list.append({
1076
+ 'gt_idx': [gt_idx],
1077
+ 'gt': gt_lines[gt_idx],
1078
+ 'pred_idx': [""],
1079
+ 'pred': "",
1080
+ 'gt_position': [gt_items[gt_idx].get('order') if gt_items[gt_idx].get('order') else gt_items[gt_idx].get('position', [""])[0]],
1081
+ 'pred_position': "",
1082
+ 'norm_gt': norm_gt_lines[gt_idx],
1083
+ 'norm_pred': "",
1084
+ 'gt_category_type': gt_cat_list[gt_idx],
1085
+ 'pred_category_type': "",
1086
+ 'gt_attribute': [gt_items[gt_idx].get("attribute", {})],
1087
+ 'edit': 1,
1088
+ 'img_id': img_name
1089
+ })
1090
+ return match_list
1091
+ elif len(norm_gt_lines) == 1 and len(norm_pred_lines) == 1:
1092
+ edit_distance = Levenshtein_distance(norm_gt_lines[0], norm_pred_lines[0])
1093
+ normalized_edit_distance = edit_distance / max(len(norm_gt_lines[0]), len(norm_pred_lines[0]))
1094
+ return [{
1095
+ 'gt_idx': [0],
1096
+ 'gt': gt_lines[0],
1097
+ 'pred_idx': [0],
1098
+ 'pred': pred_lines[0],
1099
+ 'gt_position': [gt_items[0].get('order') if gt_items[0].get('order') else gt_items[0].get('position', [""])[0]],
1100
+ 'pred_position': pred_items[0]['position'][0],
1101
+ 'norm_gt': norm_gt_lines[0],
1102
+ 'norm_pred': norm_pred_lines[0],
1103
+ 'gt_category_type': gt_cat_list[0],
1104
+ 'pred_category_type': get_pred_category_type(0, pred_items),
1105
+ 'gt_attribute': [gt_items[0].get("attribute", {})],
1106
+ 'edit': normalized_edit_distance,
1107
+ 'img_id': img_name
1108
+ }]
1109
+
1110
+ cost_matrix = compute_edit_distance_matrix_new(norm_gt_lines, norm_pred_lines)
1111
+
1112
+ matched_col_idx, row_ind, cost_list = cal_final_match(cost_matrix, norm_gt_lines, norm_pred_lines)
1113
+
1114
+ gt_lens_dict, pred_lens_dict = initialize_indices(norm_gt_lines, norm_pred_lines)
1115
+
1116
+ matches, unmatched_gt_indices, unmatched_pred_indices = process_matches(matched_col_idx, row_ind, cost_list, norm_gt_lines, norm_pred_lines, pred_lines)
1117
+
1118
+ matching_dict = fuzzy_match_unmatched_items(unmatched_gt_indices, norm_gt_lines, norm_pred_lines)
1119
+
1120
+ final_matches = merge_matches(matches, matching_dict)
1121
+
1122
+ recalculate_edit_distances(final_matches, gt_lens_dict, norm_gt_lines, norm_pred_lines)
1123
+
1124
+ converted_results = convert_final_matches(final_matches, norm_gt_lines, norm_pred_lines)
1125
+
1126
+ merged_results = merge_duplicates_add_unmatched(converted_results, norm_gt_lines, norm_pred_lines, gt_lines, pred_lines, all_gt_indices, all_pred_indices)
1127
+
1128
+ for entry in merged_results:
1129
+ entry['gt_idx'] = [entry['gt_idx']] if not isinstance(entry['gt_idx'], list) else entry['gt_idx']
1130
+ entry['pred_idx'] = [entry['pred_idx']] if not isinstance(entry['pred_idx'], list) else entry['pred_idx']
1131
+ entry['gt_position'] = [gt_items[_].get('order') if gt_items[_].get('order') else gt_items[_].get('position', [""])[0] for _ in entry['gt_idx']] if entry['gt_idx'] != [""] else [""]
1132
+ entry['pred_position'] = pred_items[entry['pred_idx'][0]]['position'][0] if entry['pred_idx'] != [""] else ""
1133
+ entry['gt'] = ''.join([gt_lines[_] for _ in entry['gt_idx']]) if entry['gt_idx'] != [""] else ""
1134
+ entry['pred'] = ''.join([pred_lines[_] for _ in entry['pred_idx']]) if entry['pred_idx'] != [""] else ""
1135
+ entry['norm_gt'] = ''.join([norm_gt_lines[_] for _ in entry['gt_idx']]) if entry['gt_idx'] != [""] else ""
1136
+ entry['norm_pred'] = ''.join([norm_pred_lines[_] for _ in entry['pred_idx']]) if entry['pred_idx'] != [""] else ""
1137
+
1138
+ if entry['gt_idx'] != [""]:
1139
+ ignore_type = ['figure_caption', 'figure_footnote', 'table_caption', 'table_footnote', 'code_algorithm', 'code_algorithm_caption', 'header', 'footer', 'page_footnote', 'page_number', 'equation_caption']
1140
+ gt_cagegory_clean = [gt_cat_list[_] for _ in entry['gt_idx'] if gt_cat_list[_] not in ignore_type]
1141
+ if gt_cagegory_clean:
1142
+ entry['gt_category_type'] = Counter(gt_cagegory_clean).most_common(1)[0][0]
1143
+ else:
1144
+ entry['gt_category_type'] = Counter([gt_cat_list[_] for _ in entry['gt_idx']]).most_common(1)[0][0]
1145
+ else:
1146
+ entry['gt_category_type'] = ""
1147
+ entry['pred_category_type'] = get_pred_category_type(entry['pred_idx'][0], pred_items) if entry['pred_idx'] != [""] else ""
1148
+ entry['gt_attribute'] = [gt_items[_].get("attribute", {}) for _ in entry['gt_idx']] if entry['gt_idx'] != [""] else [{}]
1149
+ entry['img_id'] = img_name
1150
+
1151
+ return merged_results
1152
+
1153
+
1154
+ def merge_duplicates_add_unmatched(converted_results, norm_gt_lines, norm_pred_lines, gt_lines, pred_lines, all_gt_indices, all_pred_indices):
1155
+ merged_results = []
1156
+ processed_pred = set()
1157
+ processed_gt = set()
1158
+
1159
+ for entry in converted_results:
1160
+ pred_idx = tuple(entry['pred_idx']) if isinstance(entry['pred_idx'], list) else (entry['pred_idx'],)
1161
+ if pred_idx not in processed_pred and pred_idx != ("",):
1162
+ merged_entry = {
1163
+ 'gt_idx': [entry['gt_idx']],
1164
+ 'gt': entry['gt'],
1165
+ 'pred_idx': entry['pred_idx'],
1166
+ 'pred': entry['pred'],
1167
+ 'edit': entry['edit']
1168
+ }
1169
+ for other_entry in converted_results:
1170
+ other_pred_idx = tuple(other_entry['pred_idx']) if isinstance(other_entry['pred_idx'], list) else (other_entry['pred_idx'],)
1171
+ if other_pred_idx == pred_idx and other_entry is not entry:
1172
+ merged_entry['gt_idx'].append(other_entry['gt_idx'])
1173
+ merged_entry['gt'] += other_entry['gt']
1174
+ processed_gt.add(other_entry['gt_idx'])
1175
+ merged_results.append(merged_entry)
1176
+ processed_pred.add(pred_idx)
1177
+ processed_gt.add(entry['gt_idx'])
1178
+
1179
+ for entry in converted_results:
1180
+ if entry['gt_idx'] not in processed_gt:
1181
+ merged_results.append(entry)
1182
+
1183
+ for gt_idx in range(len(norm_gt_lines)):
1184
+ if gt_idx not in processed_gt:
1185
+ merged_results.append({
1186
+ 'gt_idx': [gt_idx],
1187
+ 'gt': gt_lines[gt_idx],
1188
+ 'pred_idx': [""],
1189
+ 'pred': "",
1190
+ 'edit': 1
1191
+ })
1192
+ return merged_results
1193
+
1194
+
1195
+
1196
+
1197
+ def formula_format(formula_matches, img_name):
1198
+ return [
1199
+ {
1200
+ "gt": item["gt"],
1201
+ "pred": item["pred"],
1202
+ "img_id": f"{img_name}_{i}"
1203
+ }
1204
+ for i, item in enumerate(formula_matches)
1205
+ ]
1206
+
1207
+
1208
+ def merge_lists_with_sublists(main_list, sub_lists):
1209
+ main_list_final = list(copy.deepcopy(main_list))
1210
+ for sub_list in sub_lists:
1211
+ pop_idx = main_list_final.index(sub_list[0])
1212
+ for _ in sub_list:
1213
+ main_list_final.pop(pop_idx)
1214
+ main_list_final.insert(pop_idx, sub_list)
1215
+ return main_list_final
1216
+
1217
+
1218
+ def sub_pred_fuzzy_matching(gt, pred):
1219
+
1220
+ min_d = float('inf')
1221
+ # pos = -1
1222
+
1223
+ gt_len = len(gt)
1224
+ pred_len = len(pred)
1225
+
1226
+ if gt_len >= pred_len and pred_len > 0:
1227
+ for i in range(gt_len - pred_len + 1):
1228
+ sub = gt[i:i + pred_len]
1229
+ dist = Levenshtein_distance(sub, pred)/pred_len
1230
+ if dist < min_d:
1231
+ min_d = dist
1232
+ pos = i
1233
+
1234
+ return min_d
1235
+ else:
1236
+ return False
1237
+
1238
+ def sub_gt_fuzzy_matching(pred, gt):
1239
+
1240
+ min_d = float('inf')
1241
+ pos = ""
1242
+ matched_sub = ""
1243
+ gt_len = len(gt)
1244
+ pred_len = len(pred)
1245
+
1246
+ if pred_len >= gt_len and gt_len > 0:
1247
+ for i in range(pred_len - gt_len + 1):
1248
+ sub = pred[i:i + gt_len]
1249
+ dist = Levenshtein.distance(sub, gt) /gt_len
1250
+ if dist < min_d:
1251
+ min_d = dist
1252
+ pos = i
1253
+ matched_sub = sub
1254
+ return min_d, pos, gt_len, matched_sub
1255
+ else:
1256
+ return 1, "", gt_len, ""
1257
+
1258
+
1259
+ def get_final_subset(subset_certain, subset_certain_cost):
1260
+ if not subset_certain or not subset_certain_cost:
1261
+ return []
1262
+
1263
+ subset_turple = sorted([(a, b) for a, b in zip(subset_certain, subset_certain_cost)], key=lambda x: x[0][0])
1264
+
1265
+ group_list = defaultdict(list)
1266
+ group_idx = 0
1267
+ group_list[group_idx].append(subset_turple[0])
1268
+
1269
+ for item in subset_turple[1:]:
1270
+ overlap_flag = False
1271
+ for subset in group_list[group_idx]:
1272
+ for idx in item[0]:
1273
+ if idx in subset[0]:
1274
+ overlap_flag = True
1275
+ break
1276
+ if overlap_flag:
1277
+ break
1278
+ if overlap_flag:
1279
+ group_list[group_idx].append(item)
1280
+ else:
1281
+ group_idx += 1
1282
+ group_list[group_idx].append(item)
1283
+
1284
+ final_subset = []
1285
+ for _, group in group_list.items():
1286
+ if len(group) == 1:
1287
+ final_subset.append(group[0][0])
1288
+ else:
1289
+ path_dict = defaultdict(list)
1290
+ path_idx = 0
1291
+ path_dict[path_idx].append(group[0])
1292
+
1293
+ for subset in group[1:]:
1294
+ new_path = True
1295
+ for path_idx_s, path_items in path_dict.items():
1296
+ is_dup = False
1297
+ is_same = False
1298
+ for path_item in path_items:
1299
+ if path_item[0] == subset[0]:
1300
+ is_dup = True
1301
+ is_same = True
1302
+ if path_item[1] > subset[1]:
1303
+ path_dict[path_idx_s].pop(path_dict[path_idx_s].index(path_item))
1304
+ path_dict[path_idx_s].append(subset)
1305
+ else:
1306
+ for num_1 in path_item[0]:
1307
+ for num_2 in subset[0]:
1308
+ if num_1 == num_2:
1309
+ is_dup = True
1310
+ if not is_dup:
1311
+ path_dict[path_idx_s].append(subset)
1312
+ new_path = False
1313
+ if is_same:
1314
+ new_path = False
1315
+ if new_path:
1316
+ path_idx = len(path_dict.keys())
1317
+ path_dict[path_idx].append(subset)
1318
+
1319
+ saved_cost = float('inf')
1320
+ saved_subset = []
1321
+ for path_idx, path in path_dict.items():
1322
+ avg_cost = sum([i[1] for i in path]) / len(path)
1323
+ if avg_cost < saved_cost:
1324
+ saved_subset = [i[0] for i in path]
1325
+ saved_cost = avg_cost
1326
+
1327
+ final_subset.extend(saved_subset)
1328
+
1329
+ return final_subset
1330
+
1331
+ def judge_pred_merge(gt_list, pred_list, threshold=0.6):
1332
+ if len(pred_list) == 1:
1333
+ return False, False
1334
+
1335
+ cur_pred = ' '.join(pred_list[:-1])
1336
+ merged_pred = ' '.join(pred_list)
1337
+
1338
+ cur_dist = Levenshtein.distance(gt_list[0], cur_pred) / max(len(gt_list[0]), len(cur_pred))
1339
+ merged_dist = Levenshtein.distance(gt_list[0], merged_pred) / max(len(gt_list[0]), len(merged_pred))
1340
+
1341
+ if merged_dist > cur_dist:
1342
+ return False, False
1343
+
1344
+ cur_fuzzy_dists = [sub_pred_fuzzy_matching(gt_list[0], cur_pred) for cur_pred in pred_list[:-1]]
1345
+ if any(dist is False or dist > threshold for dist in cur_fuzzy_dists):
1346
+ return False, False
1347
+
1348
+ add_fuzzy_dist = sub_pred_fuzzy_matching(gt_list[0], pred_list[-1])
1349
+ if add_fuzzy_dist is False:
1350
+ return False, False
1351
+
1352
+ merged_pred_flag = add_fuzzy_dist < threshold
1353
+ continue_flag = len(merged_pred) <= len(gt_list[0])
1354
+
1355
+ return merged_pred_flag, continue_flag
1356
+
1357
+ def deal_with_truncated(cost_matrix, norm_gt_lines, norm_pred_lines):
1358
+ matched_first = np.argwhere(cost_matrix < 0.25)
1359
+ masked_gt_idx = [i[0] for i in matched_first]
1360
+ unmasked_gt_idx = [i for i in range(cost_matrix.shape[0]) if i not in masked_gt_idx]
1361
+ masked_pred_idx = [i[1] for i in matched_first]
1362
+ unmasked_pred_idx = [i for i in range(cost_matrix.shape[1]) if i not in masked_pred_idx]
1363
+
1364
+ merges_gt_dict = {}
1365
+ merges_pred_dict = {}
1366
+ merged_gt_subsets = []
1367
+
1368
+ for gt_idx in unmasked_gt_idx:
1369
+ check_merge_subset = []
1370
+ merged_dist = []
1371
+
1372
+ for pred_idx in unmasked_pred_idx:
1373
+ step = 1
1374
+ merged_pred = [norm_pred_lines[pred_idx]]
1375
+
1376
+ while True:
1377
+ if pred_idx + step in masked_pred_idx or pred_idx + step >= len(norm_pred_lines):
1378
+ break
1379
+ else:
1380
+ merged_pred.append(norm_pred_lines[pred_idx + step])
1381
+ merged_pred_flag, continue_flag = judge_pred_merge([norm_gt_lines[gt_idx]], merged_pred)
1382
+ if not merged_pred_flag:
1383
+ break
1384
+ else:
1385
+ step += 1
1386
+ if not continue_flag:
1387
+ break
1388
+
1389
+ check_merge_subset.append(list(range(pred_idx, pred_idx + step)))
1390
+ matched_line = ' '.join([norm_pred_lines[i] for i in range(pred_idx, pred_idx + step)])
1391
+ dist = Levenshtein_distance(norm_gt_lines[gt_idx], matched_line) / max(len(matched_line), len(norm_gt_lines[gt_idx]))
1392
+ merged_dist.append(dist)
1393
+
1394
+ if not merged_dist:
1395
+ subset_certain = []
1396
+ min_cost_idx = ""
1397
+ min_cost = float('inf')
1398
+ else:
1399
+ min_cost = min(merged_dist)
1400
+ min_cost_idx = merged_dist.index(min_cost)
1401
+ subset_certain = check_merge_subset[min_cost_idx]
1402
+
1403
+ merges_gt_dict[gt_idx] = {
1404
+ 'merge_subset': check_merge_subset,
1405
+ 'merged_cost': merged_dist,
1406
+ 'min_cost_idx': min_cost_idx,
1407
+ 'subset_certain': subset_certain,
1408
+ 'min_cost': min_cost
1409
+ }
1410
+
1411
+ subset_certain = [merges_gt_dict[gt_idx]['subset_certain'] for gt_idx in unmasked_gt_idx if merges_gt_dict[gt_idx]['subset_certain']]
1412
+ subset_certain_cost = [merges_gt_dict[gt_idx]['min_cost'] for gt_idx in unmasked_gt_idx if merges_gt_dict[gt_idx]['subset_certain']]
1413
+
1414
+ subset_certain_final = get_final_subset(subset_certain, subset_certain_cost)
1415
+
1416
+ if not subset_certain_final:
1417
+ return cost_matrix, norm_pred_lines, range(len(norm_pred_lines))
1418
+
1419
+ final_pred_idx_list = merge_lists_with_sublists(range(len(norm_pred_lines)), subset_certain_final)
1420
+ final_norm_pred_lines = [' '.join(norm_pred_lines[idx_list[0]:idx_list[-1]+1]) if isinstance(idx_list, list) else norm_pred_lines[idx_list] for idx_list in final_pred_idx_list]
1421
+
1422
+ new_cost_matrix = compute_edit_distance_matrix_new(norm_gt_lines, final_norm_pred_lines)
1423
+
1424
+ return new_cost_matrix, final_norm_pred_lines, final_pred_idx_list
1425
+
1426
+ def cal_move_dist(gt, pred):
1427
+ assert len(gt) == len(pred), 'Not right length'
1428
+ step = 0
1429
+ for i, gt_c in enumerate(gt):
1430
+ if gt_c != pred[i]:
1431
+ step += abs(i - pred.index(gt_c))
1432
+ pred[i], pred[pred.index(gt_c)] = pred[pred.index(gt_c)], pred[i]
1433
+ return step / len(gt)
1434
+
1435
+ def cal_final_match(cost_matrix, norm_gt_lines, norm_pred_lines):
1436
+ min_indice = cost_matrix.argmax(axis=1)
1437
+
1438
+ new_cost_matrix, final_norm_pred_lines, final_pred_idx_list = deal_with_truncated(cost_matrix, norm_gt_lines, norm_pred_lines)
1439
+
1440
+ row_ind, col_ind = linear_sum_assignment(new_cost_matrix)
1441
+
1442
+ cost_list = [new_cost_matrix[r][c] for r, c in zip(row_ind, col_ind)]
1443
+ matched_col_idx = [final_pred_idx_list[i] for i in col_ind]
1444
+
1445
+ return matched_col_idx, row_ind, cost_list
1446
+
1447
+ def initialize_indices(norm_gt_lines, norm_pred_lines):
1448
+ gt_lens_dict = {idx: len(gt_line) for idx, gt_line in enumerate(norm_gt_lines)}
1449
+ pred_lens_dict = {idx: len(pred_line) for idx, pred_line in enumerate(norm_pred_lines)}
1450
+ return gt_lens_dict, pred_lens_dict
1451
+
1452
+ def process_matches(matched_col_idx, row_ind, cost_list, norm_gt_lines, norm_pred_lines, pred_lines):
1453
+ matches = {}
1454
+ unmatched_gt_indices = []
1455
+ unmatched_pred_indices = []
1456
+
1457
+ for i in range(len(norm_gt_lines)):
1458
+ if i in row_ind:
1459
+ idx = list(row_ind).index(i)
1460
+ pred_idx = matched_col_idx[idx]
1461
+
1462
+ if pred_idx is None or (isinstance(pred_idx, list) and None in pred_idx):
1463
+ unmatched_pred_indices.append(pred_idx)
1464
+ continue
1465
+
1466
+ if isinstance(pred_idx, list):
1467
+ pred_line = ' | '.join(norm_pred_lines[pred_idx[0]:pred_idx[-1]+1])
1468
+ ori_pred_line = ' | '.join(pred_lines[pred_idx[0]:pred_idx[-1]+1])
1469
+ matched_pred_indices_range = list(range(pred_idx[0], pred_idx[-1]+1))
1470
+ else:
1471
+ pred_line = norm_pred_lines[pred_idx]
1472
+ ori_pred_line = pred_lines[pred_idx]
1473
+ matched_pred_indices_range = [pred_idx]
1474
+
1475
+ edit = cost_list[idx]
1476
+
1477
+ if edit > 0.7:
1478
+ unmatched_pred_indices.extend(matched_pred_indices_range)
1479
+ unmatched_gt_indices.append(i)
1480
+ else:
1481
+ matches[i] = {
1482
+ 'pred_indices': matched_pred_indices_range,
1483
+ 'edit_distance': edit,
1484
+ }
1485
+ for matched_pred_idx in matched_pred_indices_range:
1486
+ if matched_pred_idx in unmatched_pred_indices:
1487
+ unmatched_pred_indices.remove(matched_pred_idx)
1488
+ else:
1489
+ unmatched_gt_indices.append(i)
1490
+
1491
+ return matches, unmatched_gt_indices, unmatched_pred_indices
1492
+
1493
+ def fuzzy_match_unmatched_items(unmatched_gt_indices, norm_gt_lines, norm_pred_lines):
1494
+ matching_dict = {}
1495
+
1496
+ for pred_idx, pred_content in enumerate(norm_pred_lines):
1497
+ if isinstance(pred_idx, list):
1498
+ continue
1499
+
1500
+ matching_indices = []
1501
+
1502
+ for unmatched_gt_idx in unmatched_gt_indices:
1503
+ gt_content = norm_gt_lines[unmatched_gt_idx]
1504
+ cur_fuzzy_dist_unmatch, cur_pos, gt_lens, matched_field = sub_gt_fuzzy_matching(pred_content, gt_content)
1505
+ if cur_fuzzy_dist_unmatch < 0.4:
1506
+ matching_indices.append(unmatched_gt_idx)
1507
+
1508
+ if matching_indices:
1509
+ matching_dict[pred_idx] = matching_indices
1510
+
1511
+ return matching_dict
1512
+
1513
+ def merge_matches(matches, matching_dict):
1514
+ final_matches = {}
1515
+ processed_gt_indices = set()
1516
+
1517
+ for gt_idx, match_info in matches.items():
1518
+ pred_indices = match_info['pred_indices']
1519
+ edit_distance = match_info['edit_distance']
1520
+
1521
+ pred_key = tuple(sorted(pred_indices))
1522
+
1523
+ if pred_key in final_matches:
1524
+ if gt_idx not in processed_gt_indices:
1525
+ final_matches[pred_key]['gt_indices'].append(gt_idx)
1526
+ processed_gt_indices.add(gt_idx)
1527
+ else:
1528
+ final_matches[pred_key] = {
1529
+ 'gt_indices': [gt_idx],
1530
+ 'edit_distance': edit_distance
1531
+ }
1532
+ processed_gt_indices.add(gt_idx)
1533
+
1534
+ for pred_idx, gt_indices in matching_dict.items():
1535
+ pred_key = (pred_idx,) if not isinstance(pred_idx, (list, tuple)) else tuple(sorted(pred_idx))
1536
+
1537
+ if pred_key in final_matches:
1538
+ for gt_idx in gt_indices:
1539
+ if gt_idx not in processed_gt_indices:
1540
+ final_matches[pred_key]['gt_indices'].append(gt_idx)
1541
+ processed_gt_indices.add(gt_idx)
1542
+ else:
1543
+ final_matches[pred_key] = {
1544
+ 'gt_indices': [gt_idx for gt_idx in gt_indices if gt_idx not in processed_gt_indices],
1545
+ 'edit_distance': None
1546
+ }
1547
+ processed_gt_indices.update(final_matches[pred_key]['gt_indices'])
1548
+
1549
+ return final_matches
1550
+
1551
+
1552
+
1553
+ def recalculate_edit_distances(final_matches, gt_lens_dict, norm_gt_lines, norm_pred_lines):
1554
+ for pred_key, info in final_matches.items():
1555
+ gt_indices = sorted(set(info['gt_indices']))
1556
+
1557
+ if not gt_indices:
1558
+ info['edit_distance'] = 1
1559
+ continue
1560
+
1561
+ if len(gt_indices) > 1:
1562
+ merged_gt_content = ''.join(norm_gt_lines[gt_idx] for gt_idx in gt_indices)
1563
+ pred_content = norm_pred_lines[pred_key[0]] if isinstance(pred_key[0], int) else ''
1564
+
1565
+ try:
1566
+ edit_distance = Levenshtein_distance(merged_gt_content, pred_content)
1567
+ normalized_edit_distance = edit_distance / max(len(merged_gt_content), len(pred_content))
1568
+ except ZeroDivisionError:
1569
+ normalized_edit_distance = 1
1570
+
1571
+ info['edit_distance'] = normalized_edit_distance
1572
+ else:
1573
+ gt_idx = gt_indices[0]
1574
+ pred_content = ' '.join(norm_pred_lines[pred_idx] for pred_idx in pred_key if isinstance(pred_idx, int))
1575
+
1576
+ try:
1577
+ edit_distance = Levenshtein_distance(norm_gt_lines[gt_idx], pred_content)
1578
+ normalized_edit_distance = edit_distance / max(len(norm_gt_lines[gt_idx]), len(pred_content))
1579
+ except ZeroDivisionError:
1580
+ normalized_edit_distance = 1
1581
+
1582
+ info['edit_distance'] = normalized_edit_distance
1583
+ info['pred_content'] = pred_content
1584
+
1585
+
1586
+ def convert_final_matches(final_matches, norm_gt_lines, norm_pred_lines):
1587
+ converted_results = []
1588
+
1589
+ all_gt_indices = set(range(len(norm_gt_lines)))
1590
+ all_pred_indices = set(range(len(norm_pred_lines)))
1591
+
1592
+ for pred_key, info in final_matches.items():
1593
+ pred_content = ' '.join(norm_pred_lines[pred_idx] for pred_idx in pred_key if isinstance(pred_idx, int))
1594
+
1595
+ for gt_idx in sorted(set(info['gt_indices'])):
1596
+ result_entry = {
1597
+ 'gt_idx': int(gt_idx),
1598
+ 'gt': norm_gt_lines[gt_idx],
1599
+ 'pred_idx': list(pred_key),
1600
+ 'pred': pred_content,
1601
+ 'edit': info['edit_distance']
1602
+ }
1603
+ converted_results.append(result_entry)
1604
+
1605
+ matched_gt_indices = set().union(*[set(info['gt_indices']) for info in final_matches.values()])
1606
+ unmatched_gt_indices = all_gt_indices - matched_gt_indices
1607
+ matched_pred_indices = set(idx for pred_key in final_matches.keys() for idx in pred_key if isinstance(idx, int))
1608
+ unmatched_pred_indices = all_pred_indices - matched_pred_indices
1609
+
1610
+ if unmatched_pred_indices:
1611
+ if unmatched_gt_indices:
1612
+ distance_matrix = [
1613
+ [Levenshtein_distance(norm_gt_lines[gt_idx], norm_pred_lines[pred_idx]) for pred_idx in unmatched_pred_indices]
1614
+ for gt_idx in unmatched_gt_indices
1615
+ ]
1616
+
1617
+ row_ind, col_ind = linear_sum_assignment(distance_matrix)
1618
+
1619
+ for i, j in zip(row_ind, col_ind):
1620
+ gt_idx = list(unmatched_gt_indices)[i]
1621
+ pred_idx = list(unmatched_pred_indices)[j]
1622
+ result_entry = {
1623
+ 'gt_idx': int(gt_idx),
1624
+ 'gt': norm_gt_lines[gt_idx],
1625
+ 'pred_idx': [pred_idx],
1626
+ 'pred': norm_pred_lines[pred_idx],
1627
+ 'edit': 1
1628
+ }
1629
+ converted_results.append(result_entry)
1630
+
1631
+ matched_gt_indices.update(list(unmatched_gt_indices)[i] for i in row_ind)
1632
+ else:
1633
+ result_entry = {
1634
+ 'gt_idx': "",
1635
+ 'gt': '',
1636
+ 'pred_idx': list(unmatched_pred_indices),
1637
+ 'pred': ' '.join(norm_pred_lines[pred_idx] for pred_idx in unmatched_pred_indices),
1638
+ 'edit': 1
1639
+ }
1640
+ converted_results.append(result_entry)
1641
+ else:
1642
+ for gt_idx in unmatched_gt_indices:
1643
+ result_entry = {
1644
+ 'gt_idx': int(gt_idx),
1645
+ 'gt': norm_gt_lines[gt_idx],
1646
+ 'pred_idx': "",
1647
+ 'pred': '',
1648
+ 'edit': 1
1649
+ }
1650
+ converted_results.append(result_entry)
1651
+
1652
+ return converted_results
1653
+
1654
+ import json
1655
+
1656
+
1657
+ def read_md_file(filepath):
1658
+ with open(filepath, 'r', encoding='utf-8') as file:
1659
+ content = file.read()
1660
+
1661
+ return content
1662
+
1663
+ def save_paired_result(preds, gts, save_path):
1664
+ save_result = []
1665
+ formula_id = 0
1666
+ for gt, pred in zip(gts, preds):
1667
+ save_result.append({
1668
+ "gt": gt,
1669
+ "pred": pred,
1670
+ "img_id": formula_id
1671
+ })
1672
+ formula_id += 1
1673
+ with open(save_path, 'w', encoding='utf-8') as f:
1674
+ json.dump(save_result, f, indent=4, ensure_ascii=False)
1675
+
1676
+
1677
+ import os
1678
+ import re
1679
+
1680
+ import matplotlib.font_manager as fm
1681
+ import matplotlib.pyplot as plt
1682
+ import numpy as np
1683
+
1684
+ font = fm.FontProperties(fname=r'font/SimHei.ttf')
1685
+
1686
+
1687
+ def print_aligned_dict(data):
1688
+ # Find the maximum length of all keys
1689
+ max_key_length = max(len(key) for key in data['testcase1'])
1690
+
1691
+ # Print header
1692
+ print(f"{' ' * (max_key_length + 4)}", end="")
1693
+ for key in data:
1694
+ print(f"{key:>{max_key_length}}", end="")
1695
+ print()
1696
+
1697
+ # Print dictionary content
1698
+ for subkey in data['testcase1']:
1699
+ print(f"{subkey:<{max_key_length + 4}}", end="")
1700
+ for key in data:
1701
+ print(f"{data[key][subkey]:>{max_key_length}}", end="")
1702
+ print()
1703
+ def create_dict_from_folders(directory):
1704
+ body = {}
1705
+ for folder_name in os.listdir(directory):
1706
+ folder_path = os.path.join(directory, folder_name)
1707
+ if os.path.isdir(folder_path):
1708
+ body[folder_name] = {}
1709
+ return body
1710
+
1711
+
1712
+ def create_radar_chart(df, title, filename):
1713
+ labels = df.columns
1714
+
1715
+ # Calculate angles
1716
+ angles = np.linspace(0, 2 * np.pi, len(labels), endpoint=False).tolist()
1717
+ angles += angles[:1]
1718
+
1719
+ # Initialize radar chart
1720
+ fig, ax = plt.subplots(figsize=(10, 6), subplot_kw=dict(polar=True), dpi=200)
1721
+ # ax.spines['polar'].set_visible(False)
1722
+
1723
+ # Draw radar chart for each dataset
1724
+ for index, row in df.iterrows():
1725
+ values = row.tolist()
1726
+ values += values[:1]
1727
+ ax.fill(angles, values, alpha=0.1)
1728
+ ax.plot(angles, values, label=index)
1729
+
1730
+ # Add percentage labels next to each data point
1731
+ for angle, value in zip(angles, values):
1732
+ ax.text(angle, value, '{:.1%}'.format(value), ha='center', va='center', fontsize=7, alpha=0.7)
1733
+
1734
+ # Set labels
1735
+ ax.set_yticklabels([])
1736
+ ax.set_xticks(angles[:-1])
1737
+ ax.set_xticklabels(labels, fontproperties=font)
1738
+ ax.spines['polar'].set_visible(False) # Hide the outermost circle
1739
+ ax.grid(False)
1740
+ for j in np.arange(0, 1.2, 0.2):
1741
+ ax.plot(angles, len(values) * [j], '-.', lw=0.5, color='black', alpha=0.5)
1742
+ for j in range(len(values)):
1743
+ ax.plot([angles[j], angles[j]], [0, 1], '-.', lw=0.5, color='black', alpha=0.5)
1744
+
1745
+ # Add title and legend
1746
+ plt.legend(loc='upper right', bbox_to_anchor=(0.1, 0.1))
1747
+
1748
+ ax.tick_params(pad=30)
1749
+ ax.set_theta_zero_location('N')
1750
+ # Save chart to file
1751
+ plt.savefig(filename)
1752
+
1753
+ # The function is from https://github.com/intsig-textin/markdown_tester
1754
+ def markdown_to_html(markdown_table):
1755
+ rows = [row.strip() for row in markdown_table.strip().split('\n')]
1756
+ num_columns = len(rows[0].split('|')) - 2
1757
+
1758
+ html_table = '<table>\n <thead>\n <tr>\n'
1759
+
1760
+ header_cells = [cell.strip() for cell in rows[0].split('|')[1:-1]]
1761
+ for cell in header_cells:
1762
+ html_table += f' <th>{cell}</th>\n'
1763
+ html_table += ' </tr>\n </thead>\n <tbody>\n'
1764
+
1765
+ for row in rows[2:]:
1766
+ cells = [cell.strip() for cell in row.split('|')[1:-1]]
1767
+ html_table += ' <tr>\n'
1768
+ for cell in cells:
1769
+ html_table += f' <td>{cell}</td>\n'
1770
+ html_table += ' </tr>\n'
1771
+
1772
+ html_table += ' </tbody>\n</table>\n'
1773
+ return html_table
1774
+ def convert_markdown_to_html(self, markdown_content, md_type):
1775
+ # Define a regex pattern to find Markdown tables with newlines
1776
+ markdown_content = markdown_content.replace('\r', '')
1777
+ pattern = re.compile(r'\|\s*.*?\s*\|\n', re.DOTALL)
1778
+
1779
+ # Find all matches in the Markdown content
1780
+ matches = pattern.findall(markdown_content)
1781
+ for match in matches:
1782
+ html_table = markdown_to_html(match)
1783
+ markdown_content = markdown_content.replace(match, html_table, 1) # Only replace the first occurrence
1784
+ res_html = convert_table(replace_table_with_placeholder(markdown_content))
1785
+
1786
+ return res_html
1787
+ def convert_table_str(s):
1788
+ s = re.sub(r'<table.*?>','<table>',s)
1789
+ s = re.sub(r'<th','<td',s)
1790
+ s = re.sub(r'</th>','</td>',s)
1791
+ # s = re.sub(r'<td rowspan="(.)">',lambda x:f'<td colspan="1" rowspan="{x.group(1)}">',s)
1792
+ # s = re.sub(r'<td colspan="(.)">',lambda x:f'<td colspan="{x.group(1)}" rowspan="1">',s)
1793
+ res = ''
1794
+ res += '\n\n'
1795
+ temp_item = ''
1796
+ for c in s:
1797
+ temp_item += c
1798
+ if c == '>' and not re.search(r'<td.*?>\$',temp_item):
1799
+ res += temp_item+'\n'
1800
+ temp_item = ''
1801
+ return res+'\n'
1802
+ def merge_table(md):
1803
+ table_temp = ''
1804
+ for line in md:
1805
+ table_temp += line
1806
+ return convert_table_str(table_temp)
1807
+ def find_md_table_mode(line):
1808
+ if re.search(r'-*?:',line) or re.search(r'---',line) or re.search(r':-*?',line):
1809
+ return True
1810
+ return False
1811
+ def delete_table_and_body(input_list):
1812
+ res = []
1813
+ for line in input_list:
1814
+ if not re.search(r'</?t(able|head|body)>',line):
1815
+ res.append(line)
1816
+ return res
1817
+ def merge_tables(input_str):
1818
+ # Delete HTML comments
1819
+ input_str = re.sub(r'<!--[\s\S]*?-->', '', input_str)
1820
+
1821
+ # Use regex to find each <table> block
1822
+ table_blocks = re.findall(r'<table>[\s\S]*?</table>', input_str)
1823
+
1824
+ # Process each <table> block, replace <th> with <td>
1825
+ output_lines = []
1826
+ for block in table_blocks:
1827
+ block_lines = block.split('\n')
1828
+ for i, line in enumerate(block_lines):
1829
+ if '<th>' in line:
1830
+ block_lines[i] = line.replace('<th>', '<td>').replace('</th>', '</td>')
1831
+ final_tr = delete_table_and_body(block_lines)
1832
+ if len(final_tr) > 2:
1833
+ output_lines.extend(final_tr) # Ignore <table> and </table> tags, keep only table content
1834
+
1835
+ # Rejoin the processed strings
1836
+ merged_output = '<table>\n{}\n</table>'.format('\n'.join(output_lines))
1837
+
1838
+ return "\n\n" + merged_output + "\n\n"
1839
+
1840
+ def replace_table_with_placeholder(input_string):
1841
+ lines = input_string.split('\n')
1842
+ output_lines = []
1843
+
1844
+ in_table_block = False
1845
+ temp_block = ""
1846
+ last_line = ""
1847
+
1848
+ org_table_list = []
1849
+ in_org_table = False
1850
+
1851
+ for idx, line in enumerate(lines):
1852
+ # if not in_org_table:
1853
+ # if "<table>" not in last_line and in_table_block == False and temp_block != "":
1854
+ # output_lines.append(merge_tables(temp_block))
1855
+ # temp_block = ""
1856
+ if "<table>" in line:
1857
+ # if "<table><tr" in line:
1858
+ # org_table_list.append(line)
1859
+ # in_org_table = True
1860
+ # output_lines.append(last_line)
1861
+ # continue
1862
+ # else:
1863
+ in_table_block = True
1864
+ temp_block += last_line
1865
+ elif in_table_block:
1866
+ if not find_md_table_mode(last_line) and "</thead>" not in last_line:
1867
+ temp_block += "\n" + last_line
1868
+ if "</table>" in last_line:
1869
+ if "<table>" not in line:
1870
+ in_table_block = False
1871
+ output_lines.append(merge_tables(temp_block))
1872
+ temp_block = ""
1873
+ else:
1874
+ output_lines.append(last_line)
1875
+
1876
+ last_line = line
1877
+ # else:
1878
+ # org_table_list.append(line)
1879
+ # if "</table" in line:
1880
+ # in_org_table = False
1881
+ # last_line = merge_table(org_table_list)
1882
+ # org_table_list = []
1883
+
1884
+ if last_line:
1885
+ if in_table_block or "</table>" in last_line:
1886
+ temp_block += "\n" + last_line
1887
+ output_lines.append(merge_tables(temp_block))
1888
+ else:
1889
+ output_lines.append(last_line)
1890
+ # if "</table>" in last_line:
1891
+ # output_lines.append(merge_tables(temp_block))
1892
+
1893
+ return '\n'.join(output_lines)
1894
+
1895
+ def convert_table(input_str):
1896
+ # Replace <table>
1897
+ output_str = input_str.replace("<table>", "<table border=\"1\" >")
1898
+
1899
+ # Replace <td>
1900
+ output_str = output_str.replace("<td>", "<td colspan=\"1\" rowspan=\"1\">")
1901
+
1902
+ return output_str
1903
+
1904
+ def convert_markdown_to_html(markdown_content):
1905
+ # Define a regex pattern to find Markdown tables with newlines
1906
+ markdown_content = markdown_content.replace('\r', '')+'\n'
1907
+ pattern = re.compile(r'\|\s*.*?\s*\|\n', re.DOTALL)
1908
+
1909
+ # Find all matches in the Markdown content
1910
+ matches = pattern.findall(markdown_content)
1911
+
1912
+ for match in matches:
1913
+ html_table = markdown_to_html(match)
1914
+ markdown_content = markdown_content.replace(match, html_table, 1) # Only replace the first occurrence
1915
+
1916
+ res_html = convert_table(replace_table_with_placeholder(markdown_content))
1917
+
1918
+ return res_html
reference/code/Fast-dLLM/third_party/VLMEvalKit/vlmeval/dataset/SGI_Bench_1_0/__init__.py ADDED
File without changes