Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- modules/image_recommender/__init__.py +3 -0
- modules/image_recommender/analysis.py +73 -0
- modules/image_recommender/check.py +194 -0
- modules/image_recommender/create_index.py +243 -0
- modules/image_recommender/image_recommender.py +732 -0
- modules/image_recommender/modify.py +70 -0
- modules/image_recommender/prompt.json +135 -0
- modules/image_recommender/test.py +112 -0
modules/image_recommender/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Image Recommender package initialization.
|
| 3 |
+
"""
|
modules/image_recommender/analysis.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# check results_m folder and get distribution
|
| 2 |
+
import os, json
|
| 3 |
+
json_path = './results_m/'
|
| 4 |
+
|
| 5 |
+
available_list = ['image_content', 'topic', 'data_facts', 'color_style', 'size']
|
| 6 |
+
data_fact_list = ['increasing', 'decreasing', 'highlight', 'deny', 'maximum', 'minimum', 'comparison', 'none']
|
| 7 |
+
color_style_list = ['monochrome', 'colorful', 'grayscale', 'dual-color']
|
| 8 |
+
size_list = ['icon', 'clipart', 'background']
|
| 9 |
+
|
| 10 |
+
distribution = {
|
| 11 |
+
'data_facts': {},
|
| 12 |
+
'color_style': {},
|
| 13 |
+
'size': {}
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
cases = {
|
| 17 |
+
'data_facts': {},
|
| 18 |
+
'color_style': {},
|
| 19 |
+
'size': {}
|
| 20 |
+
}
|
| 21 |
+
ct = 0
|
| 22 |
+
case_limit = 10
|
| 23 |
+
image_pathes = []
|
| 24 |
+
with open('image_pathes.json', 'r') as f:
|
| 25 |
+
image_pathes = json.load(f)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
for i, file in enumerate(os.listdir(json_path)):
|
| 29 |
+
if not file.endswith('.json'):
|
| 30 |
+
continue
|
| 31 |
+
|
| 32 |
+
# load json
|
| 33 |
+
with open(f'{json_path}{file}', 'r') as f:
|
| 34 |
+
data = json.load(f)
|
| 35 |
+
|
| 36 |
+
ct += 1
|
| 37 |
+
distribution['color_style'][data['color_style']] = distribution['color_style'].get(data['color_style'], 0) + 1
|
| 38 |
+
distribution['size'][data['size']] = distribution['size'].get(data['size'], 0) + 1
|
| 39 |
+
distribution['data_facts'][data['data_facts']] = distribution['data_facts'].get(data['data_facts'], 0) + 1
|
| 40 |
+
|
| 41 |
+
file_id = int(file.split('.')[0])
|
| 42 |
+
cases['color_style'][data['color_style']] = cases['color_style'].get(data['color_style'], [])
|
| 43 |
+
if len(cases['color_style'][data['color_style']]) < case_limit:
|
| 44 |
+
cases['color_style'][data['color_style']].append((file_id, image_pathes[file_id]))
|
| 45 |
+
cases['size'][data['size']] = cases['size'].get(data['size'], [])
|
| 46 |
+
if len(cases['size'][data['size']]) < case_limit:
|
| 47 |
+
cases['size'][data['size']].append((file_id, image_pathes[file_id]))
|
| 48 |
+
cases['data_facts'][data['data_facts']] = cases['data_facts'].get(data['data_facts'], [])
|
| 49 |
+
if len(cases['data_facts'][data['data_facts']]) < case_limit:
|
| 50 |
+
cases['data_facts'][data['data_facts']].append((file_id, image_pathes[file_id]))
|
| 51 |
+
|
| 52 |
+
# save json
|
| 53 |
+
with open('distribution.json', 'w') as f:
|
| 54 |
+
json.dump(distribution, f, indent=4)
|
| 55 |
+
with open('cases.json', 'w') as f:
|
| 56 |
+
json.dump(cases, f, indent=4)
|
| 57 |
+
|
| 58 |
+
# move cases to './cases/'
|
| 59 |
+
cases_path = './cases/'
|
| 60 |
+
if not os.path.exists(cases_path):
|
| 61 |
+
os.makedirs(cases_path)
|
| 62 |
+
for key in cases:
|
| 63 |
+
for case in cases[key]:
|
| 64 |
+
for i, info in enumerate(cases[key][case]):
|
| 65 |
+
file_id, image_path = info
|
| 66 |
+
target_path = f'{cases_path}{key}/{case}/'
|
| 67 |
+
if not os.path.exists(target_path):
|
| 68 |
+
os.makedirs(target_path)
|
| 69 |
+
os.system(f'cp {image_path} {target_path}{file_id}.jpg')
|
| 70 |
+
|
| 71 |
+
print(ct)
|
| 72 |
+
|
| 73 |
+
|
modules/image_recommender/check.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 1. 扫描当前目录下所有image文件 包含png jpg jpeg webp,进行编号 存储下list
|
| 2 |
+
import os, json
|
| 3 |
+
import uuid # 添加导入uuid模块
|
| 4 |
+
image_pathes = []
|
| 5 |
+
root = '/data/lizhen/resources/image'
|
| 6 |
+
image_root = os.path.join(root, 'images')
|
| 7 |
+
image_path_file = os.path.join(root, 'image_pathes.txt')
|
| 8 |
+
result_map_file = os.path.join(root, 'result_map.txt')
|
| 9 |
+
|
| 10 |
+
# Load existing result mappings
|
| 11 |
+
result_map = {}
|
| 12 |
+
if os.path.exists(result_map_file):
|
| 13 |
+
with open(result_map_file, 'r') as f:
|
| 14 |
+
for line in f:
|
| 15 |
+
filename, result_file = line.strip().split(',')
|
| 16 |
+
result_map[filename] = result_file
|
| 17 |
+
|
| 18 |
+
existing_paths = set()
|
| 19 |
+
if os.path.exists(image_path_file):
|
| 20 |
+
with open(image_path_file, 'r') as f:
|
| 21 |
+
existing_paths = set(line.strip() for line in f.readlines())
|
| 22 |
+
image_pathes = [os.path.join(image_root, path) for path in existing_paths]
|
| 23 |
+
print(f"Loaded {len(image_pathes)} existing image paths")
|
| 24 |
+
|
| 25 |
+
print('Scanning images...')
|
| 26 |
+
new_paths = []
|
| 27 |
+
for root_dir, dirs, files in os.walk(image_root):
|
| 28 |
+
for file in files:
|
| 29 |
+
if file.lower().endswith(('.png', '.jpg', '.jpeg', '.webp')):
|
| 30 |
+
abs_path = os.path.join(root_dir, file)
|
| 31 |
+
rel_path = os.path.relpath(abs_path, image_root)
|
| 32 |
+
if rel_path not in existing_paths:
|
| 33 |
+
new_paths.append(rel_path)
|
| 34 |
+
image_pathes.append(abs_path)
|
| 35 |
+
|
| 36 |
+
if new_paths:
|
| 37 |
+
print(f"Found {len(new_paths)} new images")
|
| 38 |
+
with open(image_path_file, 'a') as f:
|
| 39 |
+
for path in new_paths:
|
| 40 |
+
f.write(path + '\n')
|
| 41 |
+
|
| 42 |
+
print(f"Total images: {len(image_pathes)}")
|
| 43 |
+
|
| 44 |
+
from openai import OpenAI
|
| 45 |
+
from PIL import Image
|
| 46 |
+
import base64
|
| 47 |
+
from io import BytesIO
|
| 48 |
+
import requests
|
| 49 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 50 |
+
import threading
|
| 51 |
+
|
| 52 |
+
client = OpenAI(
|
| 53 |
+
api_key=os.getenv("OPENAI_API_KEY") or os.getenv("AIHUBMIX_API_KEY", ""),
|
| 54 |
+
base_url=os.getenv("OPENAI_BASE_URL", "https://aihubmix.com/v1")
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
def resize_image(img, max_size=512):
|
| 58 |
+
width, height = img.size
|
| 59 |
+
ratio = min(max_size / width, max_size / height)
|
| 60 |
+
if ratio >= 1:
|
| 61 |
+
return img
|
| 62 |
+
new_width = int(width * ratio)
|
| 63 |
+
new_height = int(height * ratio)
|
| 64 |
+
resized_img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
| 65 |
+
return resized_img
|
| 66 |
+
|
| 67 |
+
def repaint_image(img):
|
| 68 |
+
# rapaint transparent area with white color
|
| 69 |
+
img = img.convert('RGBA')
|
| 70 |
+
data = img.getdata()
|
| 71 |
+
new_data = []
|
| 72 |
+
for item in data:
|
| 73 |
+
if item[3] == 0:
|
| 74 |
+
new_data.append((255, 255, 255, 255))
|
| 75 |
+
else:
|
| 76 |
+
new_data.append(item)
|
| 77 |
+
img.putdata(new_data)
|
| 78 |
+
img = img.convert('RGB')
|
| 79 |
+
# img.save('temp.png')
|
| 80 |
+
return img
|
| 81 |
+
|
| 82 |
+
def image_to_base64(image_path, show=False, target_size=512):
|
| 83 |
+
with Image.open(image_path) as img:
|
| 84 |
+
# img = img.resize(size)
|
| 85 |
+
img = resize_image(img, 512)
|
| 86 |
+
img = repaint_image(img)
|
| 87 |
+
buffered = BytesIO()
|
| 88 |
+
img.save(buffered, format="PNG")
|
| 89 |
+
img_base64 = base64.b64encode(buffered.getvalue()).decode('utf-8')
|
| 90 |
+
return img_base64
|
| 91 |
+
|
| 92 |
+
wwxxhh = 0
|
| 93 |
+
def ask_image(prompt, image_data):
|
| 94 |
+
number_of_trials = 0
|
| 95 |
+
while number_of_trials < 5:
|
| 96 |
+
try:
|
| 97 |
+
response = requests.post(
|
| 98 |
+
"https://aihubmix.com/v1/chat/completions",
|
| 99 |
+
headers={
|
| 100 |
+
"Authorization": f"Bearer {os.getenv('OPENAI_API_KEY') or os.getenv('AIHUBMIX_API_KEY', '')}",
|
| 101 |
+
"Content-Type": "application/json"
|
| 102 |
+
},
|
| 103 |
+
json={
|
| 104 |
+
"model": "gemini-2.0-flash",
|
| 105 |
+
"messages": [{
|
| 106 |
+
"role": "user",
|
| 107 |
+
"content": [
|
| 108 |
+
{"type": "text", "text": prompt},
|
| 109 |
+
{
|
| 110 |
+
"type": "image_url",
|
| 111 |
+
"image_url": {
|
| 112 |
+
"url": f"data:image/jpeg;base64,{image_data}"
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
]
|
| 116 |
+
}]
|
| 117 |
+
}
|
| 118 |
+
)
|
| 119 |
+
if response.status_code == 200:
|
| 120 |
+
return response.json()['choices'][0]['message']['content']
|
| 121 |
+
else:
|
| 122 |
+
print(f"Error status code: {response.status_code}")
|
| 123 |
+
number_of_trials += 1
|
| 124 |
+
except Exception as e:
|
| 125 |
+
print(f"Request error: {e}")
|
| 126 |
+
number_of_trials += 1
|
| 127 |
+
|
| 128 |
+
return 'Error!'
|
| 129 |
+
|
| 130 |
+
# 2. 读取prompt.json文件,读取整个作为字符串,逐个读取image文件,调用ask_image函数,将返回的结果存储下来
|
| 131 |
+
import json
|
| 132 |
+
with open('modules/image_recommender/prompt.json', 'r') as f:
|
| 133 |
+
prompt = f.read()
|
| 134 |
+
# print(prompt)
|
| 135 |
+
|
| 136 |
+
results_path = os.path.join(root, 'results')
|
| 137 |
+
if not os.path.exists(results_path):
|
| 138 |
+
os.makedirs(results_path)
|
| 139 |
+
|
| 140 |
+
def process_image(args):
|
| 141 |
+
i, image_path, prompt, results_path = args
|
| 142 |
+
rel_path = os.path.relpath(image_path, image_root)
|
| 143 |
+
|
| 144 |
+
# 使用UUID生成��机文件名,而不是使用索引
|
| 145 |
+
random_filename = str(uuid.uuid4())
|
| 146 |
+
target_path = os.path.join(results_path, f'{random_filename}.json')
|
| 147 |
+
|
| 148 |
+
# Skip if already processed
|
| 149 |
+
if rel_path in result_map:
|
| 150 |
+
print(f'Skipping {i+1}/{len(image_pathes)} (already exists in result map)')
|
| 151 |
+
return
|
| 152 |
+
|
| 153 |
+
print(f'Processing {i+1}/{len(image_pathes)}')
|
| 154 |
+
try:
|
| 155 |
+
image_data = image_to_base64(image_path)
|
| 156 |
+
result = ask_image(prompt, image_data)
|
| 157 |
+
try:
|
| 158 |
+
result = json.loads(result)
|
| 159 |
+
except:
|
| 160 |
+
result = result.replace('```json', '').replace('```', '')
|
| 161 |
+
result = json.loads(result)
|
| 162 |
+
|
| 163 |
+
# Add filename and remove explanation
|
| 164 |
+
result['filename'] = rel_path
|
| 165 |
+
if 'explanation' in result:
|
| 166 |
+
del result['explanation']
|
| 167 |
+
|
| 168 |
+
with open(target_path, 'w') as f:
|
| 169 |
+
json.dump(result, f)
|
| 170 |
+
|
| 171 |
+
# Update result mapping
|
| 172 |
+
with open(result_map_file, 'a') as f:
|
| 173 |
+
f.write(f"{rel_path},{target_path}\n")
|
| 174 |
+
result_map[rel_path] = target_path
|
| 175 |
+
except Exception as e:
|
| 176 |
+
print(f'Failed to process {i+1}/{len(image_pathes)}: {str(e)}')
|
| 177 |
+
|
| 178 |
+
# Pre-scan for existing results
|
| 179 |
+
print("Pre-scanning for existing results...")
|
| 180 |
+
results_path = os.path.join(root, 'results')
|
| 181 |
+
if not os.path.exists(results_path):
|
| 182 |
+
os.makedirs(results_path)
|
| 183 |
+
|
| 184 |
+
# Filter out already processed images
|
| 185 |
+
image_pathes = [path for path in image_pathes if os.path.relpath(path, image_root) not in result_map]
|
| 186 |
+
print(f"Remaining images to process: {len(image_pathes)}")
|
| 187 |
+
|
| 188 |
+
# Main processing loop with thread pool
|
| 189 |
+
num_threads = 20
|
| 190 |
+
with ThreadPoolExecutor(max_workers=num_threads) as executor:
|
| 191 |
+
tasks = [
|
| 192 |
+
(i, image_pathes[i], prompt, results_path) for i in range(len(image_pathes))
|
| 193 |
+
]
|
| 194 |
+
executor.map(process_image, tasks)
|
modules/image_recommender/create_index.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import json
|
| 4 |
+
import numpy as np
|
| 5 |
+
import faiss
|
| 6 |
+
from tqdm import tqdm
|
| 7 |
+
from utils.model_loader import ModelLoader
|
| 8 |
+
|
| 9 |
+
# Add the project root directory to Python path
|
| 10 |
+
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 11 |
+
sys.path.append(project_root)
|
| 12 |
+
|
| 13 |
+
from typing import Optional, List, Dict, Union
|
| 14 |
+
|
| 15 |
+
class ImageRecommender:
|
| 16 |
+
def __init__(self, embed_model_path: str):
|
| 17 |
+
self.model = ModelLoader.get_model(embed_model_path)
|
| 18 |
+
self.index = None
|
| 19 |
+
self.image_paths = []
|
| 20 |
+
self.image_data = []
|
| 21 |
+
self.icon_indices = [] # Store indices of icon images
|
| 22 |
+
self.clipart_indices = [] # Store indices of clipart images
|
| 23 |
+
|
| 24 |
+
def create_index(self, image_list_path: str, image_resource_path: str):
|
| 25 |
+
"""Create FAISS index from image embeddings"""
|
| 26 |
+
# Read image paths
|
| 27 |
+
with open(image_list_path, 'r') as f:
|
| 28 |
+
self.image_paths = [line.strip().split(',') for line in f.readlines()]
|
| 29 |
+
|
| 30 |
+
# Load and process each image's data
|
| 31 |
+
embeddings = []
|
| 32 |
+
for idx, (image_path, json_path) in enumerate(tqdm(self.image_paths)):
|
| 33 |
+
# Construct full path to the image's JSON data
|
| 34 |
+
#json_path = os.path.join(image_resource_path, f'results/{idx}.json')
|
| 35 |
+
|
| 36 |
+
if not os.path.exists(json_path):
|
| 37 |
+
continue
|
| 38 |
+
|
| 39 |
+
try:
|
| 40 |
+
with open(json_path, 'r') as f:
|
| 41 |
+
try:
|
| 42 |
+
image_info = json.load(f)
|
| 43 |
+
except json.JSONDecodeError as e:
|
| 44 |
+
print(f"Error decoding JSON at index {idx}: {str(e)}")
|
| 45 |
+
continue
|
| 46 |
+
|
| 47 |
+
# Combine multiple fields for better semantic representation
|
| 48 |
+
semantic_text = ""
|
| 49 |
+
try:
|
| 50 |
+
image_content = image_info.get('image_content', '')
|
| 51 |
+
topic = image_info.get('topic', '')
|
| 52 |
+
explanation = image_info.get('explanation', '')
|
| 53 |
+
|
| 54 |
+
if image_content:
|
| 55 |
+
semantic_text += f"{image_content}"
|
| 56 |
+
if topic:
|
| 57 |
+
semantic_text += f". {topic}"
|
| 58 |
+
if explanation:
|
| 59 |
+
semantic_text += f". {explanation}"
|
| 60 |
+
|
| 61 |
+
if not semantic_text:
|
| 62 |
+
semantic_text = "Image without description"
|
| 63 |
+
print(f"Warning: Missing semantic information for image at index {idx}")
|
| 64 |
+
except Exception as e:
|
| 65 |
+
semantic_text = "Image without description"
|
| 66 |
+
print(f"Error processing semantic text at index {idx}: {str(e)}")
|
| 67 |
+
|
| 68 |
+
# Generate embedding
|
| 69 |
+
try:
|
| 70 |
+
embedding = self.model.encode(semantic_text)
|
| 71 |
+
embeddings.append(embedding)
|
| 72 |
+
self.image_data.append(image_info)
|
| 73 |
+
|
| 74 |
+
# Store indices based on image type
|
| 75 |
+
if image_info.get('icon_or_clipart') == 'icon':
|
| 76 |
+
self.icon_indices.append(len(embeddings) - 1)
|
| 77 |
+
elif image_info.get('icon_or_clipart') == 'clipart':
|
| 78 |
+
self.clipart_indices.append(len(embeddings) - 1)
|
| 79 |
+
except Exception as e:
|
| 80 |
+
print(f"Error generating embedding at index {idx}: {str(e)}")
|
| 81 |
+
continue
|
| 82 |
+
|
| 83 |
+
except Exception as e:
|
| 84 |
+
print(f"Error processing image at index {idx}: {str(e)}")
|
| 85 |
+
continue
|
| 86 |
+
|
| 87 |
+
self.image_paths = [image_path for image_path, _ in self.image_paths]
|
| 88 |
+
embeddings = np.array(embeddings).astype('float32')
|
| 89 |
+
|
| 90 |
+
# Create and train FAISS index
|
| 91 |
+
dimension = embeddings.shape[1]
|
| 92 |
+
self.index = faiss.IndexFlatL2(dimension)
|
| 93 |
+
self.index.add(embeddings)
|
| 94 |
+
|
| 95 |
+
def save_index(self, index_path, data_path):
|
| 96 |
+
"""Save the FAISS index and associated data"""
|
| 97 |
+
faiss.write_index(self.index, index_path)
|
| 98 |
+
|
| 99 |
+
# Save image data and indices
|
| 100 |
+
with open(data_path, 'w') as f:
|
| 101 |
+
json.dump({
|
| 102 |
+
'paths': self.image_paths,
|
| 103 |
+
'data': self.image_data,
|
| 104 |
+
'icon_indices': self.icon_indices,
|
| 105 |
+
'clipart_indices': self.clipart_indices
|
| 106 |
+
}, f)
|
| 107 |
+
|
| 108 |
+
def load_index(self, index_path, data_path):
|
| 109 |
+
"""Load the FAISS index and associated data"""
|
| 110 |
+
self.index = faiss.read_index(index_path)
|
| 111 |
+
|
| 112 |
+
with open(data_path, 'r') as f:
|
| 113 |
+
data = json.load(f)
|
| 114 |
+
self.image_paths = data['paths']
|
| 115 |
+
self.image_data = data['data']
|
| 116 |
+
self.icon_indices = data['icon_indices']
|
| 117 |
+
self.clipart_indices = data['clipart_indices']
|
| 118 |
+
|
| 119 |
+
def search(self, query_text: str, new_index = None, new_data = None, top_k: int = 5, image_type: Optional[str] = None) -> List[Dict]:
|
| 120 |
+
"""
|
| 121 |
+
Search for similar images based on query text
|
| 122 |
+
|
| 123 |
+
Args:
|
| 124 |
+
query_text: The text query to search for
|
| 125 |
+
new_index: Optional new FAISS index to search in addition
|
| 126 |
+
new_data: Optional new data associated with new_index
|
| 127 |
+
top_k: Number of results to return
|
| 128 |
+
image_type: Optional filter for image type ('icon' or 'clipart')
|
| 129 |
+
|
| 130 |
+
Returns:
|
| 131 |
+
List of dictionaries containing image information and similarity scores
|
| 132 |
+
"""
|
| 133 |
+
if self.index is None:
|
| 134 |
+
raise ValueError("Index not loaded. Please load the index first.")
|
| 135 |
+
|
| 136 |
+
# Generate query embedding
|
| 137 |
+
query_embedding = self.model.encode(query_text)
|
| 138 |
+
query_embedding = np.array([query_embedding]).astype('float32')
|
| 139 |
+
|
| 140 |
+
results = []
|
| 141 |
+
|
| 142 |
+
# 搜索旧索引
|
| 143 |
+
# Determine which indices to search in
|
| 144 |
+
if image_type == 'icon':
|
| 145 |
+
search_indices = self.icon_indices
|
| 146 |
+
elif image_type == 'clipart':
|
| 147 |
+
search_indices = self.clipart_indices
|
| 148 |
+
else:
|
| 149 |
+
search_indices = None
|
| 150 |
+
|
| 151 |
+
if search_indices:
|
| 152 |
+
# Create a subset index for the specific image type
|
| 153 |
+
subset_index = faiss.IndexFlatL2(self.index.d)
|
| 154 |
+
subset_index.add(self.index.reconstruct_n(0, self.index.ntotal)[search_indices])
|
| 155 |
+
|
| 156 |
+
# Search in the subset index
|
| 157 |
+
distances, indices = subset_index.search(query_embedding, top_k)
|
| 158 |
+
# Map back to original indices
|
| 159 |
+
indices = [search_indices[i] for i in indices[0]]
|
| 160 |
+
distances = distances[0]
|
| 161 |
+
else:
|
| 162 |
+
# Search in the full index
|
| 163 |
+
distances, indices = self.index.search(query_embedding, top_k)
|
| 164 |
+
indices = indices[0]
|
| 165 |
+
distances = distances[0]
|
| 166 |
+
|
| 167 |
+
# 添加旧索引结果
|
| 168 |
+
for idx, distance in zip(indices, distances):
|
| 169 |
+
if idx < len(self.image_paths): # Ensure index is valid
|
| 170 |
+
results.append({
|
| 171 |
+
'image_path': self.image_paths[idx],
|
| 172 |
+
'image_data': self.image_data[idx],
|
| 173 |
+
'distance': float(distance)
|
| 174 |
+
})
|
| 175 |
+
|
| 176 |
+
# 如果是icon类型且有新索引,搜索新索引
|
| 177 |
+
if image_type == 'icon' and new_index is not None and new_data is not None:
|
| 178 |
+
new_distances, new_indices = new_index.search(query_embedding, top_k)
|
| 179 |
+
new_indices = new_indices[0]
|
| 180 |
+
new_distances = new_distances[0]
|
| 181 |
+
|
| 182 |
+
# 添加新索引结果
|
| 183 |
+
for idx, distance in zip(new_indices, new_distances):
|
| 184 |
+
if idx < len(new_data['index']):
|
| 185 |
+
data = new_data['index'][str(idx)]
|
| 186 |
+
# print("data: ", data)
|
| 187 |
+
results.append({
|
| 188 |
+
'image_path': data["path"],
|
| 189 |
+
'image_data': data["data"],
|
| 190 |
+
'distance': float(distance) + 0.1
|
| 191 |
+
})
|
| 192 |
+
|
| 193 |
+
# 按距离排序并返回前top_k个结果
|
| 194 |
+
results.sort(key=lambda x: x['distance'])
|
| 195 |
+
return results[:top_k]
|
| 196 |
+
|
| 197 |
+
def main(image_list_path: str = None,
|
| 198 |
+
image_resource_path: str = None,
|
| 199 |
+
index_path: str = None,
|
| 200 |
+
data_path: str = None,
|
| 201 |
+
embed_model_path: str = None,
|
| 202 |
+
force: bool = False):
|
| 203 |
+
"""
|
| 204 |
+
Create and save the image index
|
| 205 |
+
|
| 206 |
+
Args:
|
| 207 |
+
image_list_path: Path to the file containing list of image paths
|
| 208 |
+
image_resource_path: Path to the directory containing image resources
|
| 209 |
+
index_path: Path to save the FAISS index
|
| 210 |
+
data_path: Path to save the image data
|
| 211 |
+
embed_model_path: Path to the sentence embedding model
|
| 212 |
+
force: Whether to force rebuild the index if it exists
|
| 213 |
+
"""
|
| 214 |
+
# Check if index exists and handle force flag
|
| 215 |
+
if os.path.exists(index_path) and not force:
|
| 216 |
+
print(f"Index file {index_path} already exists. Use --force to rebuild.")
|
| 217 |
+
return 0
|
| 218 |
+
|
| 219 |
+
recommender = ImageRecommender(embed_model_path)
|
| 220 |
+
recommender.create_index(image_list_path, image_resource_path)
|
| 221 |
+
recommender.save_index(index_path, data_path)
|
| 222 |
+
print("Index built successfully!")
|
| 223 |
+
return 0
|
| 224 |
+
|
| 225 |
+
if __name__ == '__main__':
|
| 226 |
+
import argparse
|
| 227 |
+
parser = argparse.ArgumentParser(description='Build image index using FAISS and SentenceTransformer')
|
| 228 |
+
parser.add_argument('--image_list_path', type=str, required=True, help='Path to the file containing list of image paths')
|
| 229 |
+
parser.add_argument('--image_resource_path', type=str, required=True, help='Path to the directory containing image resources')
|
| 230 |
+
parser.add_argument('--index_path', type=str, required=True, help='Path to save the FAISS index')
|
| 231 |
+
parser.add_argument('--data_path', type=str, required=True, help='Path to save the image data')
|
| 232 |
+
parser.add_argument('--embed_model_path', type=str, required=True, help='Path to the sentence embedding model')
|
| 233 |
+
parser.add_argument('--force', action='store_true', help='Force rebuild even if index exists')
|
| 234 |
+
|
| 235 |
+
args = parser.parse_args()
|
| 236 |
+
main(
|
| 237 |
+
image_list_path=args.image_list_path,
|
| 238 |
+
image_resource_path=args.image_resource_path,
|
| 239 |
+
index_path=args.index_path,
|
| 240 |
+
data_path=args.data_path,
|
| 241 |
+
embed_model_path=args.embed_model_path,
|
| 242 |
+
force=args.force
|
| 243 |
+
)
|
modules/image_recommender/image_recommender.py
ADDED
|
@@ -0,0 +1,732 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
import requests
|
| 4 |
+
from typing import Dict, List, Optional
|
| 5 |
+
import pandas as pd
|
| 6 |
+
import faiss
|
| 7 |
+
from logging import getLogger
|
| 8 |
+
from utils.model_loader import ModelLoader
|
| 9 |
+
logger = getLogger(__name__)
|
| 10 |
+
|
| 11 |
+
class ImageRecommender:
|
| 12 |
+
def __init__(self, embed_model_path: str = None, resource_path: str = None, data_path: str = None, index_path: str = None, base_url: str = None, api_key: str = None):
|
| 13 |
+
self.index_builder = None
|
| 14 |
+
if embed_model_path and data_path and index_path:
|
| 15 |
+
from .create_index import ImageRecommender as IndexBuilder
|
| 16 |
+
self.index_builder = IndexBuilder(embed_model_path)
|
| 17 |
+
self.index_builder.load_index(index_path, data_path)
|
| 18 |
+
self.model = ModelLoader.get_model(embed_model_path)
|
| 19 |
+
self.base_url = base_url
|
| 20 |
+
self.request_url = self.base_url + '/chat/completions'
|
| 21 |
+
self.api_key = api_key
|
| 22 |
+
self.resource_path = resource_path
|
| 23 |
+
self.normal_icons = os.path.join(resource_path, 'images')
|
| 24 |
+
self.special_icons = os.path.join(resource_path, 'special_icons')
|
| 25 |
+
self.special_icon_index = os.path.join(self.special_icons, 'data.json')
|
| 26 |
+
self.newicon_path = os.path.join(resource_path, 'attribute_icons')
|
| 27 |
+
self.newicon_data_path = os.path.join(self.newicon_path, 'index.json')
|
| 28 |
+
self.newicon_data = json.load(open(self.newicon_data_path))
|
| 29 |
+
self.newicon_faiss = os.path.join(self.newicon_path, 'faiss.index')
|
| 30 |
+
self.newicon_index = faiss.read_index(self.newicon_faiss)
|
| 31 |
+
self.special_categories = ["country", "emotion"]
|
| 32 |
+
|
| 33 |
+
def create_query_text_for_value(self, input_data: Dict, group_value: str, group_col: str) -> str:
|
| 34 |
+
"""Create a text query from input data for finding similar images."""
|
| 35 |
+
# titles = input_data.get("titles", {})
|
| 36 |
+
text_parts = []
|
| 37 |
+
|
| 38 |
+
text_parts.append(group_value)
|
| 39 |
+
columns = [col["name"] + " (" + col["description"] + ")" for col in input_data.get("data", {}).get("columns", []) if col["name"] == group_col][0]
|
| 40 |
+
text_parts.append(" in " + columns)
|
| 41 |
+
|
| 42 |
+
# if "main_title" in titles:
|
| 43 |
+
# text_parts.append(titles["main_title"])
|
| 44 |
+
|
| 45 |
+
return "; ".join(text_parts)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def create_query_text(self, input_data: Dict) -> str:
|
| 49 |
+
"""Create a text query from input data for finding similar images."""
|
| 50 |
+
titles = input_data.get("titles", {})
|
| 51 |
+
data_facts = input_data.get("datafacts", [])
|
| 52 |
+
|
| 53 |
+
text_parts = []
|
| 54 |
+
|
| 55 |
+
# Add metadata
|
| 56 |
+
if "main_title" in titles:
|
| 57 |
+
text_parts.append(titles["main_title"])
|
| 58 |
+
if "sub_title" in titles:
|
| 59 |
+
text_parts.append(titles["sub_title"])
|
| 60 |
+
# columns = [col["name"] + " (" + col["description"] + ")" for col in input_data.get("data", {}).get("columns", [])]
|
| 61 |
+
# text_parts.append("Columns: " + ", ".join(columns))
|
| 62 |
+
|
| 63 |
+
for fact in data_facts[:1]:
|
| 64 |
+
text_parts.append(f'{fact["subtype"]} {fact["type"]}, {fact["annotation"]}')
|
| 65 |
+
|
| 66 |
+
return "; ".join(text_parts)
|
| 67 |
+
|
| 68 |
+
def select_optimal_icons(self, group_icons: Dict, embeddings: Dict) -> Dict:
|
| 69 |
+
"""
|
| 70 |
+
Select optimal icons for each group value maximizing pairwise similarity.
|
| 71 |
+
|
| 72 |
+
Args:
|
| 73 |
+
group_icons: Dictionary mapping group values to lists of icon candidates
|
| 74 |
+
embeddings: Dictionary mapping image paths to their embeddings
|
| 75 |
+
|
| 76 |
+
Returns:
|
| 77 |
+
Dictionary mapping group values to selected optimal icons
|
| 78 |
+
"""
|
| 79 |
+
from scipy.spatial.distance import cosine
|
| 80 |
+
import numpy as np
|
| 81 |
+
|
| 82 |
+
selected_icons = {}
|
| 83 |
+
used_images = set()
|
| 84 |
+
|
| 85 |
+
# Sort group values by number of candidates (ascending)
|
| 86 |
+
sorted_groups = sorted(group_icons.keys(), key=lambda x: len(group_icons[x]))
|
| 87 |
+
|
| 88 |
+
for group_value in sorted_groups:
|
| 89 |
+
candidates = group_icons[group_value]
|
| 90 |
+
best_score = float('-inf')
|
| 91 |
+
best_candidate = None
|
| 92 |
+
|
| 93 |
+
for candidate in candidates:
|
| 94 |
+
if candidate['image_path'] in used_images:
|
| 95 |
+
continue
|
| 96 |
+
|
| 97 |
+
# If this is the first selection, just use similarity score
|
| 98 |
+
if not selected_icons:
|
| 99 |
+
if candidate['similarity_score'] > best_score:
|
| 100 |
+
best_score = candidate['similarity_score']
|
| 101 |
+
best_candidate = candidate
|
| 102 |
+
continue
|
| 103 |
+
|
| 104 |
+
# Calculate average dissimilarity with previously selected icons
|
| 105 |
+
candidate_embedding = embeddings[candidate['image_path']]
|
| 106 |
+
dissimilarities = []
|
| 107 |
+
for selected in selected_icons.values():
|
| 108 |
+
selected_embedding = embeddings[selected['image_path']]
|
| 109 |
+
# Use cosine distance directly as dissimilarity measure
|
| 110 |
+
dissimilarity = cosine(candidate_embedding, selected_embedding)
|
| 111 |
+
dissimilarities.append(dissimilarity)
|
| 112 |
+
|
| 113 |
+
# Combine original similarity score with average dissimilarity
|
| 114 |
+
# Higher dissimilarity is better for diversity
|
| 115 |
+
avg_dissimilarity = np.mean(dissimilarities)
|
| 116 |
+
combined_score = 0.3 * candidate['similarity_score'] + 0.7 * avg_dissimilarity
|
| 117 |
+
|
| 118 |
+
if combined_score > best_score:
|
| 119 |
+
best_score = combined_score
|
| 120 |
+
best_candidate = candidate
|
| 121 |
+
|
| 122 |
+
if best_candidate:
|
| 123 |
+
selected_icons[group_value] = best_candidate
|
| 124 |
+
used_images.add(best_candidate['image_path'])
|
| 125 |
+
return selected_icons
|
| 126 |
+
|
| 127 |
+
def get_semantic_text(self, image_data: Dict) -> str:
|
| 128 |
+
"""
|
| 129 |
+
Get semantic text from image data
|
| 130 |
+
"""
|
| 131 |
+
semantic_text = ""
|
| 132 |
+
image_content = image_data.get('image_content', '')
|
| 133 |
+
topic = image_data.get('topic', '')
|
| 134 |
+
color_style = image_data.get('color_style', '')
|
| 135 |
+
name = image_data.get('name', '')
|
| 136 |
+
|
| 137 |
+
if image_content:
|
| 138 |
+
semantic_text += f"{image_content}"
|
| 139 |
+
if topic:
|
| 140 |
+
semantic_text += f". {topic}"
|
| 141 |
+
if color_style:
|
| 142 |
+
semantic_text += f". {color_style}"
|
| 143 |
+
if name:
|
| 144 |
+
semantic_text += f". {name}"
|
| 145 |
+
|
| 146 |
+
return semantic_text
|
| 147 |
+
|
| 148 |
+
def process_special_icons(self, category: str, unique_values: List) -> Optional[Dict]:
|
| 149 |
+
"""
|
| 150 |
+
Process icons for special categories (country, sports)
|
| 151 |
+
|
| 152 |
+
Args:
|
| 153 |
+
category: Category name (e.g. "country", "sports")
|
| 154 |
+
unique_values: List of unique values to match icons for
|
| 155 |
+
|
| 156 |
+
Returns:
|
| 157 |
+
Dictionary mapping values to icons, or None if processing fails
|
| 158 |
+
"""
|
| 159 |
+
try:
|
| 160 |
+
# Load special icons data
|
| 161 |
+
with open(self.special_icon_index, 'r') as f:
|
| 162 |
+
special_icons = json.load(f)
|
| 163 |
+
|
| 164 |
+
if category not in special_icons:
|
| 165 |
+
return None
|
| 166 |
+
|
| 167 |
+
# Randomly select a type
|
| 168 |
+
import random
|
| 169 |
+
available_types = list(special_icons[category].keys())
|
| 170 |
+
if category == "country":
|
| 171 |
+
selected_type = "circle"
|
| 172 |
+
else:
|
| 173 |
+
selected_type = random.choice(available_types)
|
| 174 |
+
icons = special_icons[category][selected_type]
|
| 175 |
+
|
| 176 |
+
model = self.model
|
| 177 |
+
|
| 178 |
+
icon_names = [icon["name"] for icon in icons]
|
| 179 |
+
icon_embeddings = model.encode(icon_names)
|
| 180 |
+
|
| 181 |
+
result = {}
|
| 182 |
+
for value in unique_values:
|
| 183 |
+
# Convert numpy types to Python native types
|
| 184 |
+
value_str = str(value.item() if hasattr(value, 'item') else value)
|
| 185 |
+
value_embedding = model.encode([value_str])[0]
|
| 186 |
+
|
| 187 |
+
# Calculate similarities
|
| 188 |
+
from scipy.spatial.distance import cosine
|
| 189 |
+
similarities = [1 - cosine(value_embedding, icon_emb) for icon_emb in icon_embeddings]
|
| 190 |
+
best_idx = max(range(len(similarities)), key=lambda i: similarities[i])
|
| 191 |
+
|
| 192 |
+
result[value_str] = {
|
| 193 |
+
"image_path": os.path.join(self.special_icons, icons[best_idx]["path"]),
|
| 194 |
+
"similarity_score": similarities[best_idx]
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
return result
|
| 198 |
+
|
| 199 |
+
except Exception as e:
|
| 200 |
+
logger.error(f"Error processing special icons: {str(e)}")
|
| 201 |
+
return None
|
| 202 |
+
|
| 203 |
+
def process_normal_icons(self, input_data: Dict, unique_values: List, group_col: str) -> Dict:
|
| 204 |
+
"""
|
| 205 |
+
Process icons for normal categories using image search
|
| 206 |
+
|
| 207 |
+
Args:
|
| 208 |
+
input_data: Input data dictionary
|
| 209 |
+
unique_values: List of unique values to find icons for
|
| 210 |
+
|
| 211 |
+
Returns:
|
| 212 |
+
Dictionary mapping values to icons
|
| 213 |
+
"""
|
| 214 |
+
all_group_icons = {}
|
| 215 |
+
for value in unique_values:
|
| 216 |
+
group_query = self.create_query_text_for_value(input_data, str(value), group_col)
|
| 217 |
+
group_icons = self.index_builder.search(group_query, top_k=20, new_index=self.newicon_index, new_data=self.newicon_data, image_type='icon')
|
| 218 |
+
|
| 219 |
+
all_group_icons[str(value)] = [
|
| 220 |
+
{
|
| 221 |
+
"image_path": img["image_path"],
|
| 222 |
+
"image_data": img["image_data"],
|
| 223 |
+
"similarity_score": 1.0 / (1.0 + float(img["distance"]))
|
| 224 |
+
}
|
| 225 |
+
for img in group_icons
|
| 226 |
+
]
|
| 227 |
+
|
| 228 |
+
# Get embeddings for all candidate images using the model directly
|
| 229 |
+
all_images = {img["image_path"]: img["image_data"]
|
| 230 |
+
for group_candidates in all_group_icons.values()
|
| 231 |
+
for img in group_candidates}
|
| 232 |
+
|
| 233 |
+
for key in all_images:
|
| 234 |
+
all_images[key] = self.model.encode(self.get_semantic_text(all_images[key]))
|
| 235 |
+
|
| 236 |
+
# Select optimal icons
|
| 237 |
+
return self.select_optimal_icons(all_group_icons, all_images)
|
| 238 |
+
def post_process_image(self, image_path: str, max_size: int = 768) -> str:
|
| 239 |
+
"""
|
| 240 |
+
Post-process the recommended image:
|
| 241 |
+
1. Convert relative path to absolute path
|
| 242 |
+
2. Remove white background that connects to edges while preserving inner fills
|
| 243 |
+
3. Resize to specified resolution
|
| 244 |
+
4. Convert to base64 string
|
| 245 |
+
|
| 246 |
+
Args:
|
| 247 |
+
image_path: Relative path to the image
|
| 248 |
+
max_size: Maximum dimension for resizing (default: 768)
|
| 249 |
+
|
| 250 |
+
Returns:
|
| 251 |
+
Base64 encoded string of the processed image
|
| 252 |
+
"""
|
| 253 |
+
try:
|
| 254 |
+
from PIL import Image
|
| 255 |
+
import numpy as np
|
| 256 |
+
import base64
|
| 257 |
+
import io
|
| 258 |
+
from scipy.ndimage import label
|
| 259 |
+
|
| 260 |
+
# Get absolute path
|
| 261 |
+
abs_path = os.path.join(self.normal_icons, image_path)
|
| 262 |
+
if not os.path.exists(abs_path):
|
| 263 |
+
abs_path = os.path.join(self.newicon_path, image_path)
|
| 264 |
+
if not os.path.exists(abs_path):
|
| 265 |
+
logger.error(f"Image not found: {abs_path}")
|
| 266 |
+
return ""
|
| 267 |
+
|
| 268 |
+
# Open and convert image
|
| 269 |
+
img = Image.open(abs_path)
|
| 270 |
+
|
| 271 |
+
# Resize image while maintaining aspect ratio
|
| 272 |
+
if img.width > max_size or img.height > max_size:
|
| 273 |
+
ratio = min(max_size / img.width, max_size / img.height)
|
| 274 |
+
new_width = int(img.width * ratio)
|
| 275 |
+
new_height = int(img.height * ratio)
|
| 276 |
+
img = img.resize((new_width, new_height), Image.LANCZOS)
|
| 277 |
+
|
| 278 |
+
if img.mode != 'RGBA':
|
| 279 |
+
img = img.convert('RGBA')
|
| 280 |
+
|
| 281 |
+
# Convert to numpy array for processing
|
| 282 |
+
data = np.array(img)
|
| 283 |
+
|
| 284 |
+
# Create mask for white background (with tolerance)
|
| 285 |
+
white_color = np.array([255, 255, 255])
|
| 286 |
+
tolerance = 30
|
| 287 |
+
color_dists = np.sqrt(np.sum((data[..., :3] - white_color)**2, axis=2))
|
| 288 |
+
bg_mask = color_dists < tolerance
|
| 289 |
+
|
| 290 |
+
# Find connected components
|
| 291 |
+
labeled, num_components = label(bg_mask)
|
| 292 |
+
|
| 293 |
+
# Create border mask
|
| 294 |
+
border_mask = np.zeros_like(bg_mask, dtype=bool)
|
| 295 |
+
border_mask[0, :] = border_mask[-1, :] = True
|
| 296 |
+
border_mask[:, 0] = border_mask[:, -1] = True
|
| 297 |
+
|
| 298 |
+
# Find components that touch the border
|
| 299 |
+
border_components = set(labeled[border_mask & (labeled > 0)])
|
| 300 |
+
|
| 301 |
+
# Create mask for border-connected white areas
|
| 302 |
+
outer_mask = np.zeros_like(bg_mask, dtype=bool)
|
| 303 |
+
for component in border_components:
|
| 304 |
+
outer_mask = outer_mask | (labeled == component)
|
| 305 |
+
|
| 306 |
+
# Set alpha to 0 for border-connected white areas
|
| 307 |
+
data[outer_mask, 3] = 0
|
| 308 |
+
|
| 309 |
+
# Convert back to PIL Image
|
| 310 |
+
processed_img = Image.fromarray(data)
|
| 311 |
+
|
| 312 |
+
# Convert to base64 string
|
| 313 |
+
buffered = io.BytesIO()
|
| 314 |
+
processed_img.save(buffered, format="PNG")
|
| 315 |
+
img_str = base64.b64encode(buffered.getvalue()).decode()
|
| 316 |
+
|
| 317 |
+
return f"data:image/png;base64,{img_str}"
|
| 318 |
+
|
| 319 |
+
except Exception as e:
|
| 320 |
+
logger.error(f"Error processing image {image_path}: {str(e)}")
|
| 321 |
+
return ""
|
| 322 |
+
|
| 323 |
+
def identify_categorical_columns(self, columns: List[Dict], input_data: Dict) -> List[Dict]:
|
| 324 |
+
"""
|
| 325 |
+
识别所有categorical列,为每个列确定是否应使用图标以及图标类别。
|
| 326 |
+
|
| 327 |
+
Args:
|
| 328 |
+
columns: 列信息列表
|
| 329 |
+
input_data: 输入数据
|
| 330 |
+
|
| 331 |
+
Returns:
|
| 332 |
+
列表,包含所有应该使用图标的categorical列信息
|
| 333 |
+
"""
|
| 334 |
+
categorical_columns = []
|
| 335 |
+
|
| 336 |
+
for column in columns:
|
| 337 |
+
if column["data_type"] == "categorical":
|
| 338 |
+
# 检查是否应该使用图标
|
| 339 |
+
result = self._should_use_icons(column["name"], input_data)
|
| 340 |
+
if result:
|
| 341 |
+
categorical_columns.append(result)
|
| 342 |
+
|
| 343 |
+
return categorical_columns
|
| 344 |
+
|
| 345 |
+
def _should_use_icons(self, column_name: str, input_data: Dict) -> Optional[Dict]:
|
| 346 |
+
"""
|
| 347 |
+
Determine whether icons should be used for a specific column
|
| 348 |
+
|
| 349 |
+
Args:
|
| 350 |
+
column_name: Column name
|
| 351 |
+
input_data: Input data
|
| 352 |
+
|
| 353 |
+
Returns:
|
| 354 |
+
Dictionary containing column name and category, or None if icons should not be used
|
| 355 |
+
"""
|
| 356 |
+
if not self.base_url or not self.api_key:
|
| 357 |
+
return None
|
| 358 |
+
|
| 359 |
+
# Get unique values for the column
|
| 360 |
+
data_rows = input_data.get("data", {}).get("data", [])
|
| 361 |
+
unique_values = list(set([row[column_name] for row in data_rows]))
|
| 362 |
+
titles = input_data.get("metadata", {}).get("titles", {})
|
| 363 |
+
|
| 364 |
+
prompt = f"""Please analyze whether icons should be used to distinguish between different groups in this chart.
|
| 365 |
+
|
| 366 |
+
Chart Context:
|
| 367 |
+
Title: {titles.get('main_title', '')}
|
| 368 |
+
Subtitle: {titles.get('sub_title', '')}
|
| 369 |
+
Column Name: {column_name}
|
| 370 |
+
Unique Values: {', '.join(map(str, unique_values[:10]))}{"..." if len(unique_values) > 10 else ""}
|
| 371 |
+
|
| 372 |
+
Categorize these values into one of the following types:
|
| 373 |
+
1. country (use country flags, including historical countries and regions)
|
| 374 |
+
2. industry (e.g., tech, finance, healthcare, manufacturing)
|
| 375 |
+
3. weather (e.g., sunny, rainy, cloudy, stormy)
|
| 376 |
+
4. emotion (e.g., happy, sad, neutral, excited, like, dislike, support, oppose, agree, disagree)
|
| 377 |
+
5. transport (e.g., car, plane, train, ship)
|
| 378 |
+
6. nature (e.g., animals, plants, landscapes)
|
| 379 |
+
7. sports (e.g., football, basketball, tennis)
|
| 380 |
+
8. politics (e.g., president, prime minister, king, queen, party, government)
|
| 381 |
+
9. other (e.g., food, drink, art, music, science, technology, history, geography, etc.) suitable for icons
|
| 382 |
+
10. abstract (not suitable for icons)
|
| 383 |
+
|
| 384 |
+
Consider whether icons would enhance understanding of the data. If the values don't clearly fit into any specific category above, or if using icons would not add meaningful information, select 'abstract' and set 'use_icons' to false.
|
| 385 |
+
|
| 386 |
+
Please respond in JSON format:
|
| 387 |
+
{{
|
| 388 |
+
"use_icons": true/false,
|
| 389 |
+
"category": "country/industry/weather/emotion/transport/nature/sports/abstract"
|
| 390 |
+
}}"""
|
| 391 |
+
|
| 392 |
+
try:
|
| 393 |
+
response = requests.post(
|
| 394 |
+
self.request_url,
|
| 395 |
+
headers={
|
| 396 |
+
"Authorization": f"Bearer {self.api_key}",
|
| 397 |
+
"Content-Type": "application/json"
|
| 398 |
+
},
|
| 399 |
+
json={
|
| 400 |
+
"model": "gpt-4o-mini",
|
| 401 |
+
"messages": [{
|
| 402 |
+
"role": "user",
|
| 403 |
+
"content": prompt
|
| 404 |
+
}]
|
| 405 |
+
}
|
| 406 |
+
)
|
| 407 |
+
if response.status_code == 200:
|
| 408 |
+
content = response.json()['choices'][0]['message']['content']
|
| 409 |
+
content = content.replace('```json\n', '').replace('\n```', '').strip()
|
| 410 |
+
try:
|
| 411 |
+
decision = json.loads(content)
|
| 412 |
+
logger.info(f"Column {column_name} icon usage decision: {decision}")
|
| 413 |
+
|
| 414 |
+
if not decision['use_icons']:
|
| 415 |
+
return None
|
| 416 |
+
|
| 417 |
+
return {
|
| 418 |
+
"column": column_name,
|
| 419 |
+
"category": decision['category']
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
except json.JSONDecodeError:
|
| 423 |
+
logger.error("Failed to parse LLM response as JSON")
|
| 424 |
+
return None
|
| 425 |
+
else:
|
| 426 |
+
logger.error(f"LLM API request failed with status code: {response.status_code}")
|
| 427 |
+
return None
|
| 428 |
+
|
| 429 |
+
except Exception as e:
|
| 430 |
+
logger.error(f"Error calling LLM API: {str(e)}")
|
| 431 |
+
return None
|
| 432 |
+
|
| 433 |
+
def recommend_images(self, input_data: Dict) -> Dict:
|
| 434 |
+
"""
|
| 435 |
+
Recommend images based on the input data.
|
| 436 |
+
"""
|
| 437 |
+
if self.index_builder is None:
|
| 438 |
+
raise ValueError("Index builder not initialized")
|
| 439 |
+
|
| 440 |
+
# Extract necessary information
|
| 441 |
+
data_dict = input_data.get("data", {})
|
| 442 |
+
columns = data_dict.get("columns", [])
|
| 443 |
+
|
| 444 |
+
# Step 1: 识别所有应该使用图标的categorical列
|
| 445 |
+
categorical_columns = self.identify_categorical_columns(columns, input_data)
|
| 446 |
+
# print(categorical_columns)
|
| 447 |
+
|
| 448 |
+
# Step 2: Get topic-level clipart recommendations
|
| 449 |
+
query_text = self.create_query_text(input_data)
|
| 450 |
+
topic_clipart = self.index_builder.search(query_text, top_k=5, image_type='clipart')
|
| 451 |
+
|
| 452 |
+
# Prepare the result
|
| 453 |
+
result = {
|
| 454 |
+
"topic_clipart": [
|
| 455 |
+
{
|
| 456 |
+
"image_path": img["image_path"],
|
| 457 |
+
"image_data": img["image_data"],
|
| 458 |
+
"similarity_score": 1.0 / (1.0 + img["distance"])
|
| 459 |
+
}
|
| 460 |
+
for img in topic_clipart
|
| 461 |
+
],
|
| 462 |
+
"group_icons": {}
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
# Step 3: 为每个categorical列处理图标
|
| 466 |
+
for column_info in categorical_columns:
|
| 467 |
+
group_col = column_info["column"]
|
| 468 |
+
category = column_info["category"]
|
| 469 |
+
data_rows = input_data.get("data", {}).get("data", [])
|
| 470 |
+
unique_values = list(set([row[group_col] for row in data_rows]))
|
| 471 |
+
|
| 472 |
+
# 创建一个包含列名的键,用于存储该列的图标
|
| 473 |
+
column_key = f"{group_col}"
|
| 474 |
+
result["group_icons"][column_key] = {}
|
| 475 |
+
|
| 476 |
+
# Try special categories first
|
| 477 |
+
if category in self.special_categories:
|
| 478 |
+
icons = self.process_special_icons(category, unique_values)
|
| 479 |
+
if icons:
|
| 480 |
+
result["group_icons"][column_key] = icons
|
| 481 |
+
else:
|
| 482 |
+
# print(f"Processing normal icons for column {group_col}")
|
| 483 |
+
# 为该列创建专门的输入数据
|
| 484 |
+
column_input = {
|
| 485 |
+
"data": data_dict,
|
| 486 |
+
"titles": input_data.get("metadata", {}).get("titles", {})
|
| 487 |
+
}
|
| 488 |
+
icons = self.process_normal_icons(column_input, unique_values, group_col)
|
| 489 |
+
result["group_icons"][column_key] = icons
|
| 490 |
+
|
| 491 |
+
return self.process_recommendation_result(result)
|
| 492 |
+
|
| 493 |
+
def process_recommendation_result(self, result: Dict) -> Dict:
|
| 494 |
+
"""
|
| 495 |
+
Process the recommendation result:
|
| 496 |
+
1. Remove unnecessary fields
|
| 497 |
+
2. Convert paths to absolute paths
|
| 498 |
+
3. Post-process images
|
| 499 |
+
4. Random select from lists
|
| 500 |
+
|
| 501 |
+
Args:
|
| 502 |
+
result: Original recommendation result
|
| 503 |
+
|
| 504 |
+
Returns:
|
| 505 |
+
Processed recommendation result
|
| 506 |
+
"""
|
| 507 |
+
import random
|
| 508 |
+
|
| 509 |
+
processed = {"other": {}, "field": {}}
|
| 510 |
+
|
| 511 |
+
# Process topic clipart
|
| 512 |
+
if result["topic_clipart"]:
|
| 513 |
+
# Randomly select one clipart
|
| 514 |
+
clipart = random.choice(result["topic_clipart"])
|
| 515 |
+
processed["other"]["primary"] = self.post_process_image(clipart["image_path"], max_size=768)
|
| 516 |
+
|
| 517 |
+
# Process group icons - 新结构支持多列的图标
|
| 518 |
+
for column_name, column_icons in result["group_icons"].items():
|
| 519 |
+
key_icon_pairs = {}
|
| 520 |
+
for group, icons in column_icons.items():
|
| 521 |
+
field_key = group
|
| 522 |
+
if isinstance(icons, list):
|
| 523 |
+
# For normal icons (list of candidates)
|
| 524 |
+
icon = random.choice(icons)
|
| 525 |
+
processed["field"][field_key] = self.post_process_image(icon["image_path"], max_size=256)
|
| 526 |
+
key_icon_pairs[group] = icon
|
| 527 |
+
else:
|
| 528 |
+
# For special icons (single icon)
|
| 529 |
+
processed["field"][field_key] = self.post_process_image(icons["image_path"], max_size=256)
|
| 530 |
+
key_icon_pairs[group] = icons
|
| 531 |
+
# processed["field"] = self.process_stretch_icons(self.input_data, processed["field"], column_name, key_icon_pairs)
|
| 532 |
+
|
| 533 |
+
return processed
|
| 534 |
+
|
| 535 |
+
def process_stretch_icons(self, input_data: Dict, icons: Dict, column_name: str, key_icon_pair: Dict) -> Dict:
|
| 536 |
+
"""
|
| 537 |
+
Process stretch icons based on the input data.
|
| 538 |
+
"""
|
| 539 |
+
data_rows = input_data.get("data", {}).get("data", [])
|
| 540 |
+
numerical_column = {}
|
| 541 |
+
for column in input_data.get("data", {}).get("columns", []):
|
| 542 |
+
if column["role"] == "y":
|
| 543 |
+
numerical_column = column
|
| 544 |
+
break
|
| 545 |
+
# print(numerical_column)
|
| 546 |
+
if not numerical_column:
|
| 547 |
+
return icons
|
| 548 |
+
data_values = {}
|
| 549 |
+
for key, icon in key_icon_pair.items():
|
| 550 |
+
for row in data_rows:
|
| 551 |
+
if row[column_name] == key:
|
| 552 |
+
data_values[key] = {
|
| 553 |
+
"value": row[numerical_column["name"]],
|
| 554 |
+
"icon": icon
|
| 555 |
+
}
|
| 556 |
+
# print(data_values)
|
| 557 |
+
# 检查data_values是否是数值
|
| 558 |
+
if not all(isinstance(value["value"], (int, float)) for value in data_values.values()):
|
| 559 |
+
return icons
|
| 560 |
+
|
| 561 |
+
width = 30
|
| 562 |
+
min_height = 5
|
| 563 |
+
max_height = 300
|
| 564 |
+
min_value = 1e5
|
| 565 |
+
max_value = -1e5
|
| 566 |
+
for key, value in data_values.items():
|
| 567 |
+
if value["value"] < min_value:
|
| 568 |
+
min_value = value["value"]
|
| 569 |
+
if value["value"] > max_value:
|
| 570 |
+
max_value = value["value"]
|
| 571 |
+
height_range = max_value - min_value
|
| 572 |
+
for key, value in data_values.items():
|
| 573 |
+
value["height"] = min(max(min_height, width * value["value"] / height_range), max_height)
|
| 574 |
+
from PIL import Image
|
| 575 |
+
import numpy as np
|
| 576 |
+
import base64
|
| 577 |
+
import io
|
| 578 |
+
# Get absolute path
|
| 579 |
+
abs_path = os.path.join(self.normal_icons, value["icon"]["image_path"])
|
| 580 |
+
if not os.path.exists(abs_path):
|
| 581 |
+
logger.error(f"Image not found: {abs_path}")
|
| 582 |
+
return ""
|
| 583 |
+
|
| 584 |
+
# Convert white background to transparent
|
| 585 |
+
img = Image.open(abs_path)
|
| 586 |
+
if img.mode != 'RGBA':
|
| 587 |
+
img = img.convert('RGBA')
|
| 588 |
+
|
| 589 |
+
data = np.array(img)
|
| 590 |
+
# Convert white-ish pixels to transparent
|
| 591 |
+
# RGB all > 240 is considered white-ish
|
| 592 |
+
white_mask = (data[..., :3] > 240).all(axis=2)
|
| 593 |
+
data[white_mask, 3] = 0
|
| 594 |
+
|
| 595 |
+
processed_img = Image.fromarray(data)
|
| 596 |
+
|
| 597 |
+
# Convert to base64 string
|
| 598 |
+
buffered = io.BytesIO()
|
| 599 |
+
processed_img.save(buffered, format="PNG")
|
| 600 |
+
img_str = base64.b64encode(buffered.getvalue()).decode()
|
| 601 |
+
request_data = {
|
| 602 |
+
"image": f"data:image/png;base64,{img_str}",
|
| 603 |
+
"scale": value["height"] / 30,
|
| 604 |
+
'hrz': 'false'
|
| 605 |
+
}
|
| 606 |
+
response = requests.post(
|
| 607 |
+
"http://166.111.86.168:5000/scale",
|
| 608 |
+
json=request_data
|
| 609 |
+
)
|
| 610 |
+
response.raise_for_status()
|
| 611 |
+
result = response.content
|
| 612 |
+
# 解码base64数据并保存图片
|
| 613 |
+
img_data = base64.b64decode(result.split(b"data:image/png;base64,")[1])
|
| 614 |
+
with open(f"./tmp/stretch_icon_{key}.png", "wb") as f:
|
| 615 |
+
f.write(img_data)
|
| 616 |
+
# value["icon"]["image_path"] = result["image_path"]
|
| 617 |
+
return data_values
|
| 618 |
+
|
| 619 |
+
def scale_icon(icon: Dict, height: float) -> Dict:
|
| 620 |
+
"""
|
| 621 |
+
Scale the icon to the given height.
|
| 622 |
+
"""
|
| 623 |
+
from PIL import Image
|
| 624 |
+
import numpy as np
|
| 625 |
+
import base64
|
| 626 |
+
import io
|
| 627 |
+
|
| 628 |
+
# Get absolute path
|
| 629 |
+
abs_path = os.path.join(self.normal_icons, icon["image_path"])
|
| 630 |
+
if not os.path.exists(abs_path):
|
| 631 |
+
logger.error(f"Image not found: {abs_path}")
|
| 632 |
+
return ""
|
| 633 |
+
|
| 634 |
+
def process(input: str, output: str, embed_model_path: str = None, resource_path: str = None, data_path: str = None, index_path: str = None, base_url: str = None, api_key: str = None) -> bool:
|
| 635 |
+
"""
|
| 636 |
+
Pipeline入口函数,处理单个文件的图像推荐
|
| 637 |
+
|
| 638 |
+
Args:
|
| 639 |
+
input_path: 输入JSON文件路径
|
| 640 |
+
output_path: 输出JSON文件路径
|
| 641 |
+
embed_model_path: 嵌入模型路径
|
| 642 |
+
data_path: 图像数据路径
|
| 643 |
+
index_path: 索引文件路径
|
| 644 |
+
"""
|
| 645 |
+
# print("process")
|
| 646 |
+
try:
|
| 647 |
+
# 读取输入文件
|
| 648 |
+
with open(input, "r", encoding="utf-8") as f:
|
| 649 |
+
data = json.load(f)
|
| 650 |
+
# print("input")
|
| 651 |
+
# 预处理数据
|
| 652 |
+
processed_data = preprocess_data(data)
|
| 653 |
+
# print("processed_data")
|
| 654 |
+
# 生成图像推荐
|
| 655 |
+
recommender = ImageRecommender(
|
| 656 |
+
embed_model_path=embed_model_path,
|
| 657 |
+
data_path=data_path,
|
| 658 |
+
index_path=index_path,
|
| 659 |
+
resource_path=resource_path,
|
| 660 |
+
base_url=base_url,
|
| 661 |
+
api_key=api_key
|
| 662 |
+
)
|
| 663 |
+
recommender.input_data = processed_data
|
| 664 |
+
image_result = recommender.recommend_images(processed_data)
|
| 665 |
+
# print("image_result")
|
| 666 |
+
# 添加图像推荐到数据中
|
| 667 |
+
processed_data["images"] = image_result
|
| 668 |
+
|
| 669 |
+
# 保存结果
|
| 670 |
+
with open(output, "w", encoding="utf-8") as f:
|
| 671 |
+
json.dump(processed_data, f, indent=2, ensure_ascii=False)
|
| 672 |
+
|
| 673 |
+
return True
|
| 674 |
+
|
| 675 |
+
except Exception as e:
|
| 676 |
+
logger.error(f"图像推荐失败: {str(e)}")
|
| 677 |
+
return False
|
| 678 |
+
|
| 679 |
+
def preprocess_data(data: Dict) -> Dict:
|
| 680 |
+
"""
|
| 681 |
+
预处理数据,确保格式正确
|
| 682 |
+
"""
|
| 683 |
+
try:
|
| 684 |
+
# 深拷贝避免修改原始数据
|
| 685 |
+
processed = data.copy()
|
| 686 |
+
|
| 687 |
+
# 确保metadata字段存在
|
| 688 |
+
if "metadata" not in processed:
|
| 689 |
+
processed["metadata"] = {}
|
| 690 |
+
|
| 691 |
+
# 确保titles字段存在
|
| 692 |
+
if "titles" not in processed["metadata"]:
|
| 693 |
+
processed["metadata"]["titles"] = {}
|
| 694 |
+
|
| 695 |
+
# 确保data_facts字段存在
|
| 696 |
+
if "data_facts" not in processed["metadata"]:
|
| 697 |
+
processed["metadata"]["data_facts"] = []
|
| 698 |
+
return processed
|
| 699 |
+
|
| 700 |
+
except Exception as e:
|
| 701 |
+
logger.error(f"数据预处理失败: {str(e)}")
|
| 702 |
+
raise
|
| 703 |
+
|
| 704 |
+
def main():
|
| 705 |
+
import argparse
|
| 706 |
+
parser = argparse.ArgumentParser(description="Image Recommender")
|
| 707 |
+
parser.add_argument("--input", type=str, required=True, help="Input JSON file path")
|
| 708 |
+
parser.add_argument("--output", type=str, required=True, help="Output JSON file path")
|
| 709 |
+
parser.add_argument("--embed_model_path", type=str, help="Path to the embedding model")
|
| 710 |
+
parser.add_argument("--data_path", type=str, help="Path to the image data file")
|
| 711 |
+
parser.add_argument("--index_path", type=str, help="Path to the index file")
|
| 712 |
+
parser.add_argument("--base_url", type=str, help="Base URL for LLM API")
|
| 713 |
+
parser.add_argument("--api_key", type=str, help="API key for LLM")
|
| 714 |
+
args = parser.parse_args()
|
| 715 |
+
|
| 716 |
+
success = process(
|
| 717 |
+
input=args.input,
|
| 718 |
+
output=args.output,
|
| 719 |
+
embed_model_path=args.embed_model_path,
|
| 720 |
+
data_path=args.data_path,
|
| 721 |
+
index_path=args.index_path,
|
| 722 |
+
base_url=args.base_url,
|
| 723 |
+
api_key=args.api_key
|
| 724 |
+
)
|
| 725 |
+
|
| 726 |
+
if success:
|
| 727 |
+
print("Processing json successed.")
|
| 728 |
+
else:
|
| 729 |
+
print("Processing json failed.")
|
| 730 |
+
|
| 731 |
+
if __name__ == "__main__":
|
| 732 |
+
main()
|
modules/image_recommender/modify.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# check all json file in results_path
|
| 2 |
+
import json, os
|
| 3 |
+
results_path = './results/'
|
| 4 |
+
target_path = './results_m/'
|
| 5 |
+
# remove target_path
|
| 6 |
+
if os.path.exists(target_path):
|
| 7 |
+
os.system(f'rm -r {target_path}')
|
| 8 |
+
if not os.path.exists(target_path):
|
| 9 |
+
os.makedirs(target_path)
|
| 10 |
+
|
| 11 |
+
available_list = ['image_content', 'topic', 'data_facts', 'color_style', 'icon_or_clipart']
|
| 12 |
+
data_fact_list = ['increasing', 'decreasing', 'highlight', 'deny', 'maximum', 'minimum', 'comparison', 'none']
|
| 13 |
+
color_style_list = ['monochrome', 'colorful', 'grayscale', 'dual-color']
|
| 14 |
+
scale_and_complexity_level_list = ['icon', 'clipart', 'background']
|
| 15 |
+
for i, file in enumerate(os.listdir(results_path)):
|
| 16 |
+
if not file.endswith('.json'):
|
| 17 |
+
continue
|
| 18 |
+
|
| 19 |
+
# load json
|
| 20 |
+
with open(f'{results_path}{file}', 'r') as f:
|
| 21 |
+
data = json.load(f)
|
| 22 |
+
|
| 23 |
+
if 'output_case' in data:
|
| 24 |
+
data = data['output_case']
|
| 25 |
+
|
| 26 |
+
if 'Topic' in data:
|
| 27 |
+
data['topic'] = data['Topic']
|
| 28 |
+
del data['Topic']
|
| 29 |
+
|
| 30 |
+
if 'key_words' in data:
|
| 31 |
+
data['topic'] = data['key_words']
|
| 32 |
+
del data['key_words']
|
| 33 |
+
|
| 34 |
+
# key error
|
| 35 |
+
error = False
|
| 36 |
+
for key in available_list:
|
| 37 |
+
if key not in data:
|
| 38 |
+
error = True
|
| 39 |
+
break
|
| 40 |
+
if error:
|
| 41 |
+
print(f'{file} is not complete')
|
| 42 |
+
continue
|
| 43 |
+
|
| 44 |
+
# data_facts error
|
| 45 |
+
if data['data_facts'] == 'increase':
|
| 46 |
+
data['data_facts'] = 'increasing'
|
| 47 |
+
if data['data_facts'] not in data_fact_list:
|
| 48 |
+
print(f'{file} data_facts is error')
|
| 49 |
+
continue
|
| 50 |
+
|
| 51 |
+
# color_style error
|
| 52 |
+
if data['color_style'] == 'gray-scale' or data['color_style'] == 'black and white':
|
| 53 |
+
data['color_style'] = 'grayscale'
|
| 54 |
+
if data['color_style'] == 'duo-color' or data['color_style'] == 'duel-color':
|
| 55 |
+
data['color_style'] = 'dual-color'
|
| 56 |
+
if data['color_style'] not in color_style_list:
|
| 57 |
+
print(f'{file} color_style is error')
|
| 58 |
+
continue
|
| 59 |
+
|
| 60 |
+
# scale_and_complexity_level error
|
| 61 |
+
if data['icon_or_clipart'] not in scale_and_complexity_level_list:
|
| 62 |
+
print(f'{file} icon_or_clipart is error')
|
| 63 |
+
continue
|
| 64 |
+
data['size'] = data['icon_or_clipart']
|
| 65 |
+
del data['icon_or_clipart']
|
| 66 |
+
|
| 67 |
+
# save json
|
| 68 |
+
target_file = f'{target_path}{file}'
|
| 69 |
+
with open(target_file, 'w') as f:
|
| 70 |
+
json.dump(data, f)
|
modules/image_recommender/prompt.json
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"task": "Analyze the semantic and style modes in the given image and provide explanations.",
|
| 3 |
+
"output_format": {
|
| 4 |
+
"image_content": {
|
| 5 |
+
"type": "string",
|
| 6 |
+
"description": "Primary content in image (less than 10 words)",
|
| 7 |
+
"examples": [
|
| 8 |
+
{
|
| 9 |
+
"example_value": "A single red apple",
|
| 10 |
+
"explanation": "A concise description with 5 words focusing on 'what' the image is."
|
| 11 |
+
},
|
| 12 |
+
{
|
| 13 |
+
"example_value": "Two people shaking hands",
|
| 14 |
+
"explanation": "An example describing the main objects and action within 10 words."
|
| 15 |
+
}
|
| 16 |
+
]
|
| 17 |
+
},
|
| 18 |
+
"topic": {
|
| 19 |
+
"type": "string",
|
| 20 |
+
"description": "Relevant topic (3-5 words)",
|
| 21 |
+
"examples": [
|
| 22 |
+
{
|
| 23 |
+
"example_value": "fruit, environment, healthy eating",
|
| 24 |
+
"explanation": "Three to five words separated by commas, capturing the core theme."
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"example_value": "teamwork, collaboration, business partnership",
|
| 28 |
+
"explanation": "Three to five key words describing the main idea of the image."
|
| 29 |
+
}
|
| 30 |
+
]
|
| 31 |
+
},
|
| 32 |
+
"data_facts": {
|
| 33 |
+
"type": "enum",
|
| 34 |
+
"description": "Possible usage to reflect any data fact (choose one from the list or 'none')",
|
| 35 |
+
"choices": [
|
| 36 |
+
"increasing",
|
| 37 |
+
"decreasing",
|
| 38 |
+
"highlight",
|
| 39 |
+
"deny",
|
| 40 |
+
"maximum",
|
| 41 |
+
"minimum",
|
| 42 |
+
"none"
|
| 43 |
+
],
|
| 44 |
+
"examples": [
|
| 45 |
+
{
|
| 46 |
+
"example_value": "increasing",
|
| 47 |
+
"explanation": "Use if the image shows an upward arrow or indicates growth."
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
"example_value": "decreasing",
|
| 51 |
+
"explanation": "Use if the image shows a downward arrow or indicates decline."
|
| 52 |
+
},
|
| 53 |
+
{
|
| 54 |
+
"example_value": "highlight",
|
| 55 |
+
"explanation": "Use if the image emphasizes something align with it (e.g., a light bulb)."
|
| 56 |
+
},
|
| 57 |
+
{
|
| 58 |
+
"example_value": "deny",
|
| 59 |
+
"explanation": "Use if the image represents a 'no' or 'wrong' symbol."
|
| 60 |
+
},
|
| 61 |
+
{
|
| 62 |
+
"example_value": "maximum",
|
| 63 |
+
"explanation": "Use if the image indicates a top/peak point."
|
| 64 |
+
},
|
| 65 |
+
{
|
| 66 |
+
"example_value": "minimum",
|
| 67 |
+
"explanation": "Use if the image indicates a bottom/lowest point."
|
| 68 |
+
},
|
| 69 |
+
{
|
| 70 |
+
"example_value": "none",
|
| 71 |
+
"explanation": "Use if none of the above data facts apply."
|
| 72 |
+
}
|
| 73 |
+
]
|
| 74 |
+
},
|
| 75 |
+
"color_style": {
|
| 76 |
+
"type": "enum",
|
| 77 |
+
"description": "Dominant color style, if image is black and white, return grayscale instead of monochrome",
|
| 78 |
+
"choices": [
|
| 79 |
+
"grayscale",
|
| 80 |
+
"monochrome",
|
| 81 |
+
"dual-color",
|
| 82 |
+
"colorful"
|
| 83 |
+
],
|
| 84 |
+
"examples": [
|
| 85 |
+
{
|
| 86 |
+
"example_value": "grayscale",
|
| 87 |
+
"explanation": "Use if the image is purely in black, white, and shades of gray."
|
| 88 |
+
},
|
| 89 |
+
{
|
| 90 |
+
"example_value": "monochrome",
|
| 91 |
+
"explanation": "Use if the image uses colors with same hue but not grayscale."
|
| 92 |
+
},
|
| 93 |
+
{
|
| 94 |
+
"example_value": "dual-color",
|
| 95 |
+
"explanation": "Use if the image uses two sets of colors on the color wheel."
|
| 96 |
+
},
|
| 97 |
+
{
|
| 98 |
+
"example_value": "colorful",
|
| 99 |
+
"explanation": "Use if the image has multiple bright or varied colors."
|
| 100 |
+
}
|
| 101 |
+
]
|
| 102 |
+
},
|
| 103 |
+
"icon_or_clipart": {
|
| 104 |
+
"type": "enum",
|
| 105 |
+
"description": "judge the image whether it is an small icon, medium clipart or large background",
|
| 106 |
+
"choices": [
|
| 107 |
+
"icon",
|
| 108 |
+
"clipart",
|
| 109 |
+
"background"
|
| 110 |
+
],
|
| 111 |
+
"examples": [
|
| 112 |
+
{
|
| 113 |
+
"example_value": "icon",
|
| 114 |
+
"explanation": "Icon showing basic shapes and always small."
|
| 115 |
+
},
|
| 116 |
+
{
|
| 117 |
+
"example_value": "clipart",
|
| 118 |
+
"explanation": "Clipart showing detailed objects."
|
| 119 |
+
},
|
| 120 |
+
{
|
| 121 |
+
"example_value": "background",
|
| 122 |
+
"explanation": "Large scale image or clipart that suitable to be used as a background."
|
| 123 |
+
}
|
| 124 |
+
]
|
| 125 |
+
}
|
| 126 |
+
},
|
| 127 |
+
"output_example": {
|
| 128 |
+
"image_content": "A single red apple",
|
| 129 |
+
"topic": "fruit, environment",
|
| 130 |
+
"data_facts": "none",
|
| 131 |
+
"color_style": "colorful",
|
| 132 |
+
"icon_or_clipart": "icon",
|
| 133 |
+
"explanation": "The image shows a red apple, focusing on a fruit in nature. It is colorful and icon-sized. No specific data fact is represented."
|
| 134 |
+
}
|
| 135 |
+
}
|
modules/image_recommender/test.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 1. 扫描当前目录下所有image文件 包含png jpg jpeg webp,进行编号 存储下list
|
| 2 |
+
import os, json
|
| 3 |
+
image_pathes = []
|
| 4 |
+
root = './images/'
|
| 5 |
+
image_path_file = 'image_pathes.json'
|
| 6 |
+
|
| 7 |
+
if os.path.exists(image_path_file):
|
| 8 |
+
with open(image_path_file, 'r') as f:
|
| 9 |
+
image_pathes = json.load(f)
|
| 10 |
+
print(len(image_pathes))
|
| 11 |
+
else:
|
| 12 |
+
print('error')
|
| 13 |
+
|
| 14 |
+
from openai import OpenAI
|
| 15 |
+
from PIL import Image
|
| 16 |
+
import base64
|
| 17 |
+
from io import BytesIO
|
| 18 |
+
|
| 19 |
+
client = OpenAI(
|
| 20 |
+
api_key=os.getenv("OPENAI_API_KEY") or os.getenv("AIHUBMIX_API_KEY", ""),
|
| 21 |
+
base_url=os.getenv("OPENAI_BASE_URL", "https://aihubmix.com/v1")
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
def resize_image(img, max_size=512):
|
| 25 |
+
width, height = img.size
|
| 26 |
+
ratio = min(max_size / width, max_size / height)
|
| 27 |
+
if ratio >= 1:
|
| 28 |
+
return img
|
| 29 |
+
new_width = int(width * ratio)
|
| 30 |
+
new_height = int(height * ratio)
|
| 31 |
+
resized_img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
| 32 |
+
return resized_img
|
| 33 |
+
|
| 34 |
+
def repaint_image(img):
|
| 35 |
+
# rapaint transparent area with white color
|
| 36 |
+
img = img.convert('RGBA')
|
| 37 |
+
data = img.getdata()
|
| 38 |
+
new_data = []
|
| 39 |
+
for item in data:
|
| 40 |
+
if item[3] == 0:
|
| 41 |
+
new_data.append((255, 255, 255, 255))
|
| 42 |
+
else:
|
| 43 |
+
new_data.append(item)
|
| 44 |
+
img.putdata(new_data)
|
| 45 |
+
img = img.convert('RGB')
|
| 46 |
+
# img.save('temp.png')
|
| 47 |
+
return img
|
| 48 |
+
|
| 49 |
+
def image_to_base64(image_path, show=False, target_size=512):
|
| 50 |
+
with Image.open(image_path) as img:
|
| 51 |
+
# img = img.resize(size)
|
| 52 |
+
img = resize_image(img, 512)
|
| 53 |
+
img = repaint_image(img)
|
| 54 |
+
buffered = BytesIO()
|
| 55 |
+
img.save(buffered, format="PNG")
|
| 56 |
+
img_base64 = base64.b64encode(buffered.getvalue()).decode('utf-8')
|
| 57 |
+
return img_base64
|
| 58 |
+
|
| 59 |
+
wwxxhh = 0
|
| 60 |
+
def ask_image(prompt, image_data):
|
| 61 |
+
global wwxxhh
|
| 62 |
+
number_of_trials = 0
|
| 63 |
+
while number_of_trials < 5:
|
| 64 |
+
try:
|
| 65 |
+
response = client.chat.completions.create(
|
| 66 |
+
# model="gpt-4o-mini",
|
| 67 |
+
model="gemini-2.0-flash",
|
| 68 |
+
messages=[
|
| 69 |
+
{
|
| 70 |
+
"role": "user",
|
| 71 |
+
"content": [
|
| 72 |
+
{
|
| 73 |
+
"type": "text",
|
| 74 |
+
"text": prompt},
|
| 75 |
+
{
|
| 76 |
+
"type": "image_url",
|
| 77 |
+
"image_url": {
|
| 78 |
+
"url": f"data:image/jpeg;base64,{image_data}"
|
| 79 |
+
},
|
| 80 |
+
},
|
| 81 |
+
],
|
| 82 |
+
}
|
| 83 |
+
]
|
| 84 |
+
)
|
| 85 |
+
wwxxhh += response.usage.total_tokens
|
| 86 |
+
return response.choices[0].message.content
|
| 87 |
+
|
| 88 |
+
except Exception as e:
|
| 89 |
+
number_of_trials += 1
|
| 90 |
+
print(e)
|
| 91 |
+
|
| 92 |
+
return 'Error!'
|
| 93 |
+
|
| 94 |
+
# 2. 读取prompt.json文件,读取整个作为字符串,逐个读取image文件,调用ask_image函数,将返回的结果存储下来
|
| 95 |
+
import json
|
| 96 |
+
with open('prompt.json', 'r') as f:
|
| 97 |
+
prompt = f.read()
|
| 98 |
+
# print(prompt)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
ques_id = 15394
|
| 103 |
+
results_path = './results/'
|
| 104 |
+
image_path = image_pathes[ques_id]
|
| 105 |
+
print(image_path)
|
| 106 |
+
|
| 107 |
+
image_data = image_to_base64(image_path)
|
| 108 |
+
result = ask_image(prompt, image_data)
|
| 109 |
+
print(result)
|
| 110 |
+
|
| 111 |
+
# from IPython import embed
|
| 112 |
+
# embed()
|