mnhkahn commited on
Commit
5f9d252
·
verified ·
1 Parent(s): ee0a285

Upload main.py

Browse files
Files changed (1) hide show
  1. main.py +304 -0
main.py ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # main.py
2
+ import os
3
+ import json
4
+ import torch
5
+ import torch.nn as nn
6
+ from torchvision.models import resnet50
7
+ from torchvision.transforms import transforms
8
+ from PIL import Image, ImageOps, ImageEnhance
9
+ from typing import List, Dict
10
+ from fastapi import FastAPI, File, UploadFile, HTTPException, Request
11
+ from fastapi.middleware.cors import CORSMiddleware
12
+ from fastapi.responses import JSONResponse
13
+ import io
14
+ import cv2
15
+ import numpy as np
16
+ import hashlib
17
+ import logging
18
+ from PIL.ExifTags import TAGS
19
+
20
+ # 设置日志
21
+ logging.basicConfig(level=logging.INFO)
22
+ logger = logging.getLogger(__name__)
23
+
24
+ # --- 配置部分 ---
25
+ MODEL_PATH = "best_model.pth"
26
+ MAP_PATH = "char_map.json"
27
+
28
+
29
+ # --- 全局初始化模型(启动时加载一次) ---
30
+ class Recognizer:
31
+ def __init__(self, model_path: str = MODEL_PATH, map_path: str = MAP_PATH):
32
+ if not os.path.exists(model_path) or not os.path.exists(map_path):
33
+ raise FileNotFoundError(
34
+ f"模型文件 '{model_path}' 或字符映射表 '{map_path}' 不存在,"
35
+ "请先运行 train_model.py 进行模型训练。"
36
+ )
37
+
38
+ self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
39
+
40
+ # 加载字符映射
41
+ with open(map_path, "r", encoding="utf-8") as f:
42
+ self.char_to_idx = json.load(f)
43
+ self.idx_to_char = {v: k for k, v in self.char_to_idx.items()}
44
+ num_classes = len(self.char_to_idx)
45
+
46
+ # 构建并加载模型
47
+ self.model = self._get_model(num_classes)
48
+ try:
49
+ ckpt = torch.load(model_path, map_location=self.device, weights_only=True)
50
+ except Exception:
51
+ ckpt = torch.load(model_path, map_location=self.device, weights_only=False)
52
+ if isinstance(ckpt, nn.Module):
53
+ state_dict = ckpt.state_dict()
54
+ elif isinstance(ckpt, dict):
55
+ state_dict = ckpt.get("state_dict", ckpt.get("model_state", ckpt))
56
+ else:
57
+ raise ValueError("不支持的模型文件格式")
58
+ self.model.load_state_dict(state_dict, strict=False)
59
+ self.model.to(self.device)
60
+ self.model.eval()
61
+
62
+ # 定义图像预处理
63
+ self.transform = transforms.Compose(
64
+ [
65
+ transforms.Resize((224, 224)),
66
+ transforms.ToTensor(),
67
+ transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
68
+ ]
69
+ )
70
+
71
+ def _get_model(self, num_classes: int) -> nn.Module:
72
+ model = resnet50(weights=None)
73
+ num_ftrs = model.fc.in_features
74
+ model.fc = nn.Sequential(nn.Dropout(0.3), nn.Linear(num_ftrs, num_classes))
75
+ return model
76
+
77
+ def preprocess_image(self, image_bytes: bytes, user_agent: str = "") -> Image.Image:
78
+ """统一的图像预处理,特别处理移动设备上传的图片"""
79
+ try:
80
+ image = Image.open(io.BytesIO(image_bytes))
81
+
82
+ # 记录原始图像信息用于调试
83
+ logger.info(
84
+ f"原始图像 - 格式: {image.format}, 模式: {image.mode}, 尺寸: {image.size}"
85
+ )
86
+
87
+ # 处理EXIF方向信息(手机照片常有旋转问题)
88
+ try:
89
+ exif = image._getexif()
90
+ if exif:
91
+ for tag, value in exif.items():
92
+ decoded = TAGS.get(tag, tag)
93
+ if decoded == "Orientation":
94
+ if value == 3:
95
+ image = image.rotate(180, expand=True)
96
+ elif value == 6:
97
+ image = image.rotate(270, expand=True)
98
+ elif value == 8:
99
+ image = image.rotate(90, expand=True)
100
+ break
101
+ except Exception as e:
102
+ logger.warning(f"EXIF处理失败: {e}")
103
+
104
+ # 转换为RGB
105
+ if image.mode != "RGB":
106
+ image = image.convert("RGB")
107
+
108
+ # 检测是否为移动设备并应用增强处理
109
+ is_mobile = any(
110
+ mobile_indicator in user_agent.lower()
111
+ for mobile_indicator in ["mobile", "iphone", "android", "ipad"]
112
+ )
113
+
114
+ if is_mobile:
115
+ logger.info("检测到移动设备,应用增强预处理")
116
+ # 增强对比度
117
+ enhancer = ImageEnhance.Contrast(image)
118
+ image = enhancer.enhance(1.2)
119
+
120
+ # 轻微锐化
121
+ enhancer = ImageEnhance.Sharpness(image)
122
+ image = enhancer.enhance(1.1)
123
+
124
+ return image
125
+
126
+ except Exception as e:
127
+ raise ValueError(f"无法处理图片: {e}")
128
+
129
+ def assess_image_quality(self, image: Image.Image) -> Dict:
130
+ """评估图像质量"""
131
+ # 转换为numpy数组进行处理
132
+ img_array = np.array(image)
133
+
134
+ if len(img_array.shape) == 3:
135
+ gray = cv2.cvtColor(img_array, cv2.COLOR_RGB2GRAY)
136
+ else:
137
+ gray = img_array
138
+
139
+ # 计算清晰度(拉普拉斯方差)
140
+ clarity = cv2.Laplacian(gray, cv2.CV_64F).var()
141
+
142
+ # 计算亮度和对比度
143
+ brightness = np.mean(gray)
144
+ contrast = np.std(gray)
145
+
146
+ quality_info = {
147
+ "clarity": float(clarity),
148
+ "brightness": float(brightness),
149
+ "contrast": float(contrast),
150
+ "is_acceptable": clarity > 50 and 30 < brightness < 220,
151
+ }
152
+
153
+ logger.info(f"图像质量评估: {quality_info}")
154
+ return quality_info
155
+
156
+ def recognize(
157
+ self, image_bytes: bytes, top_k: int = 5, user_agent: str = ""
158
+ ) -> List[Dict[str, str]]:
159
+ """
160
+ 识别上传的图像。
161
+
162
+ Args:
163
+ image_bytes: 图片的二进制数据。
164
+ top_k: 返回前k个结果。
165
+ user_agent: 用户代理字符串,用于设备检测
166
+
167
+ Returns:
168
+ 一个字典列表,每个字典包含 `char` 和 `prob` 键。
169
+ """
170
+ # 记录图像哈希用于调试
171
+ image_hash = hashlib.md5(image_bytes).hexdigest()
172
+ logger.info(f"处理图像 - 哈希: {image_hash}, 设备: {user_agent}")
173
+
174
+ # 预处理图像
175
+ image = self.preprocess_image(image_bytes, user_agent)
176
+
177
+ # 评估图像质量
178
+ quality_info = self.assess_image_quality(image)
179
+ if not quality_info["is_acceptable"]:
180
+ logger.warning(f"图像质量可能影响识别: {quality_info}")
181
+
182
+ # 应用模型预处理
183
+ image_tensor = self.transform(image).unsqueeze(0).to(self.device)
184
+
185
+ with torch.no_grad():
186
+ outputs = self.model(image_tensor)
187
+ probabilities = torch.nn.functional.softmax(outputs, dim=1)
188
+ top_probs, top_indices = torch.topk(probabilities, top_k)
189
+
190
+ results = []
191
+ top_probs_np = top_probs.cpu().numpy().flatten()
192
+ top_indices_np = top_indices.cpu().numpy().flatten()
193
+
194
+ for i in range(top_k):
195
+ char_idx = top_indices_np[i]
196
+ char_name = self.idx_to_char.get(char_idx, "?")
197
+ probability = top_probs_np[i]
198
+ results.append({"char": char_name, "prob": f"{probability:.2%}"})
199
+
200
+ logger.info(f"识别结果: {results}")
201
+ return results
202
+
203
+
204
+ # --- FastAPI 应用初始化 ---
205
+ app = FastAPI(
206
+ title="汉字书法字体识别",
207
+ description="上传一张汉字图片,返回识别出的汉字及其置信度。",
208
+ version="1.0.0",
209
+ )
210
+
211
+ # 添加 CORS 中间件
212
+ app.add_middleware(
213
+ CORSMiddleware,
214
+ allow_origins=["*"], # 生产环境请替换为具体域名
215
+ allow_credentials=True,
216
+ allow_methods=["*"],
217
+ allow_headers=["*"],
218
+ )
219
+
220
+ # 初始化识别器(全局单例)
221
+ try:
222
+ recognizer = Recognizer()
223
+ logger.info("模型加载成功,服务已启动")
224
+ except Exception as e:
225
+ logger.error(f"启动失败: {e}")
226
+ recognizer = None
227
+
228
+
229
+ # --- API 路由 ---
230
+ @app.post("/upload", response_model=List[Dict[str, str]])
231
+ async def upload_image(request: Request, file: UploadFile = File(...)):
232
+ """
233
+ 上传图片进行汉字识别。
234
+
235
+ - **file**: 需要识别的图片文件 (jpg, png, etc.)
236
+ """
237
+ if not recognizer:
238
+ raise HTTPException(status_code=503, detail="服务暂不可用,模型文件未找到。")
239
+
240
+ # 检查文件类型
241
+ if not file.content_type.startswith("image/"):
242
+ raise HTTPException(status_code=400, detail="上传的文件必须是图片格式。")
243
+
244
+ try:
245
+ image_bytes = await file.read()
246
+ user_agent = request.headers.get("User-Agent", "")
247
+ results = recognizer.recognize(image_bytes, top_k=5, user_agent=user_agent)
248
+ return results
249
+ except ValueError as ve:
250
+ logger.error(f"图片处理错误: {ve}")
251
+ raise HTTPException(status_code=400, detail=str(ve))
252
+ except Exception as e:
253
+ logger.error(f"识别错误: {e}")
254
+ raise HTTPException(status_code=500, detail=f"服务器内部错误: {e}")
255
+
256
+
257
+ # 调试接口
258
+ @app.post("/debug_upload")
259
+ async def debug_upload(request: Request, file: UploadFile = File(...)):
260
+ """调试接口,返回上传图片的详细信息"""
261
+ try:
262
+ image_bytes = await file.read()
263
+ user_agent = request.headers.get("User-Agent", "")
264
+
265
+ # 使用recognizer的预处理方法来分析图片
266
+ image = recognizer.preprocess_image(image_bytes, user_agent)
267
+ quality_info = recognizer.assess_image_quality(image)
268
+
269
+ debug_info = {
270
+ "file_size": len(image_bytes),
271
+ "file_hash": hashlib.md5(image_bytes).hexdigest(),
272
+ "image_size": image.size,
273
+ "image_mode": image.mode,
274
+ "quality_assessment": quality_info,
275
+ "user_agent": user_agent,
276
+ }
277
+
278
+ return JSONResponse(content=debug_info)
279
+ except Exception as e:
280
+ logger.error(f"调试接口错误: {e}")
281
+ raise HTTPException(status_code=500, detail=str(e))
282
+
283
+
284
+ from fastapi.staticfiles import StaticFiles
285
+
286
+ app.mount("/", StaticFiles(directory="static", html=True), name="web")
287
+
288
+
289
+ @app.get("/")
290
+ async def root():
291
+ return {"message": "欢迎使用汉字书法识别模型,请使用 POST /upload 接口上传图片。"}
292
+
293
+
294
+ @app.get("/health")
295
+ async def health_check():
296
+ """健康检查接口"""
297
+ status = "healthy" if recognizer else "unhealthy"
298
+ return {"status": status, "model_loaded": recognizer is not None}
299
+
300
+
301
+ if __name__ == "__main__":
302
+ import uvicorn
303
+
304
+ uvicorn.run(app, host="0.0.0.0", port=8000, log_level="info")