lze888lze commited on
Commit
2ce8a16
·
verified ·
1 Parent(s): b842a1d

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +191 -43
main.py CHANGED
@@ -1,99 +1,247 @@
1
- import gc
2
- import os
3
- from typing import List
4
-
5
- import cv2
6
- import numpy as np
7
- from captcha_recognizer.slider import Slider
8
- from fastapi import FastAPI, File, UploadFile
9
- from fastapi.middleware.cors import CORSMiddleware
10
- from pydantic import BaseModel
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  app = FastAPI(
13
- docs_url=None,
14
- redoc_url=None,
15
  )
16
 
17
- # 允许跨域
 
18
  app.add_middleware(
19
  CORSMiddleware,
20
- allow_origins=["*"],
21
- allow_methods=["*"],
22
- allow_headers=["*"],
23
  )
24
 
25
- # 启动时只加载一次模型
 
 
 
 
 
26
  slider = Slider()
27
 
28
 
 
 
 
 
29
  @app.get("/")
30
  def hello_captcha():
 
31
  return {"Hello": "Captcha", "status": "running"}
32
 
33
 
34
  @app.get("/health")
35
  def health():
 
36
  return {"status": "ok"}
37
 
38
 
39
- class DetectionResult(BaseModel):
40
- box: List[int] # [x1, y1, x2, y2]
41
- confidence: float
42
- message: str = None
43
-
44
-
45
- @app.post("/captcha", response_model=DetectionResult)
46
- async def captcha(file: UploadFile = File(...)):
47
- contents = await file.read()
48
 
49
- nparr = np.frombuffer(contents, np.uint8)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
51
 
52
  if image is None:
53
- return DetectionResult(box=[], confidence=0, message="不支持的图片")
54
 
55
- box, confidence = slider.identify(source=image)
56
- box = [int(x) for x in box]
57
 
58
- del image, nparr, contents
 
59
  gc.collect()
60
 
61
- return DetectionResult(box=box, confidence=confidence)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
 
64
  @app.post("/captcha/base64", response_model=DetectionResult)
65
  async def captcha_base64(data: dict):
66
- """支持base64图片上传,用于懒人精灵调用"""
 
 
 
67
  import base64
68
 
69
  image_b64 = data.get("image", "")
70
  if not image_b64:
71
  return DetectionResult(box=[], confidence=0, message="缺少image参数")
72
 
73
- # 去掉base64头(如果有)
74
  if "," in image_b64:
75
  image_b64 = image_b64.split(",")[1]
76
 
77
  try:
78
  image_bytes = base64.b64decode(image_b64)
79
- nparr = np.frombuffer(image_bytes, np.uint8)
80
- image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
81
  except Exception as e:
82
  return DetectionResult(box=[], confidence=0, message=f"base64解码失败: {str(e)}")
83
 
84
- if image is None:
85
- return DetectionResult(box=[], confidence=0, message="不支持的图片")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
- box, confidence = slider.identify(source=image)
88
- box = [int(x) for x in box]
89
 
90
- del image, nparr, image_bytes
91
- gc.collect()
 
 
92
 
93
- return DetectionResult(box=box, confidence=confidence)
94
 
95
 
 
 
 
96
  if __name__ == "__main__":
97
  import uvicorn
 
 
98
  port = int(os.environ.get("PORT", 7860))
99
  uvicorn.run(app, host="0.0.0.0", port=port)
 
1
+ """
2
+ 滑块验证码识别 API 服务
3
+ ========================
4
+ 基于 FastAPI + ONNX 模型,提供 HTTP 接口识别滑块验证码的滑块和缺口位置。
5
+
6
+ 接口说明:
7
+ GET / - 健康检查
8
+ GET /health - 健康检查
9
+ POST /captcha - 旧版:上传图片文件,只返回缺口(向后兼容)
10
+ POST /captcha/base64 - 旧版:上传 base64 图片,只返回缺口
11
+ POST /captcha/v2 - 新版:上传图片文件,返回滑块+缺口+偏移量
12
+ POST /captcha/v2/base64 - 新版:上传 base64 图片,返回滑块+缺口+偏移量
13
+
14
+ 旧版返回格式(/captcha、/captcha/base64):
15
+ {
16
+ "box": [x1, y1, x2, y2], // 缺口坐标
17
+ "confidence": 0.95, // 置信度
18
+ "message": null // 错误信息
19
+ }
20
+
21
+ 新版返回格式(/captcha/v2、/captcha/v2/base64):
22
+ {
23
+ "slider": [x1, y1, x2, y2], // 滑块坐标(空列表=未检测到)
24
+ "slider_confidence": 0.92, // 滑块置信度
25
+ "gap": [x1, y1, x2, y2], // 缺口坐标(空列表=未检测到)
26
+ "gap_confidence": 0.88, // 缺口置信度
27
+ "offset": 162, // 滑动距离 = 缺口x1 - 滑块x1
28
+ "message": null // 错误信息
29
+ }
30
+ """
31
+
32
+ import gc # 垃圾回收,手动释放内存(HF免费层512MB)
33
+ import os # 读取环境变量(端口号)
34
+ from typing import List, Optional
35
+
36
+ import cv2 # OpenCV,图片解码
37
+ import numpy as np # 数组操作
38
+
39
+ from captcha_recognizer.slider import Slider # 核心:滑块识别模型
40
+ from fastapi import FastAPI, File, UploadFile # Web 框架
41
+ from fastapi.middleware.cors import CORSMiddleware # 跨域支持
42
+ from pydantic import BaseModel # 数据模型定义
43
+
44
+
45
+ # ============================================================
46
+ # FastAPI 应用初始化
47
+ # ============================================================
48
 
49
  app = FastAPI(
50
+ docs_url=None, # 关闭 /docs(Swagger UI),避免暴露接口文档
51
+ redoc_url=None, # 关闭 /redoc,同上
52
  )
53
 
54
+ # 允许跨域请求(任何域名都能调用这个 API)
55
+ # 如果你只想允许特定域名调用,把 "*" 改成具体域名列表
56
  app.add_middleware(
57
  CORSMiddleware,
58
+ allow_origins=["*"], # 允许的来源域名,* 表示全部
59
+ allow_methods=["*"], # 允许的 HTTP 方法
60
+ allow_headers=["*"], # 允许的请求头
61
  )
62
 
63
+
64
+ # ============================================================
65
+ # 模型加载(服务启动时只加载一次)
66
+ # ============================================================
67
+ # Slider() 会加载 slider.onnx 模型文件到内存
68
+ # 放在全局意味着整个服务生命周期只加载一次,不用每次请求都读模型
69
  slider = Slider()
70
 
71
 
72
+ # ============================================================
73
+ # 健康检查接口
74
+ # ============================================================
75
+
76
  @app.get("/")
77
  def hello_captcha():
78
+ """根路径,简单返回服务状态,可用于判断服务是否存活"""
79
  return {"Hello": "Captcha", "status": "running"}
80
 
81
 
82
  @app.get("/health")
83
  def health():
84
+ """健康检查接口,部署平台(如 HF Spaces)会定期调用"""
85
  return {"status": "ok"}
86
 
87
 
88
+ # ============================================================
89
+ # 返回数据模型
90
+ # ============================================================
 
 
 
 
 
 
91
 
92
+ class DetectionResult(BaseModel):
93
+ """旧版接口返回格式(只返回缺口,保持向后兼容)"""
94
+ box: List[int] # 缺口坐标 [x1, y1, x2, y2]
95
+ confidence: float # 置信度 0~1
96
+ message: str = None # 错误信息,正常为 null
97
+
98
+
99
+ class SliderGapResult(BaseModel):
100
+ """新版接口返回格式(同时返回滑块和缺口)"""
101
+ slider: List[int] = [] # 滑块坐标 [x1, y1, x2, y2],空列表=未检测到
102
+ slider_confidence: float = 0.0 # 滑块置信度
103
+ gap: List[int] = [] # 缺口坐标 [x1, y1, x2, y2],空列表=未检测到
104
+ gap_confidence: float = 0.0 # 缺口置信度
105
+ offset: int = 0 # 滑动距离 = 缺口x1 - 滑块x1,0=无法计算
106
+ message: Optional[str] = None # 错误信息
107
+
108
+
109
+ # ============================================================
110
+ # 内部公共方法:图片解码 + 识别
111
+ # ============================================================
112
+
113
+ def _decode_and_recognize(image_bytes: bytes) -> SliderGapResult:
114
+ """
115
+ 图片字节 → 识别结果(滑块+缺口)
116
+
117
+ 流程:字节 → OpenCV图片 → 模型推理 → 返回结构化结果
118
+ 所有接口共用这个方法,避免重复代码
119
+ """
120
+ nparr = np.frombuffer(image_bytes, np.uint8)
121
  image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
122
 
123
  if image is None:
124
+ return SliderGapResult(message="不支持的图片")
125
 
126
+ # 调用模型,同时获取滑块和缺口
127
+ result = slider.identify_both(source=image)
128
 
129
+ # 手动释放内存(HF 免费层内存有限,防止 OOM)
130
+ del image, nparr
131
  gc.collect()
132
 
133
+ return SliderGapResult(
134
+ slider=result['slider'],
135
+ slider_confidence=result['slider_confidence'],
136
+ gap=result['gap'],
137
+ gap_confidence=result['gap_confidence'],
138
+ offset=result['offset'],
139
+ )
140
+
141
+
142
+ # ============================================================
143
+ # 旧版接口(保持向后兼容,只返回缺口)
144
+ # ============================================================
145
+
146
+ @app.post("/captcha", response_model=DetectionResult)
147
+ async def captcha(file: UploadFile = File(...)):
148
+ """
149
+ 旧版:上传图片文件识别,只返回缺口
150
+ 用法:POST /captcha,body 里传图片文件(form-data)
151
+ """
152
+ contents = await file.read()
153
+ result = _decode_and_recognize(contents)
154
+ if result.message:
155
+ return DetectionResult(box=[], confidence=0, message=result.message)
156
+ return DetectionResult(box=result.gap, confidence=result.gap_confidence)
157
 
158
 
159
  @app.post("/captcha/base64", response_model=DetectionResult)
160
  async def captcha_base64(data: dict):
161
+ """
162
+ 旧版:上传 base64 图片识别,只返回缺口
163
+ 用法:POST /captcha/base64,JSON body: {"image": "base64字符串"}
164
+ """
165
  import base64
166
 
167
  image_b64 = data.get("image", "")
168
  if not image_b64:
169
  return DetectionResult(box=[], confidence=0, message="缺少image参数")
170
 
171
+ # 去掉 base64 头(如果有),如 "data:image/png;base64,"
172
  if "," in image_b64:
173
  image_b64 = image_b64.split(",")[1]
174
 
175
  try:
176
  image_bytes = base64.b64decode(image_b64)
 
 
177
  except Exception as e:
178
  return DetectionResult(box=[], confidence=0, message=f"base64解码失败: {str(e)}")
179
 
180
+ result = _decode_and_recognize(image_bytes)
181
+ if result.message:
182
+ return DetectionResult(box=[], confidence=0, message=result.message)
183
+ return DetectionResult(box=result.gap, confidence=result.gap_confidence)
184
+
185
+
186
+ # ============================================================
187
+ # 新版接口(返回滑块 + 缺口 + 偏移量)
188
+ # ============================================================
189
+
190
+ @app.post("/captcha/v2", response_model=SliderGapResult)
191
+ async def captcha_v2(file: UploadFile = File(...)):
192
+ """
193
+ 新版:上传图片文件识别,返回滑块+缺口+偏移量
194
+ 用法:POST /captcha/v2,body 里传图片文件(form-data)
195
+
196
+ 返回示例:
197
+ {
198
+ "slider": [23, 78, 62, 117],
199
+ "slider_confidence": 0.92,
200
+ "gap": [185, 76, 224, 115],
201
+ "gap_confidence": 0.88,
202
+ "offset": 162,
203
+ "message": null
204
+ }
205
+ """
206
+ contents = await file.read()
207
+ return _decode_and_recognize(contents)
208
+
209
+
210
+ @app.post("/captcha/v2/base64", response_model=SliderGapResult)
211
+ async def captcha_v2_base64(data: dict):
212
+ """
213
+ 新版:上传 base64 图片识别,返回滑块+缺口+偏移量
214
+
215
+ 请求格式:{"image": "base64字符串"}
216
+ 适合懒人精灵等脚本通过 HTTP POST 调用
217
+
218
+ base64 字符串支持两种格式:
219
+ - 带 data:image 前缀的:data:image/png;base64,iVBORw0KGgo...
220
+ - 纯 base64:iVBORw0KGgo...
221
+ """
222
+ import base64
223
+
224
+ image_b64 = data.get("image", "")
225
+ if not image_b64:
226
+ return SliderGapResult(message="缺少image参数")
227
 
228
+ if "," in image_b64:
229
+ image_b64 = image_b64.split(",")[1]
230
 
231
+ try:
232
+ image_bytes = base64.b64decode(image_b64)
233
+ except Exception as e:
234
+ return SliderGapResult(message=f"base64解码失败: {str(e)}")
235
 
236
+ return _decode_and_recognize(image_bytes)
237
 
238
 
239
+ # ============================================================
240
+ # 启动入口
241
+ # ============================================================
242
  if __name__ == "__main__":
243
  import uvicorn
244
+ # HF Spaces 要求从环境变量读端口号,默认 7860
245
+ # 本地开发时直接 python main.py 即可启动
246
  port = int(os.environ.get("PORT", 7860))
247
  uvicorn.run(app, host="0.0.0.0", port=port)