├── src
├── utils.py
└── metric
│ ├── __init__.py
│ ├── metric.py
│ ├── l1.py
│ ├── l2.py
│ ├── psnr.py
│ ├── ssim.py
│ ├── lpips.py
│ ├── dino.py
│ └── clip_score.py
├── guidance
├── guidance.md
├── image.png
└── guidance_cn.md
├── .gitignore
├── example_data
├── test1
│ ├── gt
│ │ └── img1.jpg
│ └── pred
│ │ └── img1.jpg
└── example_data.json
├── requirments.txt
├── config
└── example.yaml
├── README.md
└── benchmark.py
/src/utils.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/guidance/guidance.md:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/metric/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | src/metric/__pycache__/*
--------------------------------------------------------------------------------
/guidance/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamjimmy/cv_benchmark_hub/HEAD/guidance/image.png
--------------------------------------------------------------------------------
/example_data/test1/gt/img1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamjimmy/cv_benchmark_hub/HEAD/example_data/test1/gt/img1.jpg
--------------------------------------------------------------------------------
/example_data/test1/pred/img1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamjimmy/cv_benchmark_hub/HEAD/example_data/test1/pred/img1.jpg
--------------------------------------------------------------------------------
/example_data/example_data.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "prompt": "a girl",
4 | "pred": "example_data/test1/gt/img1.jpg"
5 | }
6 | ]
--------------------------------------------------------------------------------
/requirments.txt:
--------------------------------------------------------------------------------
1 | torch
2 | torchvision
3 | tqdm
4 | numpy
5 | torchmetrics
6 | pillow
7 | PyYAML
8 | openpyxl
9 | transformers
10 | pandas
11 |
--------------------------------------------------------------------------------
/src/metric/metric.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 |
3 | class Metric(ABC):
4 | def __init__(self):
5 | pass
6 |
7 | """
8 | 定义一个 Metric 接口,所有指标类必须实现 __call__ 方法。
9 | """
10 | @abstractmethod
11 | def __call__(self, input_paths, keys):
12 | """
13 | 所有指标类必须实现的方法。
14 | :param input_paths: 输入路径列表
15 | :param keys: 用于计算指标的关键字列表
16 | :return: 指标分数
17 | """
18 | pass
--------------------------------------------------------------------------------
/config/example.yaml:
--------------------------------------------------------------------------------
1 | path_key: test # Choice. Only files with 'test' in their name in the input directory will be processed.
2 |
3 |
4 | keys:
5 | target: gt
6 | pred: pred
7 |
8 | metrics:
9 |
10 | clip_image_to_image_score: # custom test name
11 | metric:
12 | class: src.metric.clip_score.ClipScore
13 | clip_model: openai/clip-vit-base-patch16
14 | device: cuda
15 |
16 | clip_image_to_text_score:
17 | metric:
18 | class: src.metric.clip_score.ClipScore
19 | clip_model: openai/clip-vit-base-patch16
20 | device: cuda
21 | special_keys: # custom key
22 | - prompt
23 | - pred
24 | special_input_paths:
25 | - example_data/example_data.json
26 | just_special_input_paths: true # if true, only the special_input_paths will be used
27 |
28 | ssim:
29 | metric: # metric class
30 | class: src.metric.ssim.SSIM
31 | device: cuda
32 |
33 | psnr_score:
34 | metric:
35 | class: src.metric.psnr.PSNR
36 | device: cuda
37 |
38 | lipis_score:
39 | metric:
40 | class: src.metric.lpips.LPIPS
41 | device: cuda
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/guidance/guidance_cn.md:
--------------------------------------------------------------------------------
1 | # 中文教程
2 |
3 |
4 | [ ](视频正在录制)
5 | ## 文件结构
6 | 确定待测试的文件以下列形式排布
7 | ### 文件夹格式
8 | ```bash
9 | input_path
10 | ├── input_dir1
11 | │ ├── dir1
12 | │ │ ├── img1.jpg # all the filename must match in each dir
13 | │ │ ├── img2.jpg
14 | │ │ └── ...
15 | │ └── dir2
16 | │ ├── img1.jpg
17 | │ ├── img2.jpg
18 | │ └── ...
19 | │
20 | ├── input_dir2
21 | │ ├── dir1
22 | │ └── ...
23 | │ └── dir2
24 | │ └── ...
25 | ...
26 | ```
27 |
28 | ### JSON 格式
29 | ```json
30 | [
31 | {
32 | "args1": "image path or text of target",
33 | "args2": "image path or text of pred",
34 | },
35 |
36 | {
37 | ...
38 | },
39 | {
40 | "args1": "image path or text of target",
41 | "args2": "image path or text of pred",
42 | }
43 | ]
44 | ```
45 |
46 | ### **!!!下文提到的`keys`的概念其实就是文件结构的 'dir1'&'dir2' 或者 'args1'&'args2'。!!!**
47 |
48 | ## 配置文件如何使用
49 | 
50 |
51 |
52 | ### 通用文件夹指定(SEC.1 & SEC.2)
53 |
54 | 本例中,所有在`--input_path`下含有'test'关键字的文件夹都会被测试。对于每个文件夹测试其中的'gt','pred'文件夹。
55 |
56 | 图中第二部分的`keys`指定了通用路径被测试的关键字。这里为了标准化,只允许指定`target key`和`pred key`。
57 |
58 | (这样做是为了方便批量测试,例如ssim、clip score都需要对生成图象和GT图像做对比。可以避免重复输入路径。)
59 |
60 | ### 具体Metric配置
61 | `Metric:`指定了测试的类
62 |
63 | `special_input_paths:`指定了针对该Metric需要单独测试的 json 或者 dir 路径。
64 |
65 |
66 | `special_keys:`指定特殊的keys(可以任意数量),keys的顺序在大部分Metric下无影响,需要根据Metric类具体确定。
67 |
68 | `just_special_input_paths:`设定为true,则只测试special_input_paths,忽略上面指定的通用文件夹。
69 |
70 | 例如本例需要特殊计算文字与图像的clip score。忽略通用路径、指定json路径、指定keys: 'prompt'和'pred'。(clip score代码会自动分别路径和prompt,顺序其实无所谓)
71 |
72 |
73 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Computer Vision Benchmark Hub
2 | [](mailto:jiangzj453@foxmail.com)
3 |
4 | 逻辑较为简单,请参考教程。有问题可以联系我~
5 |
6 | [中文教程](guidance/guidance_cn.md) | [Guidance](guidance/guidance.md)
7 |
8 | Welcome to the Computer Vision Benchmark Hub — a flexible and user-friendly tool designed to evaluate and benchmark image-based models. With support for key metrics such as SSIM, PSNR, CLIP-Score, and more, our platform enables seamless one-click evaluations, helping you compare and optimize model performance effortlessly.
9 |
10 | ## 🚀 Installation
11 | To get started, simply install the required dependencies:
12 | ```bash
13 | conda create -n cv_benchmark python==3.10
14 | conda activate cv_benchmark
15 | pip install -r requirments.txt
16 | ```
17 |
18 | ## 📁 Data Preparation
19 | The toolkit supports two types of inputs: **folders** and **JSON files**.
20 |
21 | ### Folder Structure
22 | Ensure your input folder is organized as follows:
23 | ```bash
24 | input_path
25 | ├── input_dir1
26 | │ ├── dir1
27 | │ │ ├── img1.jpg # all the filename must match in each dir
28 | │ │ ├── img2.jpg
29 | │ │ └── ...
30 | │ └── dir2
31 | │ ├── img1.jpg
32 | │ ├── img2.jpg
33 | │ └── ...
34 | │
35 | ├── input_dir2
36 | │ ├── dir1
37 | │ └── ...
38 | │ └── dir2
39 | │ └── ...
40 | ...
41 | ```
42 |
43 | ### JSON Format
44 | Alternatively, you can use a JSON file structured like this:
45 | ```json
46 | [
47 | {
48 | "args1": "image path or text of target",
49 | "args2": "image path or text of pred",
50 | },
51 |
52 | {
53 | ...
54 | },
55 | {
56 | "args1": "image path or text of target",
57 | "args2": "image path or text of pred",
58 | }
59 | ]
60 | ```
61 | ## ⚙️ Configuration
62 | The toolkit uses a YAML configuration file for customization.
63 |
64 | ### Input Paths
65 | `path_key:` Filters files containing specific keywords in their names within the `--input_path ` directory.
66 | `special_input_paths:` A list of paths that require special processing.
67 |
68 | ### Keys
69 | Define the input keys corresponding to your JSON or folder structure:
70 | ```yaml
71 | keys:
72 | pred_key: args1
73 | target_key: args2
74 | ```
75 |
76 |
77 | ## 📊 Usage
78 |
79 | Run the benchmark using the following command:
80 | ```bash
81 | python benchmark.py --config config/example.yaml --input_path example_data --output_path ./result/
82 | ```
83 | The result will be saved in `./result/test1` folder. There will be a 'benchmark.xlsx' file.
84 |
85 |
86 |
87 | ## ✅ TODO
88 |
89 | Completed
90 | - [X] Support Diffusion Metric:SSIM, PSNR, CLIP-Score, LIPIS
91 | - [ ] generate latex
92 | - [X] L1, L2...
93 |
94 | - [ ] Upcoming Features
95 |
96 | ---
97 |
98 |
99 | Feel free to tweak the config and data formats to suit your needs. Happy benchmarking! 🎯
100 |
101 | ## Star History
102 |
103 | [](https://www.star-history.com/#jamjimmy/cv_benchmark_hub&Date)
104 |
--------------------------------------------------------------------------------
/src/metric/l1.py:
--------------------------------------------------------------------------------
1 | from torch import nn
2 | import os
3 | from PIL import Image
4 | import json
5 | from torchvision import transforms
6 | import torch
7 | from .metric import Metric
8 | from tqdm import tqdm
9 |
10 | transform_img = transforms.Compose([
11 | transforms.ToTensor(),
12 | # lambda x: (x * 255)
13 | ])
14 |
15 | def preprocess_list(target_input, pred_input, device):
16 | processed_pred_input = []
17 | processed_target_input = []
18 | for target_item, pred_item in tqdm(zip(target_input, pred_input), total=len(target_input)):
19 | target_image = Image.open(target_item).convert('RGB')
20 | pred_image = Image.open(pred_item).convert('RGB')
21 | if target_image.size != pred_image.size:
22 | Warning("Target image size is not equal to pred image size in test L2!!!")
23 | pred_image = pred_image.resize(target_image.size)
24 |
25 | processed_pred_input.append(transform_img(pred_image).to(device))
26 | processed_target_input.append(transform_img(target_image).to(device))
27 | return processed_pred_input, processed_target_input
28 |
29 | class L1(Metric):
30 | def __init__(self, device = 'cuda'):
31 | self.device = device
32 | self.criterion = nn.L1Loss()
33 | def __call__(self, input: str, keys=['target', 'pred']):
34 | '''
35 | Args:
36 | - input: path(.json or .jsonl or dir) and param::keys = ['dir1', 'dir2']:
37 | - if dir, there must be 2 dirs in the input dir, and
38 | the number and filename of images in the two dirs must be the same. The dir looks like this:
39 |
40 | input_dir
41 | ├── dir1
42 | │ ├── img1.jpg
43 | │ ├── img2.jpg
44 | │ └── ...
45 | └── dir2
46 | ├── img1.jpg
47 | ├── img2.jpg
48 | └── ...
49 |
50 | - if .json or .jsonl the file looks like this and param::keys = ['args1', 'args2']:
51 | [
52 | {
53 | "args1": "image path",
54 | "args2": "image path,
55 | },
56 | ...
57 | {
58 | "args1": "image path",
59 | "args2": "image path,
60 | }
61 | ]
62 | Returns:
63 | - psnr score
64 | '''
65 | target_list = []
66 | pred_list = []
67 |
68 | if os.path.isdir(input):
69 | dir_path_1 = os.path.join(input, keys[0])
70 | dir_path_2 = os.path.join(input, keys[1])
71 | assert len(os.listdir(dir_path_1)) == len(os.listdir(dir_path_2)), f"the number of images in {dir_path_1} and {dir_path_2} must be the same"
72 | imgs = os.listdir(dir_path_1)
73 | for file in imgs:
74 | assert os.path.exists(os.path.join(dir_path_2, file)), f"file not exists {os.path.join(dir_path_2, file)}"
75 | target_list.append(os.path.join(dir_path_1, file))
76 | pred_list.append(os.path.join(dir_path_2, file))
77 |
78 | elif input.endswith(".json") or input.endswith(".jsonl"):
79 | with open(input, "r") as f:
80 | data = json.load(f)
81 | assert len(keys) == 2, f"keys must be 2, but got {keys}"
82 | for item in data:
83 | target_path = item[keys[0]]
84 | pred_path = item[keys[1]]
85 | target_list.append(target_path)
86 | pred_list.append(pred_path)
87 |
88 | else:
89 | raise ValueError(f"{input} must be dir or json file")
90 |
91 | assert len(target_list) == len(pred_list), f"the number of images in {input} must be the same"
92 | processed_target_list, processed_pred_list = preprocess_list(target_list, pred_list, self.device)
93 |
94 | score = 0.0
95 |
96 | # print(processed_target_list, processed_pred_list)
97 |
98 | with torch.no_grad():
99 | for pred, target in zip(processed_pred_list, processed_target_list):
100 | score += self.criterion(pred, target).detach().cpu()
101 | score = score / len(processed_target_list)
102 | return "L1", score.item(), len(target_list)
103 |
--------------------------------------------------------------------------------
/src/metric/l2.py:
--------------------------------------------------------------------------------
1 | from torch import nn
2 | import os
3 | from PIL import Image
4 | import json
5 | from torchvision import transforms
6 | import torch
7 | from .metric import Metric
8 | from tqdm import tqdm
9 |
10 | transform_img = transforms.Compose([
11 | transforms.ToTensor(),
12 | # lambda x: (x * 255)
13 | ])
14 |
15 | def preprocess_list(target_input, pred_input, device):
16 | processed_pred_input = []
17 | processed_target_input = []
18 | for target_item, pred_item in tqdm(zip(target_input, pred_input), total=len(target_input)):
19 | target_image = Image.open(target_item).convert('RGB')
20 | pred_image = Image.open(pred_item).convert('RGB')
21 | if target_image.size != pred_image.size:
22 | Warning("Target image size is not equal to pred image size in test L2!!!")
23 | pred_image = pred_image.resize(target_image.size)
24 |
25 | processed_pred_input.append(transform_img(pred_image).to(device))
26 | processed_target_input.append(transform_img(target_image).to(device))
27 | return processed_pred_input, processed_target_input
28 |
29 | class L2(Metric):
30 | def __init__(self, device = 'cuda'):
31 | self.device = device
32 | self.criterion = nn.MSELoss()
33 | def __call__(self, input: str, keys=['target', 'pred']):
34 | '''
35 | Args:
36 | - input: path(.json or .jsonl or dir) and param::keys = ['dir1', 'dir2']:
37 | - if dir, there must be 2 dirs in the input dir, and
38 | the number and filename of images in the two dirs must be the same. The dir looks like this:
39 |
40 | input_dir
41 | ├── dir1
42 | │ ├── img1.jpg
43 | │ ├── img2.jpg
44 | │ └── ...
45 | └── dir2
46 | ├── img1.jpg
47 | ├── img2.jpg
48 | └── ...
49 |
50 | - if .json or .jsonl the file looks like this and param::keys = ['args1', 'args2']:
51 | [
52 | {
53 | "args1": "image path",
54 | "args2": "image path,
55 | },
56 | ...
57 | {
58 | "args1": "image path",
59 | "args2": "image path,
60 | }
61 | ]
62 | Returns:
63 | - psnr score
64 | '''
65 | target_list = []
66 | pred_list = []
67 |
68 | if os.path.isdir(input):
69 | dir_path_1 = os.path.join(input, keys[0])
70 | dir_path_2 = os.path.join(input, keys[1])
71 | assert len(os.listdir(dir_path_1)) == len(os.listdir(dir_path_2)), f"the number of images in {dir_path_1} and {dir_path_2} must be the same"
72 | imgs = os.listdir(dir_path_1)
73 | for file in imgs:
74 | assert os.path.exists(os.path.join(dir_path_2, file)), f"file not exists {os.path.join(dir_path_2, file)}"
75 | target_list.append(os.path.join(dir_path_1, file))
76 | pred_list.append(os.path.join(dir_path_2, file))
77 |
78 | elif input.endswith(".json") or input.endswith(".jsonl"):
79 | with open(input, "r") as f:
80 | data = json.load(f)
81 | assert len(keys) == 2, f"keys must be 2, but got {keys}"
82 | for item in data:
83 | target_path = item[keys[0]]
84 | pred_path = item[keys[1]]
85 | target_list.append(target_path)
86 | pred_list.append(pred_path)
87 |
88 | else:
89 | raise ValueError(f"{input} must be dir or json file")
90 |
91 | assert len(target_list) == len(pred_list), f"the number of images in {input} must be the same"
92 | processed_target_list, processed_pred_list = preprocess_list(target_list, pred_list, self.device)
93 |
94 | score = 0.0
95 |
96 | # print(processed_target_list, processed_pred_list)
97 |
98 | with torch.no_grad():
99 | for pred, target in zip(processed_pred_list, processed_target_list):
100 | score += self.criterion(pred, target).detach().cpu()
101 | score = score / len(processed_target_list)
102 | return "L2", score.item(), len(target_list)
103 |
--------------------------------------------------------------------------------
/src/metric/psnr.py:
--------------------------------------------------------------------------------
1 | from torchmetrics.image import PeakSignalNoiseRatio
2 | import torch
3 | import os
4 | from PIL import Image
5 | import json
6 | from torchvision import transforms
7 | from .metric import Metric
8 | from tqdm import tqdm
9 |
10 | transform_img = transforms.Compose([
11 | transforms.ToTensor(),
12 | lambda x: (x * 255).to(torch.uint8)
13 | ])
14 |
15 | def preprocess_list(target_input, pred_input, device):
16 | processed_pred_input = []
17 | processed_target_input = []
18 | for target_item, pred_item in tqdm(zip(target_input, pred_input), total=len(target_input)):
19 | target_image = Image.open(target_item).convert('RGB')
20 | pred_image = Image.open(pred_item).convert('RGB')
21 | if target_image.size != pred_image.size:
22 | Warning("Target image size is not equal to pred image size in test PSNR!!!")
23 | pred_image = pred_image.resize(target_image.size)
24 |
25 | processed_pred_input.append(transform_img(pred_image).unsqueeze(0).to(device))
26 | processed_target_input.append(transform_img(target_image).unsqueeze(0).to(device))
27 | return processed_pred_input, processed_target_input
28 |
29 | class PSNR(Metric):
30 | def __init__(self, device = 'cuda'):
31 | self.device = device
32 | self.psnr = PeakSignalNoiseRatio().to(self.device)
33 |
34 |
35 | def __call__(self, input: str, keys=['target', 'pred']):
36 | '''
37 | Args:
38 | - input: path(.json or .jsonl or dir) and param::keys = ['dir1', 'dir2']:
39 | - if dir, there must be 2 dirs in the input dir, and
40 | the number and filename of images in the two dirs must be the same. The dir looks like this:
41 |
42 | input_dir
43 | ├── dir1
44 | │ ├── img1.jpg
45 | │ ├── img2.jpg
46 | │ └── ...
47 | └── dir2
48 | ├── img1.jpg
49 | ├── img2.jpg
50 | └── ...
51 |
52 | - if .json or .jsonl the file looks like this and param::keys = ['args1', 'args2']:
53 | [
54 | {
55 | "args1": "image path",
56 | "args2": "image path,
57 | },
58 | ...
59 | {
60 | "args1": "image path ",
61 | "args2": "image path ,
62 | }
63 | ]
64 | Returns:
65 | - psnr score
66 | '''
67 | target_list = []
68 | pred_list = []
69 | if os.path.isdir(input):
70 | dir_path_1 = os.path.join(input, keys[0])
71 | dir_path_2 = os.path.join(input, keys[1])
72 | assert len(os.listdir(dir_path_1)) == len(os.listdir(dir_path_2)), f"the number of images in {dir_path_1} and {dir_path_2} must be the same"
73 | imgs = os.listdir(dir_path_1)
74 | for file in imgs:
75 | assert os.path.exists(os.path.join(dir_path_2, file)), f"file not exists {os.path.join(dir_path_2, file)}"
76 | target_list.append(os.path.join(dir_path_1, file))
77 | pred_list.append(os.path.join(dir_path_2, file))
78 |
79 | elif input.endswith(".json") or input.endswith(".jsonl"):
80 | with open(input, "r") as f:
81 | data = json.load(f)
82 | assert len(keys) == 2, f"keys must be 2, but got {keys}"
83 | for item in data:
84 | target_path = item[keys[0]]
85 | pred_path = item[keys[1]]
86 | target_list.append(target_path)
87 | pred_list.append(pred_path)
88 | else:
89 | raise ValueError(f"{input} must be dir or json file")
90 |
91 | assert len(target_list) == len(pred_list), f"the number of images in {input} must be the same"
92 | processed_target_list, processed_pred_list = preprocess_list(target_list, pred_list, self.device)
93 |
94 | score = 0.0
95 | for pred, target in zip(processed_pred_list, processed_target_list):
96 | score += self.psnr(pred, target)
97 | score = score / len(processed_target_list)
98 | return "PSNR", score.detach().item(), len(target_list)
99 |
100 | # print(get_psnr_batch("data/", keys=['gt', 'pred']))
--------------------------------------------------------------------------------
/src/metric/ssim.py:
--------------------------------------------------------------------------------
1 | from torchmetrics.image import StructuralSimilarityIndexMeasure
2 | import os
3 | from PIL import Image
4 | import json
5 | from torchvision import transforms
6 | import torch
7 | from .metric import Metric
8 | from tqdm import tqdm
9 | transform_img = transforms.Compose([
10 | transforms.ToTensor(),
11 | lambda x: (x * 255)
12 | ])
13 |
14 | def preprocess_list(target_input, pred_input, device):
15 | processed_pred_input = []
16 | processed_target_input = []
17 | for target_item, pred_item in tqdm(zip(target_input, pred_input), total=len(target_input)):
18 | target_image = Image.open(target_item).convert('RGB')
19 | pred_image = Image.open(pred_item).convert('RGB')
20 | if target_image.size != pred_image.size:
21 | Warning("Target image size is not equal to pred image size in test SSIM!!!")
22 | pred_image = pred_image.resize(target_image.size)
23 |
24 | processed_pred_input.append(transform_img(pred_image).unsqueeze(0).to(device))
25 | processed_target_input.append(transform_img(target_image).unsqueeze(0).to(device))
26 |
27 | return processed_pred_input, processed_target_input
28 |
29 | class SSIM(Metric):
30 | def __init__(self, device = 'cuda'):
31 | self.device = device
32 | self.ssim = StructuralSimilarityIndexMeasure().to(self.device)
33 |
34 | def __call__(self, input: str, keys=['target', 'pred']):
35 | '''
36 | Args:
37 | - input: path(.json or .jsonl or dir) and param::keys = ['dir1', 'dir2']:
38 | - if dir, there must be 2 dirs in the input dir, and
39 | the number and filename of images in the two dirs must be the same. The dir looks like this:
40 |
41 | input_dir
42 | ├── dir1
43 | │ ├── img1.jpg
44 | │ ├── img2.jpg
45 | │ └── ...
46 | └── dir2
47 | ├── img1.jpg
48 | ├── img2.jpg
49 | └── ...
50 |
51 | - if .json or .jsonl the file looks like this and param::keys = ['args1', 'args2']:
52 | [
53 | {
54 | "args1": "image path",
55 | "args2": "image path,
56 | },
57 | ...
58 | {
59 | "args1": "image path",
60 | "args2": "image path,
61 | }
62 | ]
63 | Returns:
64 | - psnr score
65 | '''
66 | target_list = []
67 | pred_list = []
68 |
69 | if os.path.isdir(input):
70 | dir_path_1 = os.path.join(input, keys[0])
71 | dir_path_2 = os.path.join(input, keys[1])
72 | assert len(os.listdir(dir_path_1)) == len(os.listdir(dir_path_2)), f"the number of images in {dir_path_1} and {dir_path_2} must be the same"
73 | imgs = os.listdir(dir_path_1)
74 | for file in imgs:
75 | assert os.path.exists(os.path.join(dir_path_2, file)), f"file not exists {os.path.join(dir_path_2, file)}"
76 | target_list.append(os.path.join(dir_path_1, file))
77 | pred_list.append(os.path.join(dir_path_2, file))
78 |
79 | elif input.endswith(".json") or input.endswith(".jsonl"):
80 | with open(input, "r") as f:
81 | data = json.load(f)
82 | assert len(keys) == 2, f"keys must be 2, but got {keys}"
83 | for item in data:
84 | target_path = item[keys[0]]
85 | pred_path = item[keys[1]]
86 | target_list.append(target_path)
87 | pred_list.append(pred_path)
88 |
89 | else:
90 | raise ValueError(f"{input} must be dir or json file")
91 |
92 | assert len(target_list) == len(pred_list), f"the number of images in {input} must be the same"
93 | processed_target_list, processed_pred_list = preprocess_list(target_list, pred_list, self.device)
94 |
95 | score = 0.0
96 |
97 | # print(processed_target_list, processed_pred_list)
98 |
99 | with torch.no_grad():
100 | for pred, target in zip(processed_pred_list, processed_target_list):
101 | score += self.ssim(pred, target)
102 | score = score / len(processed_target_list)
103 | return "SSIM", score.detach().item(), len(target_list)
104 |
105 | # print(get_ssim_batch("data/", keys=['gt', 'pred']))
--------------------------------------------------------------------------------
/src/metric/lpips.py:
--------------------------------------------------------------------------------
1 | from torchmetrics.image.lpip import LearnedPerceptualImagePatchSimilarity
2 | import os
3 | from PIL import Image
4 | import json
5 | from torchvision import transforms
6 | import torch
7 | from .metric import Metric
8 | from tqdm import tqdm
9 | transform_img = transforms.Compose([
10 | transforms.ToTensor(),
11 | lambda x: (x * 2) - 1
12 | ])
13 |
14 | def preprocess_list(target_input, pred_input, device):
15 | processed_pred_input = []
16 | processed_target_input = []
17 | for target_item, pred_item in tqdm(zip(target_input, pred_input), total=len(target_input)):
18 | target_image = Image.open(target_item).convert('RGB')
19 | pred_image = Image.open(pred_item).convert('RGB')
20 | if target_image.size != pred_image.size:
21 | Warning("Target image size is not equal to pred image size in test LPIPS!!!")
22 | pred_image = pred_image.resize(target_image.size)
23 |
24 | processed_pred_input.append(transform_img(pred_image).unsqueeze(0).to(device))
25 | processed_target_input.append(transform_img(target_image).unsqueeze(0).to(device))
26 | return processed_pred_input, processed_target_input
27 |
28 |
29 | class LPIPS(Metric):
30 | def __init__(self, device = 'cuda'):
31 | self.device = device
32 | self.lpips = LearnedPerceptualImagePatchSimilarity(net_type='squeeze').to(self.device)
33 |
34 | def __call__(self, input: str, keys=['target', 'pred']):
35 | '''
36 | Args:
37 | - input: path(.json or .jsonl or dir) and param::keys = ['dir1', 'dir2']:
38 | - if dir, there must be 2 dirs in the input dir, and
39 | the number and filename of images in the two dirs must be the same. The dir looks like this:
40 |
41 | input_dir
42 | ├── dir1
43 | │ ├── img1.jpg
44 | │ ├── img2.jpg
45 | │ └── ...
46 | └── dir2
47 | ├── img1.jpg
48 | ├── img2.jpg
49 | └── ...
50 |
51 | - if .json or .jsonl the file looks like this and param::keys = ['args1', 'args2']:
52 | [
53 | {
54 | "args1": "image path",
55 | "args2": "image path,
56 | },
57 | ...
58 | {
59 | "args1": "image path",
60 | "args2": "image path,
61 | }
62 | ]
63 | Returns:
64 | - psnr score
65 | '''
66 | target_list = []
67 | pred_list = []
68 |
69 | if os.path.isdir(input):
70 | dir_path_1 = os.path.join(input, keys[0])
71 | dir_path_2 = os.path.join(input, keys[1])
72 | assert len(os.listdir(dir_path_1)) == len(os.listdir(dir_path_2)), f"the number of images in {dir_path_1} and {dir_path_2} must be the same"
73 | imgs = os.listdir(dir_path_1)
74 | for file in imgs:
75 | assert os.path.exists(os.path.join(dir_path_2, file)), f"file not exists {os.path.join(dir_path_2, file)}"
76 | target_list.append(os.path.join(dir_path_1, file))
77 | pred_list.append(os.path.join(dir_path_2, file))
78 |
79 | elif input.endswith(".json") or input.endswith(".jsonl"):
80 | with open(input, "r") as f:
81 | data = json.load(f)
82 | assert len(keys) == 2, f"keys must be 2, but got {keys}"
83 | for item in data:
84 | target_path = item[keys[0]]
85 | pred_path = item[keys[1]]
86 | target_list.append(target_path)
87 | pred_list.append(pred_path)
88 |
89 | else:
90 | raise ValueError(f"{input} must be dir or json file")
91 |
92 | assert len(target_list) == len(pred_list), f"the number of images in {input} must be the same"
93 | processed_target_list, processed_pred_list = preprocess_list(target_list, pred_list, self.device)
94 |
95 | score = 0.0
96 |
97 | # print(processed_target_list, processed_pred_list)
98 |
99 | with torch.no_grad():
100 | for pred, target in zip(processed_pred_list, processed_target_list):
101 | score += self.lpips(pred, target)
102 | score = score / len(processed_target_list)
103 | return "LPIPS", score.detach().item(), len(target_list)
104 |
105 | # print(get_lpips_batch("data/", keys=['gt', 'pred']))
--------------------------------------------------------------------------------
/benchmark.py:
--------------------------------------------------------------------------------
1 | import yaml
2 | import importlib
3 | import pandas as pd
4 | import argparse
5 | import os
6 | import shutil
7 | from datetime import datetime
8 | # import wandb
9 |
10 |
11 | def load_config(config_path):
12 | with open(config_path, 'r') as file:
13 | config = yaml.safe_load(file)
14 | return config
15 |
16 | def get_class(class_path):
17 | module_path, class_name = class_path.rsplit('.', 1)
18 | module = importlib.import_module(module_path)
19 | cls = getattr(module, class_name)
20 | return cls
21 |
22 | def postprocess(results, args, metric_name):
23 | df = pd.DataFrame(results)
24 | os.makedirs(args.output_path, exist_ok=True)
25 | excel_path = os.path.join(args.output_path, 'benchmark.xlsx')
26 | mode = 'a' if os.path.exists(excel_path) else 'w'
27 | if mode == 'a':
28 | with pd.ExcelWriter(excel_path, mode=mode, engine='openpyxl', if_sheet_exists='replace') as writer:
29 | if metric_name in writer.book.sheetnames:
30 | existing_df = pd.read_excel(excel_path, sheet_name=metric_name)
31 | df = pd.concat([existing_df, df], ignore_index=True)
32 | df.to_excel(writer, sheet_name=metric_name, index=False)
33 | else:
34 | with pd.ExcelWriter(excel_path, mode=mode, engine='openpyxl') as writer:
35 | if metric_name in writer.book.sheetnames:
36 | existing_df = pd.read_excel(excel_path, sheet_name=metric_name)
37 | df = pd.concat([existing_df, df], ignore_index=True)
38 | df.to_excel(writer, sheet_name=metric_name, index=False)
39 | destination_file = os.path.join(args.output_path, os.path.basename(args.config))
40 | if not os.path.exists(destination_file):
41 | shutil.copy(args.config, destination_file)
42 |
43 | def main(config, args):
44 |
45 |
46 | # columns = ['experiment_name', 'score', 'path', 'pred_key', 'target_key', 'current_datetime']
47 | # table = wandb.Table(columns=columns)
48 |
49 | metrics = config.get('metrics', {})
50 | input_paths = []
51 | path_key = config.get('path_key', None)
52 | for file in os.listdir(args.input_path):
53 | if path_key == None or path_key in file:
54 | input_paths.append(os.path.join(args.input_path, file))
55 |
56 | target_Key = config['keys'].get('target', None)
57 | pred_key = config['keys'].get('pred', None)
58 |
59 | for metric_name, metric_config in metrics.items():
60 |
61 | # Instantiate metric class
62 | class_name = metric_config['metric']['class']
63 | MetricClass = get_class(class_name)
64 | metric_instance = MetricClass(**{k: v for k, v in metric_config['metric'].items() if k != 'class'})
65 |
66 | # Special keys and paths
67 | keys = [target_Key, pred_key]
68 | Keys_input = metric_config.get('special_keys', keys)
69 |
70 | all_input_paths = metric_config.get('special_input_paths', [])
71 | if metric_config.get('just_special_input_paths', False) == False:
72 | all_input_paths = input_paths + all_input_paths
73 | assert(len(all_input_paths) > 0), "No input paths found"
74 | results = []
75 | # wandb.log({"score": 0.9})
76 | # Run metric on all input paths
77 | for path in all_input_paths:
78 | print(f"Running {metric_name} on {path}")
79 | experiment_name, score, count = metric_instance(path, keys=Keys_input)
80 | result = {
81 | 'experiment_name': experiment_name,
82 | 'score': score,
83 | 'count': count,
84 | 'path': path,
85 | 'keys': keys,
86 | 'current_datetime': datetime.now()
87 | }
88 | results.append(result)
89 | # table.add_data(experiment_name, score, path, pred_key_input, targetKey_input, datetime.now())
90 | postprocess(results, args, metric_name)
91 |
92 |
93 | if __name__ == "__main__":
94 |
95 | parser = argparse.ArgumentParser(description='Benchmarking')
96 | parser.add_argument('--output_path', type=str, help='output_path')
97 | parser.add_argument('--input_path', type=str, help='output_path')
98 | parser.add_argument('--config', type=str, help='Path to config file')
99 | # parser.add_argument("--use_wandb", type=bool, default=True, help="Enable or disable wandb logging.")
100 | args = parser.parse_args()
101 |
102 | # if args.use_wandb:
103 | # wandb.init(project='benchmark', name=os.path.basename(args.output_path), mode="online")
104 | # else:
105 | # wandb.init(mode="disabled")
106 | args.output_path = os.path.join(args.output_path, datetime.now().strftime("%Y-%m-%d_%H-%M-%S"))
107 | config = load_config(args.config)
108 | main(config, args)
--------------------------------------------------------------------------------
/src/metric/dino.py:
--------------------------------------------------------------------------------
1 | from scipy import spatial
2 | import torch
3 | import os
4 | from PIL import Image
5 | import json
6 | from torchvision import transforms
7 | from .metric import Metric
8 |
9 | transform_img = transforms.Compose([
10 | transforms.Resize(256, interpolation=3),
11 | transforms.CenterCrop(224),
12 | transforms.ToTensor(),
13 | transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225)),
14 | ])
15 |
16 | def check_image_path(text):
17 | if any(ext in text for ext in ['.jpg', '.png', '.jpeg']):
18 | if not os.path.exists(text):
19 | print("Warning: input is an image path, will load image")
20 |
21 | def preprocess_list(input, device):
22 | processed_input = []
23 | for item in input:
24 | # if os.path.isfile(item):
25 | image = Image.open(item)
26 | processed_input.append(transform_img(image).unsqueeze(0).to(device))
27 | return processed_input
28 | def _get_dino_score(input1, input2, metric_model):
29 | '''
30 | Args:
31 | - input1: list
32 | - input2: list
33 | - dino_model: dino model name or path
34 | '''
35 | with torch.no_grad():
36 | img_feature1 = metric_model(input1).detach().cpu().float()
37 | img_feature2 = metric_model(input2).detach().cpu().float()
38 | similarity = 1 - spatial.distance.cosine(img_feature1.view(img_feature1.shape[1]), img_feature2.view(img_feature2.shape[1]))
39 | return similarity
40 |
41 | class DinoScore(Metric):
42 | def __init__(self, device = 'cuda', dino_model="'dino_vits16'", batch_size=100):
43 | self.device = device
44 | self.batch_size = batch_size
45 | self.dino_model = torch.hub.load('facebookresearch/dino:main', dino_model)
46 | self.dino_model.eval()
47 | self.dino_model.to(self.device)
48 |
49 | def __call__(self, input: str, keys=['target', 'pred']):
50 | '''
51 | Args:
52 | - input: path(.json or .jsonl or dir) and param::keys = ['dir1', 'dir2']:
53 | - if dir, there must be 2 dirs in the input dir, and
54 | the number and filename of images in the two dirs must be the same. The dir looks like this:
55 |
56 | input_dir
57 | ├── dir1
58 | │ ├── img1.jpg
59 | │ ├── img2.jpg
60 | │ └── ...
61 | └── dir2
62 | ├── img1.jpg
63 | ├── img2.jpg
64 | └── ...
65 |
66 | - if .json or .jsonl the file looks like this and param::keys = ['args1', 'args2']:
67 | [
68 | {
69 | "args1": "image path",
70 | "args2": "image path,
71 | },
72 | ...
73 | {
74 | "args1": "image path",
75 | "args2": "image path,
76 | }
77 | ]
78 | Returns:
79 | - psnr score
80 | '''
81 | target_list = []
82 | pred_list = []
83 |
84 | if os.path.isdir(input):
85 | dir_path_1 = os.path.join(input, keys[0])
86 | dir_path_2 = os.path.join(input, keys[1])
87 | assert len(os.listdir(dir_path_1)) == len(os.listdir(dir_path_2)), f"the number of images in {dir_path_1} and {dir_path_2} must be the same"
88 | imgs = os.listdir(dir_path_1)
89 | for file in imgs:
90 | assert os.path.exists(os.path.join(dir_path_2, file)), f"file not exists {os.path.join(dir_path_2, file)}"
91 | target_list.append(os.path.join(dir_path_1, file))
92 | pred_list.append(os.path.join(dir_path_2, file))
93 |
94 | elif input.endswith(".json") or input.endswith(".jsonl"):
95 | with open(input, "r") as f:
96 | data = json.load(f)
97 | assert len(keys) == 2, f"keys must be 2, but got {keys}"
98 | for item in data:
99 | target_path = item[keys[0]]
100 | pred_path = item[keys[1]]
101 | target_list.append(target_path)
102 | pred_list.append(pred_path)
103 |
104 | else:
105 | raise ValueError(f"{input} must be dir or json file")
106 |
107 | assert len(target_list) == len(pred_list), f"the number of images in {input} must be the same"
108 | # processed_target_list, processed_pred_list = preprocess_list(target_list, pred_list, self.device)
109 | processed_target_list = preprocess_list(target_list, self.device)
110 | processed_pred_list = preprocess_list(pred_list, self.device)
111 | score = 0.0
112 |
113 | # print(processed_target_list, processed_pred_list)
114 |
115 | with torch.no_grad():
116 | for pred, target in zip(processed_pred_list, processed_target_list):
117 | score += _get_dino_score(pred, target, self.dino_model)
118 | score = score / len(processed_target_list)
119 | return "DINO", score, len(target_list)
120 |
121 | # print(get_ssim_batch("data/", keys=['gt', 'pred']))
--------------------------------------------------------------------------------
/src/metric/clip_score.py:
--------------------------------------------------------------------------------
1 | from torchmetrics.multimodal.clip_score import CLIPScore
2 | import torch
3 | import os
4 | from PIL import Image
5 | import json
6 | from torchvision import transforms
7 | from .metric import Metric
8 |
9 | from transformers import CLIPImageProcessor, CLIPModel, CLIPTokenizer
10 |
11 | transform_img = transforms.Compose([
12 | transforms.ToTensor(),
13 | lambda x: (x * 255)
14 | ])
15 |
16 | def check_image_path(text):
17 | if any(ext in text for ext in ['.jpg', '.png', '.jpeg']):
18 | if not os.path.exists(text):
19 | print("Warning: input is an image path, but can't load image")
20 |
21 | def preprocess_list(input, device):
22 | processed_input = []
23 | for item in input:
24 | if os.path.isfile(item):
25 | image = Image.open(item)
26 | processed_input.append(transform_img(image).to(device))
27 | else:
28 | processed_input.append(item)
29 | return processed_input
30 |
31 | def _get_clip_score(input1, input2, metric_model):
32 | '''
33 | Args:
34 | - input1: list
35 | - input2: list
36 | - clip_model: clip model name or path
37 | '''
38 | with torch.no_grad():
39 | score = metric_model(input1, input2)
40 | return score.detach().item()
41 |
42 | class ClipScore(Metric):
43 | def __init__(self, device = 'cuda', clip_model="openai/clip-vit-base-patch16"):
44 | self.device = device
45 | self.metric_model = CLIPScore(model_name_or_path=clip_model).to(self.device)
46 | self.clip_model_name = clip_model
47 | def __call__(self, input: str, keys: list=['gt', 'pred']):
48 | '''
49 | Args:
50 | - input: path(.json or .jsonl or dir) and param::keys = ['dir1', 'dir2']:
51 | - if dir, there must be 2 dirs in the input dir, and
52 | the number and filename of images in the two dirs must be the same. The dir looks like this:
53 |
54 | input_dir
55 | ├── dir1
56 | │ ├── img1.jpg
57 | │ ├── img2.jpg
58 | │ └── ...
59 | └── dir2
60 | ├── img1.jpg
61 | ├── img2.jpg
62 | └── ...
63 |
64 | - if .json or .jsonl the file looks like this and param::keys = ['args1', 'args2']:
65 | [
66 | {
67 | "args1": "image path or text",
68 | "args2": "image path or text,
69 | },
70 | ...
71 | {
72 | "args1": "image path or text",
73 | "args2": "image path or text,
74 | }
75 | ]
76 |
77 | - clip_model: clip model name or clip model path. Default: "openai/clip-vit-base-patch16"
78 | Available models are:
79 | - `"openai/clip-vit-base-patch16"`
80 | - `"openai/clip-vit-base-patch32"`
81 | - `"openai/clip-vit-large-patch14-336"`
82 | - `"openai/clip-vit-large-patch14"`
83 | Returns:
84 | - clip score
85 | '''
86 |
87 | if os.path.isdir(input):
88 |
89 | dir_path_1 = os.path.join(input, keys[0])
90 | dir_path_2 = os.path.join(input, keys[1])
91 |
92 | imgs = os.listdir(dir_path_1)
93 | input1_list = []
94 | input2_list = []
95 | for file in imgs:
96 | assert os.path.exists(os.path.join(dir_path_2, file)), f"file not exists {os.path.join(dir_path_2, file)}"
97 | input1_list.append(os.path.join(dir_path_1, file))
98 | input2_list.append(os.path.join(dir_path_2, file))
99 |
100 | elif input.endswith(".json") or input.endswith(".jsonl"):
101 | input1_list = []
102 | input2_list = []
103 | with open(input, "r") as f:
104 | data = json.load(f)
105 | assert len(keys) == 2, f"keys must be 2, but got {keys}"
106 | for item in data:
107 | text1 = item[keys[0]]
108 | text2 = item[keys[1]]
109 |
110 | # if there is a path, check if the path exists
111 | check_image_path(text1)
112 | check_image_path(text2)
113 |
114 | input1_list.append(text1)
115 | input2_list.append(text2)
116 | else:
117 | raise ValueError(f"{input} must be dir or json file")
118 |
119 | assert len(input1_list) == len(input2_list), f"the number of images in {input} must be the same"
120 |
121 | processed_input1_list = preprocess_list(input1_list, self.device)
122 | processed_input2_list = preprocess_list(input2_list, self.device)
123 | # metric_model = CLIPScore(model_name_or_path=clip_model).to(self.device)
124 | score = 0.0
125 | for input1, input2 in zip(processed_input1_list, processed_input2_list):
126 |
127 | score += _get_clip_score(input1, input2, self.metric_model)
128 |
129 | score = score / len(input1_list)
130 | return f"CLIP_score_{self.clip_model_name}", score, len(input1_list)
131 |
132 |
133 | # print(get_clip_score_batch('/data1/jiangzj/code/benchmark/data/', clip_model="openai/clip-vit-base-patch16",keys=['gt', 'pred'], device='cuda'))
--------------------------------------------------------------------------------