Ray1ee01 commited on
Commit
520da1b
·
verified ·
1 Parent(s): a5ee291

Upload folder using huggingface_hub

Browse files
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()