diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4025eeb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM skin-py-cpu-base:v1.0.2 + +WORKDIR /app + +ENV PYTHONUNBUFFERED=1 +ENV PIP_NO_CACHE_DIR=1 + +COPY wanzheng.py . +COPY api_server.py . +COPY config ./config +COPY best.pt . +COPY shape_predictor_68_face_landmarks.dat . + +EXPOSE 8071 + +CMD ["python", "api_server.py"] + +# 服务器执行 nvidia-smi 查看CUDA版本 比如 CUDA 12.1 就是/whl/cu121 +# RUN pip install \ +# torch==2.4.1 \ +# torchvision==0.19.1 \ +# --index-url https://download.pytorch.org/whl/cu121 diff --git a/__pycache__/api_server.cpython-39.pyc b/__pycache__/api_server.cpython-39.pyc new file mode 100644 index 0000000..a5eed34 Binary files /dev/null and b/__pycache__/api_server.cpython-39.pyc differ diff --git a/__pycache__/test_pore_integration.cpython-39.pyc b/__pycache__/test_pore_integration.cpython-39.pyc new file mode 100644 index 0000000..e9a8f7d Binary files /dev/null and b/__pycache__/test_pore_integration.cpython-39.pyc differ diff --git a/__pycache__/test_regressions.cpython-39.pyc b/__pycache__/test_regressions.cpython-39.pyc new file mode 100644 index 0000000..e425ada Binary files /dev/null and b/__pycache__/test_regressions.cpython-39.pyc differ diff --git a/__pycache__/test_task_type.cpython-39.pyc b/__pycache__/test_task_type.cpython-39.pyc new file mode 100644 index 0000000..9b5bc65 Binary files /dev/null and b/__pycache__/test_task_type.cpython-39.pyc differ diff --git a/__pycache__/wanzheng.cpython-39.pyc b/__pycache__/wanzheng.cpython-39.pyc new file mode 100644 index 0000000..27c84d9 Binary files /dev/null and b/__pycache__/wanzheng.cpython-39.pyc differ diff --git a/api_server.py b/api_server.py new file mode 100644 index 0000000..a9e450b --- /dev/null +++ b/api_server.py @@ -0,0 +1,428 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +皮肤分析 API 接口服务 +""" + +import sys +import os +import threading +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from fastapi import FastAPI, HTTPException, BackgroundTasks, Query +from pydantic import BaseModel, Field +import uvicorn +import logging +from datetime import datetime +from typing import Optional +import subprocess +from pathlib import Path +from config.settings import ( + ANALYSIS_ROOT, + API_HOST, + API_CALLBACK, + MODEL_PATH, + PORE_ENABLED, + PORE_PREDICTOR_PATH, +) + +# ==================== 日志配置 ==================== +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s | %(levelname)s | %(message)s' +) +logger = logging.getLogger(__name__) + +# ==================== FastAPI 应用 ==================== +app = FastAPI( + title="皮肤分析任务接口", + description="接收文件夹名称,触发皮肤分析任务", + version="1.0.0" +) + +# ==================== 数据存储 ==================== +task_store = {} +active_tasks = set() +task_store_lock = threading.RLock() + +# ==================== 数据模型 ==================== +class TaskRequest(BaseModel): + folder_name: str = Field(..., description="文件夹全路径") + task_id: str = Field(..., description="目标文件夹名称(任务 ID)") + task_type: Optional[str] = Field( + default=None, + description="任务类型,pore 表示仅执行大毛孔检测", + ) + +class TaskResponse(BaseModel): + code: int = Field(..., description="状态码") + message: str = Field(..., description="消息") + task_id: str = Field(..., description="任务 ID") + status: str = Field(..., description="任务状态") + +class TaskStatus(BaseModel): + task_id: str = Field(..., description="任务 ID") + status: str = Field(..., description="状态") + created_at: str = Field(..., description="创建时间") + completed_at: Optional[str] = Field(default=None, description="完成时间") + error: Optional[str] = Field(default=None, description="错误信息") + +class CallbackRequest(BaseModel): + task_id: str = Field(..., description="任务 ID") + status: int = Field(..., ge=0, le=1, description="处理状态,1=成功,0=失败") + + +def validate_task_id(task_id: str) -> str: + """校验会进入文件路径、日志和命令参数的任务 ID。""" + if not task_id or len(task_id) < 3 or len(task_id) > 128: + raise ValueError("任务 ID 格式错误,长度必须为 3 到 128 个字符") + if task_id in {".", ".."} or task_id.startswith("-"): + raise ValueError("任务 ID 格式错误") + if any(char in task_id for char in ("/", "\\")): + raise ValueError("任务 ID 不能包含路径分隔符") + if any(ord(char) < 32 or ord(char) == 127 for char in task_id): + raise ValueError("任务 ID 不能包含控制字符") + return task_id + + +def normalize_task_type(task_type: Optional[str]) -> Optional[str]: + """规范化可选任务类型;空值保持现有完整分析流程。""" + normalized = (task_type or "").strip().lower() + if not normalized: + return None + if normalized != "pore": + raise ValueError(f"不支持的任务类型:{task_type}") + return normalized + + +def resolve_input_dir(folder_name: str, task_id: str, now: Optional[datetime] = None) -> Path: + """解析输入目录,并确保目录始终位于分析数据根目录内。""" + root = Path(ANALYSIS_ROOT).resolve() + candidate = Path(folder_name) + if not candidate.is_absolute(): + candidate = root / (now or datetime.now()).strftime("%Y%m%d") / task_id + + candidate = candidate.resolve() + try: + candidate.relative_to(root) + except ValueError as exc: + raise ValueError(f"输入目录必须位于 {root} 内") from exc + if candidate == root: + raise ValueError("输入目录不能是分析数据根目录本身") + return candidate + + +def update_task(task_id: str, **updates) -> None: + """线程安全地更新仍存在的任务。""" + with task_store_lock: + task = task_store.get(task_id) + if task is not None: + task.update(updates) + + +def finish_task(task_id: str, status: str, error: Optional[str]) -> None: + """原子写入最终状态并解除运行标记,避免迟到回调覆盖结果。""" + with task_store_lock: + task = task_store.get(task_id) + if task is not None: + task.update( + status=status, + error=error, + completed_at=datetime.now().isoformat(), + ) + active_tasks.discard(task_id) + +# ==================== 后台任务 ==================== +def run_analysis_task( + task_id: str, + request: TaskRequest, + input_dir: Path, + task_type: Optional[str] = None, +): + """后台运行分析任务""" + + logger.info(f"🚀 开始执行任务:{task_id}") + + try: + update_task(task_id, status="running") + + # 再次解析路径,防止任务排队期间目录被替换为越界符号链接。 + revalidated_input_dir = resolve_input_dir(str(input_dir), task_id) + if revalidated_input_dir != input_dir: + raise Exception("输入目录在任务执行前发生变化") + input_dir = revalidated_input_dir + + # 检查目录是否存在 + if not input_dir.is_dir(): + raise Exception(f"输入目录不存在或不是目录: {input_dir}") + + # ==================== 脚本路径 ==================== + + script_path = Path(__file__).parent / "wanzheng.py" + + if not script_path.exists(): + raise Exception(f"分析脚本不存在: {script_path}") + + # ==================== 模型路径 ==================== + + pore_only = task_type == "pore" + model_path = Path(MODEL_PATH) + + if not pore_only and not model_path.is_file(): + raise Exception(f"模型文件不存在: {model_path}") + + # ==================== 构建命令 ==================== + + cmd = [ + sys.executable, + + str(script_path), + + str(input_dir), + + "--workers", "4", + + "--analysis-root", str(Path(ANALYSIS_ROOT).resolve()), + + "--api-url", f"{API_HOST.rstrip('/')}/{API_CALLBACK.lstrip('/')}", + + "--task-id", task_id, + ] + + if pore_only: + cmd.extend([ + "--task-type", "pore", + "--pore-predictor", PORE_PREDICTOR_PATH, + ]) + else: + cmd.extend(["--model", str(model_path)]) + if PORE_ENABLED: + cmd.extend(["--pore-predictor", PORE_PREDICTOR_PATH]) + else: + cmd.append("--disable-pores") + + logger.info(f"🔧 命令参数:{cmd}") + logger.info(f"📋 执行命令:{' '.join(cmd)}") + logger.info(f"📁 输入目录:{input_dir}") + + # ==================== 执行脚本 ==================== + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + timeout=3600 + ) + + # ==================== 输出日志 ==================== + + if result.stdout: + logger.info(f"📋 标准输出:\n{result.stdout}") + + if result.stderr: + if result.returncode == 0: + logger.info(f"📋 进程日志:\n{result.stderr}") + else: + logger.error(f"📋 错误输出:\n{result.stderr}") + + # ==================== 判断执行结果 ==================== + + if result.returncode == 0: + finish_task(task_id, status="completed", error=None) + + logger.info(f"✅ 任务完成:{task_id}") + + else: + + finish_task( + task_id, + status="failed", + error=result.stderr or f"分析进程退出码:{result.returncode}" + ) + + logger.error( + f"❌ 任务失败:{task_id} " + f"(code={result.returncode})" + ) + + except subprocess.TimeoutExpired: + finish_task( + task_id, + status="failed", + error="任务超时(超过1小时)" + ) + + logger.error(f"⏰ 任务超时:{task_id}") + + except Exception as e: + finish_task( + task_id, + status="failed", + error=str(e) + ) + + logger.exception(f"❌ 任务异常:{task_id}") + +# ==================== API 接口 ==================== +@app.get("/") +async def root(): + return { + "service": "皮肤分析任务接口", + "version": "1.0.0", + "status": "running" + } + +@app.get("/health") +async def health_check(): + with task_store_lock: + statuses = [task["status"] for task in task_store.values()] + return { + "status": "healthy", + "timestamp": datetime.now().isoformat(), + "tasks": { + "total": len(statuses), + "pending": statuses.count("pending"), + "running": statuses.count("running"), + "completed": statuses.count("completed"), + "failed": statuses.count("failed") + } + } + +@app.post("/api/v1/analysis/submit", response_model=TaskResponse) +async def submit_task(request: TaskRequest, background_tasks: BackgroundTasks): + # 使用传入的 task_id,如果没有则从 folder_name 提取 + task_id = request.task_id if request.task_id else Path(request.folder_name).name + + try: + validate_task_id(task_id) + task_type = normalize_task_type(request.task_type) + input_dir = resolve_input_dir(request.folder_name, task_id) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + with task_store_lock: + if task_id in active_tasks: + existing_task = task_store[task_id] + return TaskResponse( + code=200, + message="任务已存在,正在运行中", + task_id=task_id, + status=existing_task["status"] + ) + + task_store[task_id] = { + "task_id": task_id, + "status": "pending", + "created_at": datetime.now().isoformat(), + "completed_at": None, + "error": None, + "request": { + "folder_name": request.folder_name, + "task_id": request.task_id, + "task_type": task_type, + } + } + active_tasks.add(task_id) + + background_tasks.add_task( + run_analysis_task, + task_id, + request, + input_dir, + task_type, + ) + + logger.info(f"📝 任务已提交:{task_id}") + + return TaskResponse( + code=200, + message="任务提交成功", + task_id=task_id, + status="pending" + ) + +@app.post("/api/v1/analysis/callback") +async def analysis_callback(request: CallbackRequest): + try: + task_id = validate_task_id(request.task_id) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + callback_status = "completed" if request.status == 1 else "failed" + + with task_store_lock: + if task_id not in task_store: + task_store[task_id] = { + "task_id": task_id, + "status": callback_status, + "created_at": datetime.now().isoformat(), + "completed_at": datetime.now().isoformat(), + "error": None if request.status == 1 else "分析任务回调失败", + "request": None + } + else: + current_status = task_store[task_id]["status"] + is_finished = current_status in {"completed", "failed"} and task_id not in active_tasks + if not is_finished: + task_store[task_id]["status"] = callback_status + task_store[task_id]["completed_at"] = datetime.now().isoformat() + task_store[task_id]["error"] = ( + None if request.status == 1 else "分析任务回调失败" + ) + + status = task_store[task_id]["status"] + + logger.info(f"📩 收到分析回调:{task_id} status={request.status}") + return {"code": 200, "message": "回调接收成功", "task_id": task_id, "status": status} + +@app.get("/api/v1/analysis/status/{task_id}", response_model=TaskStatus) +async def get_task_status(task_id: str): + with task_store_lock: + task = task_store.get(task_id) + if task is not None: + task = dict(task) + if task is None: + raise HTTPException(status_code=404, detail=f"任务不存在:{task_id}") + return TaskStatus( + task_id=task["task_id"], + status=task["status"], + created_at=task["created_at"], + completed_at=task.get("completed_at"), + error=task.get("error") + ) + +@app.get("/api/v1/analysis/list") +async def list_tasks( + status: Optional[str] = Query(default=None), + limit: int = Query(default=20, ge=1, le=100) +): + with task_store_lock: + tasks = [dict(task) for task in task_store.values()] + + if status: + tasks = [t for t in tasks if t["status"] == status] + + tasks = sorted(tasks, key=lambda x: x["created_at"], reverse=True) + total = len(tasks) + tasks = tasks[:limit] + + return { + "total": total, + "tasks": tasks + } + +# ==================== 启动服务 ==================== +if __name__ == "__main__": + print("="*60) + print("皮肤分析 API 服务") + print("="*60) + print("监听地址:http://0.0.0.0:8071") + print("提交接口:POST /api/v1/analysis/submit") + print("状态查询:GET /api/v1/analysis/status/{task_id}") + print("健康检查:GET /health") + print("="*60) + print() + + uvicorn.run(app, host="0.0.0.0", port=8071) diff --git a/best.pt b/best.pt new file mode 100644 index 0000000..1456d9a Binary files /dev/null and b/best.pt differ diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/__pycache__/__init__.cpython-310.pyc b/config/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..1de2f04 Binary files /dev/null and b/config/__pycache__/__init__.cpython-310.pyc differ diff --git a/config/__pycache__/__init__.cpython-39.pyc b/config/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..c8c52ab Binary files /dev/null and b/config/__pycache__/__init__.cpython-39.pyc differ diff --git a/config/__pycache__/settings.cpython-310.pyc b/config/__pycache__/settings.cpython-310.pyc new file mode 100644 index 0000000..58591f0 Binary files /dev/null and b/config/__pycache__/settings.cpython-310.pyc differ diff --git a/config/__pycache__/settings.cpython-39.pyc b/config/__pycache__/settings.cpython-39.pyc new file mode 100644 index 0000000..5cbaf44 Binary files /dev/null and b/config/__pycache__/settings.cpython-39.pyc differ diff --git a/config/settings.py b/config/settings.py new file mode 100644 index 0000000..15dec5c --- /dev/null +++ b/config/settings.py @@ -0,0 +1,35 @@ +import os +from pathlib import Path + + +PROJECT_ROOT = Path(__file__).resolve().parent.parent + +API_HOST = os.getenv( + "API_HOST", + "http://skin-test-api.ailuowan.com" +) + +API_CALLBACK = os.getenv( + "API_CALLBACK", + "/api/v1/analysis/callback" +) + +ANALYSIS_ROOT = os.getenv( + "ANALYSIS_ROOT", + "/data/analysis" +) + +MODEL_PATH = os.getenv( + "MODEL_PATH", + "/app/best.pt" +) + +PORE_PREDICTOR_PATH = os.getenv( + "PORE_PREDICTOR_PATH", + str(PROJECT_ROOT / "shape_predictor_68_face_landmarks.dat") +) + +PORE_ENABLED = os.getenv( + "PORE_ENABLED", + "1" +).strip().lower() not in {"0", "false", "no", "off"} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..597ccda --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,48 @@ +services: + # 哪个项目-什么语言-做什么 + skin-py-analysis: + build: /data/www/py-skin-analysis-worker + image: analysis-py:latest + working_dir: /app + expose: + - "8071" + volumes: + - /data/www/py-skin-analysis-worker:/app + - /data/analysis:/data/analysis + environment: + - PYTHONUNBUFFERED=1 + command: python api_server.py + restart: unless-stopped + networks: + - default + - common-network +networks: + default: + external: true + name: skin-network + + common-network: + external: true + +# services: +# # 皮肤分析 API 服务 +# skin-api: +# build: . +# image: skin-analysis:latest +# container_name: skin-analysis-api +# ports: +# - "8071:8071" +# volumes: +# # 挂载数据目录 +# - D:/data/analysis:/data/analysis +# environment: +# - PYTHONUNBUFFERED=1 +# command: python api_server.py +# restart: unless-stopped +# deploy: +# resources: +# reservations: +# devices: +# - driver: nvidia +# count: 1 +# capabilities: [gpu] \ No newline at end of file diff --git a/facepp_skin_analysis.json b/facepp_skin_analysis.json new file mode 100644 index 0000000..d5fe743 --- /dev/null +++ b/facepp_skin_analysis.json @@ -0,0 +1,10414 @@ +{ + "request_id": "1786419532,7fbd2b44-fcf1-4a58-a3cf-2ae49974abf3", + "time_used": 3257, + "result": { + "skin_age": { + "value": 29 + }, + "eye_pouch": { + "value": 1, + "confidence": 0.97373587 + }, + "eye_pouch_severity": { + "value": 0, + "confidence": 0.92456836 + }, + "dark_circle": { + "value": 1, + "confidence": 1 + }, + "dark_circle_severity": { + "value": 1, + "confidence": 1 + }, + "forehead_wrinkle": { + "value": 0, + "confidence": 0.8329094 + }, + "crows_feet": { + "value": 0, + "confidence": 0.9784701 + }, + "eye_finelines": { + "value": 0, + "confidence": 0.75139356 + }, + "glabella_wrinkle": { + "value": 0, + "confidence": 0.96045905 + }, + "nasolabial_fold": { + "value": 0, + "confidence": 0.383613 + }, + "skin_type": { + "skin_type": 3, + "details": { + "0": { + "value": 0, + "confidence": 0.047169436 + }, + "1": { + "value": 0, + "confidence": 0.012784755 + }, + "2": { + "value": 0, + "confidence": 0.010530925 + }, + "3": { + "value": 1, + "confidence": 0.9295149 + } + } + }, + "pores_forehead": { + "value": 3, + "confidence": 1 + }, + "pores_left_cheek": { + "value": 3, + "confidence": 1 + }, + "pores_right_cheek": { + "value": 2, + "confidence": 1 + }, + "pores_jaw": { + "value": 0, + "confidence": 1 + }, + "blackhead": { + "value": 3, + "confidence": 1 + }, + "skintone_ita": { + "ITA": 38.754025, + "skintone": 2 + }, + "skin_hue_ha": { + "HA": 79.74526, + "skin_hue": 0 + }, + "acne": { + "rectangle": [ + { + "left": 1448, + "top": 841, + "width": 31, + "height": 32 + }, + { + "left": 584, + "top": 977, + "width": 19, + "height": 25 + }, + { + "left": 1504, + "top": 884, + "width": 31, + "height": 26 + }, + { + "left": 1838, + "top": 1871, + "width": 23, + "height": 45 + }, + { + "left": 1046, + "top": 744, + "width": 19, + "height": 20 + }, + { + "left": 1706, + "top": 2245, + "width": 23, + "height": 40 + }, + { + "left": 1108, + "top": 728, + "width": 23, + "height": 23 + } + ], + "confidence": [ + 0.49821848, + 0.4254656, + 0.42122817, + 0.39108592, + 0.37490568, + 0.3363021, + 0.30310285 + ], + "polygon": [ + [ + { + "x": 1468, + "y": 871 + }, + { + "x": 1456, + "y": 866 + }, + { + "x": 1448, + "y": 855 + }, + { + "x": 1448, + "y": 847 + }, + { + "x": 1467, + "y": 843 + }, + { + "x": 1472, + "y": 853 + } + ], + [ + { + "x": 594, + "y": 999 + }, + { + "x": 588, + "y": 989 + }, + { + "x": 588, + "y": 983 + }, + { + "x": 597, + "y": 977 + }, + { + "x": 601, + "y": 989 + } + ], + [ + { + "x": 1525, + "y": 884 + }, + { + "x": 1533, + "y": 896 + }, + { + "x": 1530, + "y": 905 + }, + { + "x": 1504, + "y": 908 + }, + { + "x": 1504, + "y": 890 + }, + { + "x": 1509, + "y": 884 + } + ], + [ + { + "x": 1860, + "y": 1871 + }, + { + "x": 1852, + "y": 1907 + }, + { + "x": 1840, + "y": 1915 + }, + { + "x": 1840, + "y": 1887 + }, + { + "x": 1848, + "y": 1871 + } + ], + [ + { + "x": 1061, + "y": 763 + }, + { + "x": 1052, + "y": 752 + }, + { + "x": 1060, + "y": 745 + }, + { + "x": 1064, + "y": 746 + }, + { + "x": 1064, + "y": 759 + } + ], + [ + { + "x": 1727, + "y": 2247 + }, + { + "x": 1728, + "y": 2258 + }, + { + "x": 1723, + "y": 2273 + }, + { + "x": 1715, + "y": 2284 + }, + { + "x": 1711, + "y": 2284 + }, + { + "x": 1706, + "y": 2271 + }, + { + "x": 1707, + "y": 2263 + }, + { + "x": 1716, + "y": 2247 + } + ], + [ + { + "x": 1126, + "y": 750 + }, + { + "x": 1120, + "y": 745 + }, + { + "x": 1118, + "y": 728 + }, + { + "x": 1130, + "y": 730 + } + ] + ], + "count": 7 + }, + "mole": { + "rectangle": [ + { + "left": 733, + "top": 2243, + "width": 13, + "height": 14 + }, + { + "left": 639, + "top": 958, + "width": 23, + "height": 26 + } + ], + "confidence": [ + 0.6399757, + 0.555981 + ], + "polygon": [ + [ + { + "x": 742, + "y": 2254 + }, + { + "x": 736, + "y": 2252 + }, + { + "x": 736, + "y": 2245 + }, + { + "x": 741, + "y": 2245 + } + ], + [ + { + "x": 656, + "y": 962 + }, + { + "x": 654, + "y": 980 + }, + { + "x": 643, + "y": 981 + }, + { + "x": 640, + "y": 975 + }, + { + "x": 644, + "y": 968 + }, + { + "x": 651, + "y": 962 + } + ] + ], + "count": 2 + }, + "brown_spot": { + "rectangle": [ + { + "left": 697, + "top": 1666, + "width": 13, + "height": 13 + }, + { + "left": 725, + "top": 1654, + "width": 17, + "height": 16 + }, + { + "left": 891, + "top": 1634, + "width": 22, + "height": 22 + }, + { + "left": 1397, + "top": 1653, + "width": 13, + "height": 17 + }, + { + "left": 880, + "top": 728, + "width": 21, + "height": 18 + }, + { + "left": 1734, + "top": 2098, + "width": 12, + "height": 17 + }, + { + "left": 618, + "top": 2119, + "width": 16, + "height": 26 + }, + { + "left": 512, + "top": 1851, + "width": 15, + "height": 20 + }, + { + "left": 1679, + "top": 1891, + "width": 26, + "height": 42 + }, + { + "left": 685, + "top": 793, + "width": 25, + "height": 23 + }, + { + "left": 1182, + "top": 550, + "width": 16, + "height": 15 + }, + { + "left": 883, + "top": 1736, + "width": 12, + "height": 11 + }, + { + "left": 1601, + "top": 1764, + "width": 13, + "height": 15 + }, + { + "left": 563, + "top": 798, + "width": 16, + "height": 17 + } + ], + "confidence": [ + 0.53899854, + 0.51789784, + 0.50496256, + 0.45168257, + 0.42048925, + 0.4029976, + 0.39821127, + 0.3608284, + 0.34666705, + 0.33999324, + 0.335511, + 0.33337867, + 0.3174294, + 0.31145585 + ], + "polygon": [ + [ + { + "x": 708, + "y": 1666 + }, + { + "x": 709, + "y": 1669 + }, + { + "x": 697, + "y": 1676 + }, + { + "x": 697, + "y": 1672 + }, + { + "x": 703, + "y": 1666 + } + ], + [ + { + "x": 740, + "y": 1659 + }, + { + "x": 739, + "y": 1664 + }, + { + "x": 730, + "y": 1669 + }, + { + "x": 725, + "y": 1661 + }, + { + "x": 731, + "y": 1654 + } + ], + [ + { + "x": 912, + "y": 1646 + }, + { + "x": 900, + "y": 1655 + }, + { + "x": 891, + "y": 1649 + }, + { + "x": 891, + "y": 1634 + }, + { + "x": 911, + "y": 1634 + } + ], + [ + { + "x": 1406, + "y": 1666 + }, + { + "x": 1401, + "y": 1666 + }, + { + "x": 1398, + "y": 1661 + }, + { + "x": 1403, + "y": 1655 + }, + { + "x": 1406, + "y": 1657 + } + ], + [ + { + "x": 900, + "y": 731 + }, + { + "x": 899, + "y": 741 + }, + { + "x": 892, + "y": 744 + }, + { + "x": 885, + "y": 742 + }, + { + "x": 880, + "y": 735 + }, + { + "x": 891, + "y": 730 + } + ], + [ + { + "x": 1743, + "y": 2101 + }, + { + "x": 1744, + "y": 2105 + }, + { + "x": 1739, + "y": 2113 + }, + { + "x": 1735, + "y": 2107 + }, + { + "x": 1739, + "y": 2100 + } + ], + [ + { + "x": 631, + "y": 2144 + }, + { + "x": 624, + "y": 2142 + }, + { + "x": 620, + "y": 2137 + }, + { + "x": 619, + "y": 2119 + }, + { + "x": 626, + "y": 2119 + }, + { + "x": 631, + "y": 2130 + } + ], + [ + { + "x": 519, + "y": 1853 + }, + { + "x": 524, + "y": 1858 + }, + { + "x": 523, + "y": 1868 + }, + { + "x": 520, + "y": 1870 + }, + { + "x": 512, + "y": 1867 + }, + { + "x": 514, + "y": 1854 + } + ], + [ + { + "x": 1695, + "y": 1932 + }, + { + "x": 1680, + "y": 1930 + }, + { + "x": 1681, + "y": 1904 + }, + { + "x": 1686, + "y": 1901 + }, + { + "x": 1703, + "y": 1902 + }, + { + "x": 1704, + "y": 1923 + } + ], + [ + { + "x": 705, + "y": 811 + }, + { + "x": 689, + "y": 815 + }, + { + "x": 685, + "y": 810 + }, + { + "x": 685, + "y": 793 + }, + { + "x": 704, + "y": 793 + } + ], + [ + { + "x": 1197, + "y": 556 + }, + { + "x": 1185, + "y": 560 + }, + { + "x": 1182, + "y": 550 + }, + { + "x": 1197, + "y": 551 + } + ], + [ + { + "x": 894, + "y": 1743 + }, + { + "x": 886, + "y": 1746 + }, + { + "x": 885, + "y": 1741 + }, + { + "x": 888, + "y": 1738 + } + ], + [ + { + "x": 1613, + "y": 1767 + }, + { + "x": 1610, + "y": 1776 + }, + { + "x": 1601, + "y": 1773 + }, + { + "x": 1605, + "y": 1764 + } + ], + [ + { + "x": 575, + "y": 803 + }, + { + "x": 576, + "y": 809 + }, + { + "x": 572, + "y": 814 + }, + { + "x": 565, + "y": 814 + }, + { + "x": 564, + "y": 808 + }, + { + "x": 567, + "y": 804 + } + ] + ], + "count": 14 + }, + "closed_comedones": { + "rectangle": [ + { + "left": 1034, + "top": 1253, + "width": 17, + "height": 18 + } + ], + "confidence": [ + 0.40557525 + ], + "polygon": [ + [ + { + "x": 1050, + "y": 1265 + }, + { + "x": 1049, + "y": 1268 + }, + { + "x": 1037, + "y": 1268 + }, + { + "x": 1039, + "y": 1262 + } + ] + ], + "count": 1 + }, + "acne_mark": { + "rectangle": [ + { + "left": 1328, + "top": 1845, + "width": 23, + "height": 31 + }, + { + "left": 1153, + "top": 2275, + "width": 33, + "height": 41 + }, + { + "left": 1080, + "top": 2265, + "width": 36, + "height": 43 + }, + { + "left": 1752, + "top": 1969, + "width": 26, + "height": 41 + }, + { + "left": 731, + "top": 2314, + "width": 25, + "height": 37 + }, + { + "left": 1313, + "top": 1692, + "width": 24, + "height": 31 + }, + { + "left": 1494, + "top": 1809, + "width": 33, + "height": 28 + }, + { + "left": 1654, + "top": 956, + "width": 31, + "height": 34 + } + ], + "confidence": [ + 0.5175053, + 0.40775132, + 0.3817651, + 0.37412858, + 0.3720529, + 0.36681947, + 0.32264802, + 0.315921 + ], + "polygon": [ + [ + { + "x": 1346, + "y": 1873 + }, + { + "x": 1340, + "y": 1875 + }, + { + "x": 1332, + "y": 1864 + }, + { + "x": 1333, + "y": 1848 + }, + { + "x": 1344, + "y": 1848 + }, + { + "x": 1349, + "y": 1863 + } + ], + [ + { + "x": 1182, + "y": 2306 + }, + { + "x": 1175, + "y": 2311 + }, + { + "x": 1158, + "y": 2312 + }, + { + "x": 1155, + "y": 2307 + }, + { + "x": 1155, + "y": 2285 + }, + { + "x": 1178, + "y": 2282 + }, + { + "x": 1183, + "y": 2292 + } + ], + [ + { + "x": 1111, + "y": 2305 + }, + { + "x": 1089, + "y": 2306 + }, + { + "x": 1081, + "y": 2297 + }, + { + "x": 1080, + "y": 2283 + }, + { + "x": 1089, + "y": 2270 + }, + { + "x": 1099, + "y": 2267 + }, + { + "x": 1114, + "y": 2274 + } + ], + [ + { + "x": 1756, + "y": 2009 + }, + { + "x": 1752, + "y": 2006 + }, + { + "x": 1755, + "y": 1990 + }, + { + "x": 1765, + "y": 1972 + }, + { + "x": 1775, + "y": 1969 + }, + { + "x": 1777, + "y": 2009 + } + ], + [ + { + "x": 750, + "y": 2350 + }, + { + "x": 740, + "y": 2346 + }, + { + "x": 732, + "y": 2321 + }, + { + "x": 733, + "y": 2316 + }, + { + "x": 744, + "y": 2316 + }, + { + "x": 754, + "y": 2335 + } + ], + [ + { + "x": 1336, + "y": 1722 + }, + { + "x": 1328, + "y": 1721 + }, + { + "x": 1317, + "y": 1709 + }, + { + "x": 1315, + "y": 1695 + }, + { + "x": 1319, + "y": 1693 + }, + { + "x": 1331, + "y": 1699 + } + ], + [ + { + "x": 1520, + "y": 1836 + }, + { + "x": 1502, + "y": 1836 + }, + { + "x": 1495, + "y": 1830 + }, + { + "x": 1494, + "y": 1818 + }, + { + "x": 1502, + "y": 1809 + }, + { + "x": 1515, + "y": 1809 + }, + { + "x": 1523, + "y": 1817 + } + ], + [ + { + "x": 1684, + "y": 957 + }, + { + "x": 1684, + "y": 972 + }, + { + "x": 1675, + "y": 989 + }, + { + "x": 1657, + "y": 989 + }, + { + "x": 1654, + "y": 975 + }, + { + "x": 1657, + "y": 966 + }, + { + "x": 1669, + "y": 956 + } + ] + ], + "count": 8 + }, + "acne_nodule": { + "rectangle": [], + "confidence": [], + "polygon": [], + "count": 0 + }, + "acne_pustule": { + "rectangle": [], + "confidence": [], + "polygon": [], + "count": 0 + }, + "blackhead_count": 203, + "skintone": { + "value": 2, + "confidence": 0.5237101 + }, + "fine_line": { + "forehead_count": 16, + "left_undereye_count": 103, + "right_undereye_count": 74, + "left_cheek_count": 2, + "right_cheek_count": 1, + "left_crowsfeet_count": 13, + "right_crowsfeet_count": 15, + "glabella_count": 19 + }, + "wrinkle_count": { + "forehead_count": 16, + "left_undereye_count": 149, + "right_undereye_count": 113, + "left_mouth_count": 1, + "right_mouth_count": 0, + "left_nasolabial_count": 1, + "right_nasolabial_count": 1, + "glabella_count": 28, + "left_cheek_count": 1, + "right_cheek_count": 1, + "left_crowsfeet_count": 17, + "right_crowsfeet_count": 19 + }, + "oily_intensity": { + "t_zone": { + "area": 0.48, + "intensity": 2 + }, + "left_cheek": { + "area": 0.09, + "intensity": 0 + }, + "right_cheek": { + "area": 0.1, + "intensity": 0 + }, + "chin_area": { + "area": 0, + "intensity": 0 + }, + "full_face": { + "area": 0.35, + "intensity": 2 + } + }, + "enlarged_pore_count": { + "forehead_count": 1132, + "left_cheek_count": 228, + "right_cheek_count": 180, + "chin_count": 36 + }, + "face_maps": { + "texture_enhanced_pores": { + "saved_to": "D:\\桌面\\皮肤分析仪\\分析毛孔代码\\facepp_output\\texture_enhanced_pores.png", + "base64_omitted": true + } + }, + "red_spot": { + "red_spot_area": 0.017, + "red_spot_intensity": 11 + }, + "right_dark_circle_rete": { + "value": 0 + }, + "left_dark_circle_rete": { + "value": 0 + }, + "right_dark_circle_pigment": { + "value": 2 + }, + "left_dark_circle_pigment": { + "value": 1 + }, + "right_dark_circle_structural": { + "value": 1 + }, + "left_dark_circle_structural": { + "value": 0 + }, + "dark_circle_mark": { + "left_eye_rect": { + "left": 518, + "top": 1333, + "width": 476, + "height": 411 + }, + "right_eye_rect": { + "left": 1338, + "top": 1301, + "width": 472, + "height": 410 + } + }, + "water": { + "water_severity": 43, + "water_area": 0.365, + "water_forehead": { + "area": 0.384 + }, + "water_leftcheek": { + "area": 0.353 + }, + "water_rightcheek": { + "area": 0.316 + } + }, + "rough": { + "rough_severity": 15, + "rough_area": 0.231, + "rough_forehead": { + "area": 0.253 + }, + "rough_leftcheek": { + "area": 0.237 + }, + "rough_rightcheek": { + "area": 0.234 + }, + "rough_jaw": { + "area": 0.045 + } + }, + "left_mouth_wrinkle_severity": { + "value": 2 + }, + "right_mouth_wrinkle_severity": { + "value": 0 + }, + "forehead_wrinkle_severity": { + "value": 1 + }, + "left_crows_feet_severity": { + "value": 3 + }, + "right_crows_feet_severity": { + "value": 3 + }, + "left_eye_finelines_severity": { + "value": 3 + }, + "right_eye_finelines_severity": { + "value": 2 + }, + "glabella_wrinkle_severity": { + "value": 1 + }, + "left_nasolabial_fold_severity": { + "value": 1 + }, + "right_nasolabial_fold_severity": { + "value": 2 + }, + "left_cheek_wrinkle_severity": { + "value": 0 + }, + "right_cheek_wrinkle_severity": { + "value": 0 + }, + "forehead_wrinkle_info": { + "wrinkle_score": 16, + "wrinkle_severity_level": 1, + "wrinkle_norm_length": 1.5303003083936617, + "wrinkle_norm_depth": 0.5669443376479132, + "wrinkle_pixel_density": 0.05918007580514332, + "wrinkle_area_ratio": 0.03573854208757103, + "wrinkle_deep_ratio": 0.5145969498910675, + "wrinkle_deep_num": 16, + "wrinkle_shallow_num": 16 + }, + "left_eye_wrinkle_info": { + "wrinkle_score": 77, + "wrinkle_severity_level": 3, + "wrinkle_norm_length": 13.324172642451334, + "wrinkle_norm_depth": 0.7910559572666276, + "wrinkle_pixel_density": 3.400079784096783, + "wrinkle_area_ratio": 0.18070525766411763, + "wrinkle_deep_ratio": 0.6267204556241102, + "wrinkle_deep_num": 149, + "wrinkle_shallow_num": 103 + }, + "right_eye_wrinkle_info": { + "wrinkle_score": 61, + "wrinkle_severity_level": 2, + "wrinkle_norm_length": 10.458644074809895, + "wrinkle_norm_depth": 0.8287127758294467, + "wrinkle_pixel_density": 2.7538035080724677, + "wrinkle_area_ratio": 0.15082124585864973, + "wrinkle_deep_ratio": 0.6484326982175783, + "wrinkle_deep_num": 113, + "wrinkle_shallow_num": 74 + }, + "left_crowsfeet_wrinkle_info": { + "wrinkle_score": 100, + "wrinkle_severity_level": 3, + "wrinkle_norm_length": 7.2976246935874896, + "wrinkle_norm_depth": 0.8323104631800727, + "wrinkle_pixel_density": 5.644740433320981, + "wrinkle_area_ratio": 0.13233827423736974, + "wrinkle_deep_ratio": 0.5545927209705372, + "wrinkle_deep_num": 17, + "wrinkle_shallow_num": 13 + }, + "right_crowsfeet_wrinkle_info": { + "wrinkle_score": 100, + "wrinkle_severity_level": 3, + "wrinkle_norm_length": 8.330917468318146, + "wrinkle_norm_depth": 0.838940329218107, + "wrinkle_pixel_density": 6.163555297233983, + "wrinkle_area_ratio": 0.14098684210526316, + "wrinkle_deep_ratio": 0.5787037037037037, + "wrinkle_deep_num": 19, + "wrinkle_shallow_num": 15 + }, + "glabella_wrinkle_info": { + "wrinkle_score": 25, + "wrinkle_severity_level": 1, + "wrinkle_norm_length": 2.899235616947992, + "wrinkle_norm_depth": 0.8284052167325071, + "wrinkle_pixel_density": 0.26925431040450654, + "wrinkle_area_ratio": 0.04457896497190072, + "wrinkle_deep_ratio": 0.6108555657773689, + "wrinkle_deep_num": 28, + "wrinkle_shallow_num": 19 + }, + "left_mouth_wrinkle_info": { + "wrinkle_score": 52, + "wrinkle_severity_level": 2, + "wrinkle_norm_length": 0.3708162061716884, + "wrinkle_norm_depth": 0.24040307317461287, + "wrinkle_pixel_density": 0.40943006046944014, + "wrinkle_area_ratio": 0.08340697103583701, + "wrinkle_deep_ratio": 1, + "wrinkle_deep_num": 1, + "wrinkle_shallow_num": 0 + }, + "right_mouth_wrinkle_info": { + "wrinkle_score": 0, + "wrinkle_severity_level": 0, + "wrinkle_norm_length": 0, + "wrinkle_norm_depth": 0, + "wrinkle_pixel_density": 0, + "wrinkle_area_ratio": 0, + "wrinkle_deep_ratio": 0, + "wrinkle_deep_num": 0, + "wrinkle_shallow_num": 0 + }, + "left_nasolabial_wrinkle_info": { + "wrinkle_score": 23, + "wrinkle_severity_level": 1, + "wrinkle_norm_length": 0.14936264447938136, + "wrinkle_norm_depth": 0.22605042016806723, + "wrinkle_pixel_density": 0.050316647124727824, + "wrinkle_area_ratio": 0.01999580612306033, + "wrinkle_deep_ratio": 1, + "wrinkle_deep_num": 1, + "wrinkle_shallow_num": 0 + }, + "right_nasolabial_wrinkle_info": { + "wrinkle_score": 31, + "wrinkle_severity_level": 2, + "wrinkle_norm_length": 0.16136499983933164, + "wrinkle_norm_depth": 0.3105392156862745, + "wrinkle_pixel_density": 0.06105697487317477, + "wrinkle_area_ratio": 0.0571491058360391, + "wrinkle_deep_ratio": 1, + "wrinkle_deep_num": 1, + "wrinkle_shallow_num": 0 + }, + "left_cheek_wrinkle_info": { + "wrinkle_score": 5, + "wrinkle_severity_level": 0, + "wrinkle_norm_length": 0.1186899585595084, + "wrinkle_norm_depth": 0.7595946243666005, + "wrinkle_pixel_density": 0.01754283747185602, + "wrinkle_area_ratio": 0.0030032398188855813, + "wrinkle_deep_ratio": 0.3146067415730337, + "wrinkle_deep_num": 1, + "wrinkle_shallow_num": 2 + }, + "right_cheek_wrinkle_info": { + "wrinkle_score": 4, + "wrinkle_severity_level": 0, + "wrinkle_norm_length": 0.0906844627196244, + "wrinkle_norm_depth": 0.7515570934256056, + "wrinkle_pixel_density": 0.013992670705573024, + "wrinkle_area_ratio": 0.0023257090325324845, + "wrinkle_deep_ratio": 0.4411764705882353, + "wrinkle_deep_num": 1, + "wrinkle_shallow_num": 1 + }, + "score_info": { + "dark_circle_score": 71, + "skin_type_score": 51, + "wrinkle_score": 56, + "oily_intensity_score": 41, + "pores_score": 59, + "blackhead_score": 39, + "acne_score": 82, + "sensitivity_score": 95, + "melanin_score": 76, + "water_score": 57, + "rough_score": 85, + "total_score": 70, + "pores_type_score": { + "pores_forehead_score": 30, + "pores_leftcheek_score": 50, + "pores_rightcheek_score": 60, + "pores_jaw_score": 94 + }, + "dark_circle_type_score": { + "left_dark_circle_score": 93, + "right_dark_circle_score": 78 + }, + "red_spot_score": 89 + }, + "left_eye_pouch_rect": { + "left": 518, + "top": 1333, + "width": 476, + "height": 411 + }, + "right_eye_pouch_rect": { + "left": 1338, + "top": 1301, + "width": 472, + "height": 410 + }, + "melasma": { + "value": 0, + "confidence": 0.35302654 + }, + "freckle": { + "value": 0, + "confidence": 0.41760963 + }, + "image_quality": { + "face_rect": { + "left": 413, + "top": 442, + "width": 1510, + "height": 2423 + }, + "face_ratio": 0.38869888, + "hair_occlusion": 0.014742425, + "face_orientation": { + "yaw": -1.6155833, + "pitch": 13.527489, + "roll": 6.3106704 + }, + "glasses": 0 + }, + "sensitivity_type_v1": 0, + "blackheads_mark": { + "coord": [ + { + "x": 1142, + "y": 1476, + "radius": 1 + }, + { + "x": 1072, + "y": 1480, + "radius": 1 + }, + { + "x": 1242, + "y": 1481, + "radius": 1 + }, + { + "x": 1152, + "y": 1488, + "radius": 1 + }, + { + "x": 1166, + "y": 1490, + "radius": 2 + }, + { + "x": 1212, + "y": 1488, + "radius": 1 + }, + { + "x": 1235, + "y": 1494, + "radius": 2 + }, + { + "x": 1109, + "y": 1500, + "radius": 2 + }, + { + "x": 1138, + "y": 1502, + "radius": 2 + }, + { + "x": 1155, + "y": 1504, + "radius": 2 + }, + { + "x": 1116, + "y": 1510, + "radius": 2 + }, + { + "x": 1219, + "y": 1511, + "radius": 1 + }, + { + "x": 1137, + "y": 1514, + "radius": 2 + }, + { + "x": 1320, + "y": 1521, + "radius": 1 + }, + { + "x": 1117, + "y": 1525, + "radius": 2 + }, + { + "x": 1139, + "y": 1533, + "radius": 2 + }, + { + "x": 1159, + "y": 1537, + "radius": 2 + }, + { + "x": 1223, + "y": 1541, + "radius": 1 + }, + { + "x": 1155, + "y": 1552, + "radius": 2 + }, + { + "x": 1276, + "y": 1564, + "radius": 1 + }, + { + "x": 1128, + "y": 1567, + "radius": 2 + }, + { + "x": 1218, + "y": 1583, + "radius": 2 + }, + { + "x": 1134, + "y": 1592, + "radius": 2 + }, + { + "x": 1108, + "y": 1594, + "radius": 1 + }, + { + "x": 1171, + "y": 1599, + "radius": 1 + }, + { + "x": 1141, + "y": 1608, + "radius": 2 + }, + { + "x": 1335, + "y": 1611, + "radius": 1 + }, + { + "x": 1208, + "y": 1613, + "radius": 1 + }, + { + "x": 1168, + "y": 1617, + "radius": 2 + }, + { + "x": 1222, + "y": 1628, + "radius": 2 + }, + { + "x": 1212, + "y": 1633, + "radius": 1 + }, + { + "x": 1226, + "y": 1638, + "radius": 1 + }, + { + "x": 1235, + "y": 1643, + "radius": 1 + }, + { + "x": 1245, + "y": 1647, + "radius": 2 + }, + { + "x": 1261, + "y": 1650, + "radius": 1 + }, + { + "x": 1130, + "y": 1652, + "radius": 1 + }, + { + "x": 1141, + "y": 1653, + "radius": 1 + }, + { + "x": 1043, + "y": 1657, + "radius": 1 + }, + { + "x": 1161, + "y": 1659, + "radius": 2 + }, + { + "x": 1145, + "y": 1663, + "radius": 2 + }, + { + "x": 1169, + "y": 1662, + "radius": 2 + }, + { + "x": 1263, + "y": 1665, + "radius": 2 + }, + { + "x": 1275, + "y": 1668, + "radius": 2 + }, + { + "x": 1305, + "y": 1668, + "radius": 1 + }, + { + "x": 1169, + "y": 1674, + "radius": 1 + }, + { + "x": 1180, + "y": 1676, + "radius": 2 + }, + { + "x": 1134, + "y": 1681, + "radius": 2 + }, + { + "x": 1255, + "y": 1683, + "radius": 2 + }, + { + "x": 1204, + "y": 1686, + "radius": 2 + }, + { + "x": 1266, + "y": 1684, + "radius": 1 + }, + { + "x": 1147, + "y": 1688, + "radius": 2 + }, + { + "x": 1244, + "y": 1692, + "radius": 1 + }, + { + "x": 1261, + "y": 1700, + "radius": 2 + }, + { + "x": 1229, + "y": 1700, + "radius": 2 + }, + { + "x": 1274, + "y": 1700, + "radius": 1 + }, + { + "x": 1248, + "y": 1707, + "radius": 2 + }, + { + "x": 1144, + "y": 1720, + "radius": 2 + }, + { + "x": 1278, + "y": 1727, + "radius": 2 + }, + { + "x": 1350, + "y": 1728, + "radius": 2 + }, + { + "x": 1185, + "y": 1732, + "radius": 2 + }, + { + "x": 1127, + "y": 1735, + "radius": 2 + }, + { + "x": 1164, + "y": 1734, + "radius": 1 + }, + { + "x": 1283, + "y": 1738, + "radius": 1 + }, + { + "x": 1059, + "y": 1738, + "radius": 2 + }, + { + "x": 1194, + "y": 1744, + "radius": 1 + }, + { + "x": 1307, + "y": 1746, + "radius": 2 + }, + { + "x": 1204, + "y": 1750, + "radius": 1 + }, + { + "x": 1133, + "y": 1754, + "radius": 2 + }, + { + "x": 1121, + "y": 1755, + "radius": 1 + }, + { + "x": 1172, + "y": 1755, + "radius": 2 + }, + { + "x": 1276, + "y": 1756, + "radius": 2 + }, + { + "x": 1305, + "y": 1758, + "radius": 1 + }, + { + "x": 1242, + "y": 1763, + "radius": 2 + }, + { + "x": 1130, + "y": 1763, + "radius": 1 + }, + { + "x": 1273, + "y": 1763, + "radius": 2 + }, + { + "x": 1261, + "y": 1767, + "radius": 2 + }, + { + "x": 1087, + "y": 1767, + "radius": 1 + }, + { + "x": 1167, + "y": 1774, + "radius": 1 + }, + { + "x": 1045, + "y": 1782, + "radius": 2 + }, + { + "x": 1089, + "y": 1781, + "radius": 2 + }, + { + "x": 1098, + "y": 1783, + "radius": 2 + }, + { + "x": 1142, + "y": 1788, + "radius": 1 + }, + { + "x": 1267, + "y": 1785, + "radius": 1 + }, + { + "x": 1287, + "y": 1787, + "radius": 1 + }, + { + "x": 1371, + "y": 1785, + "radius": 1 + }, + { + "x": 1121, + "y": 1794, + "radius": 1 + }, + { + "x": 1142, + "y": 1800, + "radius": 2 + }, + { + "x": 1248, + "y": 1804, + "radius": 2 + }, + { + "x": 1087, + "y": 1806, + "radius": 1 + }, + { + "x": 1102, + "y": 1807, + "radius": 2 + }, + { + "x": 1163, + "y": 1813, + "radius": 2 + }, + { + "x": 1174, + "y": 1813, + "radius": 1 + }, + { + "x": 1194, + "y": 1811, + "radius": 1 + }, + { + "x": 1243, + "y": 1821, + "radius": 1 + }, + { + "x": 1088, + "y": 1824, + "radius": 2 + }, + { + "x": 1285, + "y": 1831, + "radius": 1 + }, + { + "x": 1334, + "y": 1831, + "radius": 2 + }, + { + "x": 1366, + "y": 1833, + "radius": 2 + }, + { + "x": 1292, + "y": 1834, + "radius": 2 + }, + { + "x": 1319, + "y": 1839, + "radius": 2 + }, + { + "x": 1110, + "y": 1839, + "radius": 1 + }, + { + "x": 1202, + "y": 1841, + "radius": 2 + }, + { + "x": 1084, + "y": 1844, + "radius": 1 + }, + { + "x": 1289, + "y": 1846, + "radius": 1 + }, + { + "x": 1301, + "y": 1845, + "radius": 1 + }, + { + "x": 1122, + "y": 1849, + "radius": 2 + }, + { + "x": 1267, + "y": 1851, + "radius": 1 + }, + { + "x": 1159, + "y": 1853, + "radius": 1 + }, + { + "x": 1111, + "y": 1854, + "radius": 2 + }, + { + "x": 1219, + "y": 1857, + "radius": 2 + }, + { + "x": 1314, + "y": 1859, + "radius": 2 + }, + { + "x": 1164, + "y": 1862, + "radius": 2 + }, + { + "x": 1245, + "y": 1861, + "radius": 2 + }, + { + "x": 1052, + "y": 1863, + "radius": 2 + }, + { + "x": 1272, + "y": 1864, + "radius": 1 + }, + { + "x": 1345, + "y": 1863, + "radius": 1 + }, + { + "x": 1034, + "y": 1868, + "radius": 1 + }, + { + "x": 1124, + "y": 1866, + "radius": 2 + }, + { + "x": 1146, + "y": 1865, + "radius": 2 + }, + { + "x": 1105, + "y": 1870, + "radius": 2 + }, + { + "x": 1182, + "y": 1868, + "radius": 1 + }, + { + "x": 1390, + "y": 1868, + "radius": 2 + }, + { + "x": 1094, + "y": 1871, + "radius": 2 + }, + { + "x": 1304, + "y": 1870, + "radius": 1 + }, + { + "x": 1403, + "y": 1874, + "radius": 1 + }, + { + "x": 1106, + "y": 1881, + "radius": 2 + }, + { + "x": 1164, + "y": 1886, + "radius": 1 + }, + { + "x": 1379, + "y": 1885, + "radius": 2 + }, + { + "x": 1078, + "y": 1889, + "radius": 1 + }, + { + "x": 1057, + "y": 1891, + "radius": 1 + }, + { + "x": 1087, + "y": 1891, + "radius": 1 + }, + { + "x": 1160, + "y": 1894, + "radius": 2 + }, + { + "x": 1271, + "y": 1894, + "radius": 1 + }, + { + "x": 1021, + "y": 1896, + "radius": 1 + }, + { + "x": 1307, + "y": 1896, + "radius": 2 + }, + { + "x": 1412, + "y": 1897, + "radius": 2 + }, + { + "x": 1161, + "y": 1904, + "radius": 1 + }, + { + "x": 1185, + "y": 1910, + "radius": 2 + }, + { + "x": 1055, + "y": 1910, + "radius": 2 + }, + { + "x": 1044, + "y": 1914, + "radius": 2 + }, + { + "x": 998, + "y": 1915, + "radius": 1 + }, + { + "x": 1094, + "y": 1920, + "radius": 1 + }, + { + "x": 1110, + "y": 1919, + "radius": 2 + }, + { + "x": 1037, + "y": 1923, + "radius": 2 + }, + { + "x": 1291, + "y": 1922, + "radius": 1 + }, + { + "x": 1055, + "y": 1923, + "radius": 1 + }, + { + "x": 1103, + "y": 1926, + "radius": 1 + }, + { + "x": 1316, + "y": 1930, + "radius": 1 + }, + { + "x": 1325, + "y": 1931, + "radius": 2 + }, + { + "x": 1361, + "y": 1932, + "radius": 2 + }, + { + "x": 1281, + "y": 1931, + "radius": 1 + }, + { + "x": 1198, + "y": 1935, + "radius": 1 + }, + { + "x": 1053, + "y": 1935, + "radius": 2 + }, + { + "x": 1082, + "y": 1937, + "radius": 2 + }, + { + "x": 1113, + "y": 1948, + "radius": 1 + }, + { + "x": 1349, + "y": 1947, + "radius": 2 + }, + { + "x": 1193, + "y": 1948, + "radius": 1 + }, + { + "x": 1334, + "y": 1951, + "radius": 2 + }, + { + "x": 1061, + "y": 1953, + "radius": 1 + }, + { + "x": 1401, + "y": 1958, + "radius": 2 + }, + { + "x": 997, + "y": 1960, + "radius": 2 + }, + { + "x": 1324, + "y": 1959, + "radius": 2 + }, + { + "x": 1040, + "y": 1963, + "radius": 2 + }, + { + "x": 1089, + "y": 1964, + "radius": 2 + }, + { + "x": 1163, + "y": 1964, + "radius": 2 + }, + { + "x": 983, + "y": 1966, + "radius": 2 + }, + { + "x": 1215, + "y": 1967, + "radius": 2 + }, + { + "x": 1180, + "y": 1971, + "radius": 2 + }, + { + "x": 1106, + "y": 1974, + "radius": 2 + }, + { + "x": 1403, + "y": 1976, + "radius": 2 + }, + { + "x": 1023, + "y": 1978, + "radius": 2 + }, + { + "x": 1270, + "y": 1978, + "radius": 1 + }, + { + "x": 1325, + "y": 1985, + "radius": 2 + }, + { + "x": 986, + "y": 1985, + "radius": 2 + }, + { + "x": 1166, + "y": 1987, + "radius": 2 + }, + { + "x": 1313, + "y": 1988, + "radius": 2 + }, + { + "x": 1345, + "y": 1986, + "radius": 2 + }, + { + "x": 1134, + "y": 1993, + "radius": 2 + }, + { + "x": 1121, + "y": 1992, + "radius": 2 + }, + { + "x": 1370, + "y": 1991, + "radius": 1 + }, + { + "x": 1412, + "y": 1992, + "radius": 2 + }, + { + "x": 1009, + "y": 1993, + "radius": 2 + }, + { + "x": 1105, + "y": 1993, + "radius": 1 + }, + { + "x": 1030, + "y": 1996, + "radius": 2 + }, + { + "x": 984, + "y": 2001, + "radius": 2 + }, + { + "x": 1057, + "y": 2002, + "radius": 2 + }, + { + "x": 1127, + "y": 2002, + "radius": 2 + }, + { + "x": 1314, + "y": 2001, + "radius": 2 + }, + { + "x": 1001, + "y": 2005, + "radius": 1 + }, + { + "x": 1085, + "y": 2004, + "radius": 1 + }, + { + "x": 1104, + "y": 2015, + "radius": 2 + }, + { + "x": 1037, + "y": 2021, + "radius": 1 + }, + { + "x": 1128, + "y": 2028, + "radius": 2 + }, + { + "x": 1271, + "y": 2029, + "radius": 2 + }, + { + "x": 1138, + "y": 2037, + "radius": 1 + }, + { + "x": 1265, + "y": 2039, + "radius": 2 + }, + { + "x": 1145, + "y": 2043, + "radius": 1 + }, + { + "x": 1225, + "y": 2049, + "radius": 2 + }, + { + "x": 1291, + "y": 2050, + "radius": 2 + }, + { + "x": 1185, + "y": 2055, + "radius": 2 + }, + { + "x": 1214, + "y": 2058, + "radius": 2 + }, + { + "x": 1171, + "y": 2067, + "radius": 2 + }, + { + "x": 1258, + "y": 2079, + "radius": 2 + } + ] + }, + "pores_mark": { + "coord": [ + { + "x": 1255, + "y": 530, + "radius": 1 + }, + { + "x": 1390, + "y": 540, + "radius": 2 + }, + { + "x": 1031, + "y": 557, + "radius": 1 + }, + { + "x": 1275, + "y": 559, + "radius": 2 + }, + { + "x": 1300, + "y": 561, + "radius": 1 + }, + { + "x": 1328, + "y": 568, + "radius": 2 + }, + { + "x": 833, + "y": 573, + "radius": 1 + }, + { + "x": 1055, + "y": 572, + "radius": 1 + }, + { + "x": 1283, + "y": 575, + "radius": 1 + }, + { + "x": 1390, + "y": 578, + "radius": 1 + }, + { + "x": 1076, + "y": 580, + "radius": 2 + }, + { + "x": 1433, + "y": 581, + "radius": 1 + }, + { + "x": 899, + "y": 584, + "radius": 2 + }, + { + "x": 1266, + "y": 582, + "radius": 1 + }, + { + "x": 775, + "y": 584, + "radius": 1 + }, + { + "x": 1182, + "y": 587, + "radius": 2 + }, + { + "x": 1243, + "y": 585, + "radius": 1 + }, + { + "x": 855, + "y": 589, + "radius": 1 + }, + { + "x": 973, + "y": 591, + "radius": 2 + }, + { + "x": 1506, + "y": 589, + "radius": 1 + }, + { + "x": 1291, + "y": 590, + "radius": 1 + }, + { + "x": 947, + "y": 593, + "radius": 2 + }, + { + "x": 1025, + "y": 598, + "radius": 1 + }, + { + "x": 1163, + "y": 606, + "radius": 1 + }, + { + "x": 866, + "y": 609, + "radius": 2 + }, + { + "x": 952, + "y": 607, + "radius": 1 + }, + { + "x": 1192, + "y": 610, + "radius": 2 + }, + { + "x": 799, + "y": 617, + "radius": 1 + }, + { + "x": 1223, + "y": 617, + "radius": 2 + }, + { + "x": 1239, + "y": 616, + "radius": 2 + }, + { + "x": 889, + "y": 619, + "radius": 2 + }, + { + "x": 786, + "y": 619, + "radius": 1 + }, + { + "x": 871, + "y": 619, + "radius": 2 + }, + { + "x": 1010, + "y": 625, + "radius": 1 + }, + { + "x": 1294, + "y": 624, + "radius": 1 + }, + { + "x": 1279, + "y": 625, + "radius": 2 + }, + { + "x": 799, + "y": 628, + "radius": 2 + }, + { + "x": 1501, + "y": 628, + "radius": 1 + }, + { + "x": 994, + "y": 630, + "radius": 1 + }, + { + "x": 1176, + "y": 630, + "radius": 1 + }, + { + "x": 1252, + "y": 631, + "radius": 2 + }, + { + "x": 1385, + "y": 633, + "radius": 1 + }, + { + "x": 1263, + "y": 633, + "radius": 2 + }, + { + "x": 843, + "y": 639, + "radius": 1 + }, + { + "x": 1092, + "y": 639, + "radius": 2 + }, + { + "x": 877, + "y": 641, + "radius": 1 + }, + { + "x": 802, + "y": 641, + "radius": 1 + }, + { + "x": 848, + "y": 643, + "radius": 2 + }, + { + "x": 866, + "y": 645, + "radius": 2 + }, + { + "x": 1394, + "y": 643, + "radius": 1 + }, + { + "x": 1414, + "y": 643, + "radius": 1 + }, + { + "x": 812, + "y": 647, + "radius": 1 + }, + { + "x": 1021, + "y": 647, + "radius": 2 + }, + { + "x": 1541, + "y": 646, + "radius": 1 + }, + { + "x": 1260, + "y": 648, + "radius": 2 + }, + { + "x": 958, + "y": 650, + "radius": 2 + }, + { + "x": 1313, + "y": 650, + "radius": 1 + }, + { + "x": 1427, + "y": 655, + "radius": 2 + }, + { + "x": 1489, + "y": 653, + "radius": 2 + }, + { + "x": 1495, + "y": 652, + "radius": 1 + }, + { + "x": 918, + "y": 655, + "radius": 2 + }, + { + "x": 849, + "y": 657, + "radius": 1 + }, + { + "x": 841, + "y": 659, + "radius": 2 + }, + { + "x": 980, + "y": 659, + "radius": 1 + }, + { + "x": 1091, + "y": 657, + "radius": 1 + }, + { + "x": 1111, + "y": 658, + "radius": 2 + }, + { + "x": 1343, + "y": 659, + "radius": 2 + }, + { + "x": 933, + "y": 661, + "radius": 2 + }, + { + "x": 952, + "y": 662, + "radius": 1 + }, + { + "x": 1244, + "y": 664, + "radius": 2 + }, + { + "x": 1435, + "y": 663, + "radius": 1 + }, + { + "x": 1497, + "y": 662, + "radius": 1 + }, + { + "x": 898, + "y": 663, + "radius": 2 + }, + { + "x": 1271, + "y": 665, + "radius": 2 + }, + { + "x": 1305, + "y": 664, + "radius": 1 + }, + { + "x": 987, + "y": 667, + "radius": 2 + }, + { + "x": 1326, + "y": 669, + "radius": 2 + }, + { + "x": 1123, + "y": 671, + "radius": 1 + }, + { + "x": 1488, + "y": 674, + "radius": 2 + }, + { + "x": 815, + "y": 674, + "radius": 1 + }, + { + "x": 1412, + "y": 675, + "radius": 1 + }, + { + "x": 1343, + "y": 679, + "radius": 2 + }, + { + "x": 1358, + "y": 677, + "radius": 1 + }, + { + "x": 939, + "y": 679, + "radius": 2 + }, + { + "x": 815, + "y": 681, + "radius": 1 + }, + { + "x": 1092, + "y": 681, + "radius": 1 + }, + { + "x": 1465, + "y": 684, + "radius": 1 + }, + { + "x": 900, + "y": 683, + "radius": 1 + }, + { + "x": 1266, + "y": 687, + "radius": 2 + }, + { + "x": 1391, + "y": 684, + "radius": 1 + }, + { + "x": 1500, + "y": 684, + "radius": 2 + }, + { + "x": 818, + "y": 686, + "radius": 1 + }, + { + "x": 1010, + "y": 687, + "radius": 2 + }, + { + "x": 1137, + "y": 689, + "radius": 2 + }, + { + "x": 1165, + "y": 688, + "radius": 1 + }, + { + "x": 1442, + "y": 689, + "radius": 2 + }, + { + "x": 1431, + "y": 694, + "radius": 2 + }, + { + "x": 825, + "y": 694, + "radius": 2 + }, + { + "x": 1065, + "y": 693, + "radius": 1 + }, + { + "x": 1516, + "y": 694, + "radius": 1 + }, + { + "x": 1223, + "y": 694, + "radius": 2 + }, + { + "x": 842, + "y": 695, + "radius": 1 + }, + { + "x": 873, + "y": 697, + "radius": 1 + }, + { + "x": 1431, + "y": 697, + "radius": 1 + }, + { + "x": 920, + "y": 700, + "radius": 2 + }, + { + "x": 1026, + "y": 699, + "radius": 2 + }, + { + "x": 1367, + "y": 698, + "radius": 1 + }, + { + "x": 1283, + "y": 703, + "radius": 2 + }, + { + "x": 1504, + "y": 700, + "radius": 2 + }, + { + "x": 1298, + "y": 702, + "radius": 2 + }, + { + "x": 728, + "y": 703, + "radius": 1 + }, + { + "x": 883, + "y": 707, + "radius": 2 + }, + { + "x": 1095, + "y": 708, + "radius": 2 + }, + { + "x": 1156, + "y": 710, + "radius": 2 + }, + { + "x": 1530, + "y": 708, + "radius": 1 + }, + { + "x": 1289, + "y": 710, + "radius": 2 + }, + { + "x": 1340, + "y": 710, + "radius": 2 + }, + { + "x": 1471, + "y": 710, + "radius": 1 + }, + { + "x": 1123, + "y": 711, + "radius": 1 + }, + { + "x": 1171, + "y": 712, + "radius": 1 + }, + { + "x": 923, + "y": 714, + "radius": 1 + }, + { + "x": 1211, + "y": 715, + "radius": 1 + }, + { + "x": 1420, + "y": 716, + "radius": 2 + }, + { + "x": 845, + "y": 715, + "radius": 2 + }, + { + "x": 971, + "y": 717, + "radius": 2 + }, + { + "x": 995, + "y": 719, + "radius": 2 + }, + { + "x": 1058, + "y": 717, + "radius": 1 + }, + { + "x": 734, + "y": 721, + "radius": 2 + }, + { + "x": 934, + "y": 720, + "radius": 1 + }, + { + "x": 1304, + "y": 719, + "radius": 1 + }, + { + "x": 1445, + "y": 720, + "radius": 2 + }, + { + "x": 888, + "y": 722, + "radius": 2 + }, + { + "x": 940, + "y": 725, + "radius": 2 + }, + { + "x": 1252, + "y": 726, + "radius": 2 + }, + { + "x": 950, + "y": 728, + "radius": 2 + }, + { + "x": 1328, + "y": 727, + "radius": 1 + }, + { + "x": 1361, + "y": 727, + "radius": 1 + }, + { + "x": 1537, + "y": 728, + "radius": 2 + }, + { + "x": 1192, + "y": 731, + "radius": 1 + }, + { + "x": 1202, + "y": 731, + "radius": 2 + }, + { + "x": 980, + "y": 732, + "radius": 1 + }, + { + "x": 1052, + "y": 732, + "radius": 2 + }, + { + "x": 937, + "y": 737, + "radius": 2 + }, + { + "x": 1119, + "y": 733, + "radius": 1 + }, + { + "x": 1218, + "y": 735, + "radius": 2 + }, + { + "x": 872, + "y": 736, + "radius": 1 + }, + { + "x": 1519, + "y": 736, + "radius": 2 + }, + { + "x": 1560, + "y": 737, + "radius": 1 + }, + { + "x": 819, + "y": 744, + "radius": 2 + }, + { + "x": 1235, + "y": 741, + "radius": 2 + }, + { + "x": 1371, + "y": 743, + "radius": 2 + }, + { + "x": 1109, + "y": 744, + "radius": 2 + }, + { + "x": 812, + "y": 748, + "radius": 1 + }, + { + "x": 1016, + "y": 748, + "radius": 2 + }, + { + "x": 1166, + "y": 746, + "radius": 2 + }, + { + "x": 1570, + "y": 746, + "radius": 1 + }, + { + "x": 1118, + "y": 748, + "radius": 1 + }, + { + "x": 1339, + "y": 748, + "radius": 2 + }, + { + "x": 876, + "y": 750, + "radius": 2 + }, + { + "x": 967, + "y": 750, + "radius": 1 + }, + { + "x": 1152, + "y": 750, + "radius": 1 + }, + { + "x": 839, + "y": 754, + "radius": 2 + }, + { + "x": 1504, + "y": 753, + "radius": 2 + }, + { + "x": 1534, + "y": 751, + "radius": 2 + }, + { + "x": 1227, + "y": 755, + "radius": 1 + }, + { + "x": 1275, + "y": 754, + "radius": 2 + }, + { + "x": 1460, + "y": 755, + "radius": 1 + }, + { + "x": 950, + "y": 758, + "radius": 2 + }, + { + "x": 1558, + "y": 757, + "radius": 1 + }, + { + "x": 1051, + "y": 758, + "radius": 2 + }, + { + "x": 1241, + "y": 757, + "radius": 1 + }, + { + "x": 806, + "y": 762, + "radius": 2 + }, + { + "x": 1085, + "y": 764, + "radius": 2 + }, + { + "x": 1105, + "y": 763, + "radius": 2 + }, + { + "x": 1295, + "y": 762, + "radius": 2 + }, + { + "x": 1380, + "y": 763, + "radius": 1 + }, + { + "x": 1385, + "y": 764, + "radius": 2 + }, + { + "x": 1225, + "y": 766, + "radius": 2 + }, + { + "x": 1093, + "y": 765, + "radius": 1 + }, + { + "x": 1158, + "y": 766, + "radius": 2 + }, + { + "x": 1135, + "y": 768, + "radius": 1 + }, + { + "x": 1237, + "y": 769, + "radius": 2 + }, + { + "x": 1435, + "y": 767, + "radius": 1 + }, + { + "x": 899, + "y": 770, + "radius": 2 + }, + { + "x": 906, + "y": 772, + "radius": 2 + }, + { + "x": 959, + "y": 772, + "radius": 2 + }, + { + "x": 1053, + "y": 770, + "radius": 2 + }, + { + "x": 1189, + "y": 771, + "radius": 2 + }, + { + "x": 1261, + "y": 770, + "radius": 2 + }, + { + "x": 669, + "y": 774, + "radius": 2 + }, + { + "x": 835, + "y": 775, + "radius": 2 + }, + { + "x": 1199, + "y": 772, + "radius": 1 + }, + { + "x": 1210, + "y": 772, + "radius": 2 + }, + { + "x": 1605, + "y": 772, + "radius": 2 + }, + { + "x": 1068, + "y": 773, + "radius": 1 + }, + { + "x": 1126, + "y": 776, + "radius": 1 + }, + { + "x": 1142, + "y": 777, + "radius": 1 + }, + { + "x": 1368, + "y": 776, + "radius": 1 + }, + { + "x": 1620, + "y": 778, + "radius": 2 + }, + { + "x": 1643, + "y": 778, + "radius": 2 + }, + { + "x": 1008, + "y": 778, + "radius": 2 + }, + { + "x": 1171, + "y": 778, + "radius": 1 + }, + { + "x": 1395, + "y": 778, + "radius": 2 + }, + { + "x": 804, + "y": 780, + "radius": 2 + }, + { + "x": 1271, + "y": 781, + "radius": 2 + }, + { + "x": 1278, + "y": 782, + "radius": 2 + }, + { + "x": 1467, + "y": 783, + "radius": 2 + }, + { + "x": 1068, + "y": 784, + "radius": 1 + }, + { + "x": 1095, + "y": 785, + "radius": 2 + }, + { + "x": 1132, + "y": 784, + "radius": 1 + }, + { + "x": 1356, + "y": 784, + "radius": 1 + }, + { + "x": 816, + "y": 787, + "radius": 2 + }, + { + "x": 1207, + "y": 787, + "radius": 2 + }, + { + "x": 1223, + "y": 787, + "radius": 2 + }, + { + "x": 1403, + "y": 785, + "radius": 2 + }, + { + "x": 1651, + "y": 786, + "radius": 1 + }, + { + "x": 937, + "y": 790, + "radius": 2 + }, + { + "x": 1337, + "y": 789, + "radius": 2 + }, + { + "x": 789, + "y": 790, + "radius": 2 + }, + { + "x": 1323, + "y": 791, + "radius": 1 + }, + { + "x": 1354, + "y": 790, + "radius": 1 + }, + { + "x": 732, + "y": 792, + "radius": 1 + }, + { + "x": 972, + "y": 794, + "radius": 1 + }, + { + "x": 1221, + "y": 795, + "radius": 2 + }, + { + "x": 1244, + "y": 792, + "radius": 2 + }, + { + "x": 947, + "y": 795, + "radius": 2 + }, + { + "x": 1141, + "y": 795, + "radius": 2 + }, + { + "x": 853, + "y": 796, + "radius": 1 + }, + { + "x": 961, + "y": 795, + "radius": 1 + }, + { + "x": 1113, + "y": 796, + "radius": 2 + }, + { + "x": 1273, + "y": 796, + "radius": 2 + }, + { + "x": 1573, + "y": 797, + "radius": 2 + }, + { + "x": 872, + "y": 797, + "radius": 2 + }, + { + "x": 1174, + "y": 798, + "radius": 1 + }, + { + "x": 1557, + "y": 800, + "radius": 2 + }, + { + "x": 1127, + "y": 800, + "radius": 2 + }, + { + "x": 1363, + "y": 801, + "radius": 2 + }, + { + "x": 1376, + "y": 800, + "radius": 1 + }, + { + "x": 975, + "y": 801, + "radius": 1 + }, + { + "x": 1286, + "y": 803, + "radius": 2 + }, + { + "x": 1409, + "y": 802, + "radius": 1 + }, + { + "x": 1496, + "y": 802, + "radius": 1 + }, + { + "x": 795, + "y": 804, + "radius": 1 + }, + { + "x": 801, + "y": 806, + "radius": 2 + }, + { + "x": 947, + "y": 803, + "radius": 1 + }, + { + "x": 1187, + "y": 805, + "radius": 1 + }, + { + "x": 569, + "y": 806, + "radius": 1 + }, + { + "x": 693, + "y": 807, + "radius": 2 + }, + { + "x": 817, + "y": 807, + "radius": 2 + }, + { + "x": 1044, + "y": 807, + "radius": 1 + }, + { + "x": 1307, + "y": 806, + "radius": 2 + }, + { + "x": 1245, + "y": 808, + "radius": 1 + }, + { + "x": 1388, + "y": 809, + "radius": 2 + }, + { + "x": 1557, + "y": 807, + "radius": 1 + }, + { + "x": 690, + "y": 811, + "radius": 1 + }, + { + "x": 832, + "y": 810, + "radius": 2 + }, + { + "x": 854, + "y": 811, + "radius": 2 + }, + { + "x": 1503, + "y": 814, + "radius": 2 + }, + { + "x": 1520, + "y": 810, + "radius": 1 + }, + { + "x": 968, + "y": 813, + "radius": 2 + }, + { + "x": 1329, + "y": 816, + "radius": 2 + }, + { + "x": 1442, + "y": 814, + "radius": 2 + }, + { + "x": 833, + "y": 816, + "radius": 1 + }, + { + "x": 848, + "y": 817, + "radius": 1 + }, + { + "x": 917, + "y": 817, + "radius": 1 + }, + { + "x": 1136, + "y": 817, + "radius": 2 + }, + { + "x": 1273, + "y": 815, + "radius": 1 + }, + { + "x": 1301, + "y": 819, + "radius": 2 + }, + { + "x": 1394, + "y": 816, + "radius": 1 + }, + { + "x": 1018, + "y": 817, + "radius": 1 + }, + { + "x": 1194, + "y": 819, + "radius": 2 + }, + { + "x": 1530, + "y": 817, + "radius": 2 + }, + { + "x": 1569, + "y": 818, + "radius": 1 + }, + { + "x": 1484, + "y": 821, + "radius": 2 + }, + { + "x": 723, + "y": 824, + "radius": 1 + }, + { + "x": 796, + "y": 824, + "radius": 2 + }, + { + "x": 1121, + "y": 825, + "radius": 2 + }, + { + "x": 1576, + "y": 822, + "radius": 2 + }, + { + "x": 1389, + "y": 825, + "radius": 1 + }, + { + "x": 998, + "y": 827, + "radius": 1 + }, + { + "x": 1067, + "y": 825, + "radius": 1 + }, + { + "x": 1165, + "y": 828, + "radius": 2 + }, + { + "x": 1315, + "y": 828, + "radius": 1 + }, + { + "x": 1343, + "y": 827, + "radius": 2 + }, + { + "x": 1421, + "y": 828, + "radius": 2 + }, + { + "x": 1515, + "y": 825, + "radius": 2 + }, + { + "x": 899, + "y": 828, + "radius": 2 + }, + { + "x": 991, + "y": 827, + "radius": 1 + }, + { + "x": 1470, + "y": 829, + "radius": 2 + }, + { + "x": 1577, + "y": 828, + "radius": 1 + }, + { + "x": 890, + "y": 833, + "radius": 2 + }, + { + "x": 1230, + "y": 830, + "radius": 2 + }, + { + "x": 1450, + "y": 831, + "radius": 2 + }, + { + "x": 1644, + "y": 832, + "radius": 2 + }, + { + "x": 747, + "y": 833, + "radius": 2 + }, + { + "x": 940, + "y": 832, + "radius": 1 + }, + { + "x": 1302, + "y": 831, + "radius": 2 + }, + { + "x": 908, + "y": 835, + "radius": 1 + }, + { + "x": 1268, + "y": 834, + "radius": 1 + }, + { + "x": 1498, + "y": 833, + "radius": 2 + }, + { + "x": 826, + "y": 835, + "radius": 1 + }, + { + "x": 1108, + "y": 839, + "radius": 2 + }, + { + "x": 1194, + "y": 836, + "radius": 1 + }, + { + "x": 1210, + "y": 837, + "radius": 2 + }, + { + "x": 1418, + "y": 839, + "radius": 2 + }, + { + "x": 654, + "y": 840, + "radius": 2 + }, + { + "x": 896, + "y": 840, + "radius": 1 + }, + { + "x": 1009, + "y": 838, + "radius": 1 + }, + { + "x": 1280, + "y": 839, + "radius": 2 + }, + { + "x": 1357, + "y": 837, + "radius": 1 + }, + { + "x": 1477, + "y": 838, + "radius": 1 + }, + { + "x": 664, + "y": 842, + "radius": 2 + }, + { + "x": 681, + "y": 844, + "radius": 2 + }, + { + "x": 1155, + "y": 842, + "radius": 1 + }, + { + "x": 1252, + "y": 842, + "radius": 1 + }, + { + "x": 1331, + "y": 842, + "radius": 2 + }, + { + "x": 1486, + "y": 845, + "radius": 2 + }, + { + "x": 1537, + "y": 845, + "radius": 2 + }, + { + "x": 1550, + "y": 842, + "radius": 2 + }, + { + "x": 761, + "y": 847, + "radius": 2 + }, + { + "x": 1167, + "y": 844, + "radius": 1 + }, + { + "x": 694, + "y": 849, + "radius": 2 + }, + { + "x": 734, + "y": 847, + "radius": 1 + }, + { + "x": 886, + "y": 849, + "radius": 1 + }, + { + "x": 892, + "y": 848, + "radius": 1 + }, + { + "x": 1014, + "y": 848, + "radius": 1 + }, + { + "x": 1584, + "y": 848, + "radius": 1 + }, + { + "x": 1035, + "y": 852, + "radius": 2 + }, + { + "x": 1116, + "y": 850, + "radius": 2 + }, + { + "x": 1134, + "y": 850, + "radius": 2 + }, + { + "x": 1228, + "y": 850, + "radius": 2 + }, + { + "x": 1275, + "y": 852, + "radius": 2 + }, + { + "x": 1308, + "y": 850, + "radius": 1 + }, + { + "x": 804, + "y": 852, + "radius": 2 + }, + { + "x": 909, + "y": 854, + "radius": 2 + }, + { + "x": 921, + "y": 852, + "radius": 1 + }, + { + "x": 1058, + "y": 853, + "radius": 2 + }, + { + "x": 1430, + "y": 853, + "radius": 1 + }, + { + "x": 1598, + "y": 852, + "radius": 1 + }, + { + "x": 788, + "y": 855, + "radius": 2 + }, + { + "x": 849, + "y": 856, + "radius": 2 + }, + { + "x": 1028, + "y": 854, + "radius": 1 + }, + { + "x": 1084, + "y": 855, + "radius": 1 + }, + { + "x": 710, + "y": 855, + "radius": 1 + }, + { + "x": 719, + "y": 856, + "radius": 2 + }, + { + "x": 1011, + "y": 856, + "radius": 1 + }, + { + "x": 1096, + "y": 855, + "radius": 2 + }, + { + "x": 1166, + "y": 859, + "radius": 2 + }, + { + "x": 1219, + "y": 857, + "radius": 2 + }, + { + "x": 1633, + "y": 860, + "radius": 2 + }, + { + "x": 1332, + "y": 861, + "radius": 2 + }, + { + "x": 1648, + "y": 861, + "radius": 1 + }, + { + "x": 777, + "y": 862, + "radius": 1 + }, + { + "x": 957, + "y": 862, + "radius": 2 + }, + { + "x": 1147, + "y": 863, + "radius": 2 + }, + { + "x": 1666, + "y": 864, + "radius": 2 + }, + { + "x": 727, + "y": 863, + "radius": 1 + }, + { + "x": 1279, + "y": 865, + "radius": 1 + }, + { + "x": 1573, + "y": 868, + "radius": 2 + }, + { + "x": 676, + "y": 870, + "radius": 2 + }, + { + "x": 983, + "y": 866, + "radius": 2 + }, + { + "x": 978, + "y": 868, + "radius": 1 + }, + { + "x": 1167, + "y": 869, + "radius": 2 + }, + { + "x": 1395, + "y": 867, + "radius": 2 + }, + { + "x": 1649, + "y": 869, + "radius": 1 + }, + { + "x": 722, + "y": 871, + "radius": 2 + }, + { + "x": 807, + "y": 870, + "radius": 2 + }, + { + "x": 1456, + "y": 870, + "radius": 2 + }, + { + "x": 1541, + "y": 871, + "radius": 1 + }, + { + "x": 734, + "y": 873, + "radius": 1 + }, + { + "x": 775, + "y": 871, + "radius": 1 + }, + { + "x": 928, + "y": 871, + "radius": 2 + }, + { + "x": 1235, + "y": 873, + "radius": 2 + }, + { + "x": 1208, + "y": 874, + "radius": 2 + }, + { + "x": 1284, + "y": 876, + "radius": 2 + }, + { + "x": 1217, + "y": 877, + "radius": 2 + }, + { + "x": 1297, + "y": 877, + "radius": 2 + }, + { + "x": 1410, + "y": 875, + "radius": 2 + }, + { + "x": 1604, + "y": 880, + "radius": 2 + }, + { + "x": 849, + "y": 879, + "radius": 2 + }, + { + "x": 919, + "y": 881, + "radius": 2 + }, + { + "x": 1490, + "y": 879, + "radius": 1 + }, + { + "x": 1068, + "y": 879, + "radius": 2 + }, + { + "x": 1096, + "y": 882, + "radius": 2 + }, + { + "x": 1339, + "y": 881, + "radius": 2 + }, + { + "x": 766, + "y": 882, + "radius": 2 + }, + { + "x": 1168, + "y": 883, + "radius": 2 + }, + { + "x": 1590, + "y": 884, + "radius": 2 + }, + { + "x": 639, + "y": 885, + "radius": 1 + }, + { + "x": 843, + "y": 883, + "radius": 1 + }, + { + "x": 1289, + "y": 885, + "radius": 2 + }, + { + "x": 1353, + "y": 886, + "radius": 2 + }, + { + "x": 1536, + "y": 885, + "radius": 1 + }, + { + "x": 690, + "y": 885, + "radius": 1 + }, + { + "x": 1369, + "y": 885, + "radius": 2 + }, + { + "x": 1555, + "y": 886, + "radius": 1 + }, + { + "x": 1650, + "y": 886, + "radius": 2 + }, + { + "x": 988, + "y": 887, + "radius": 2 + }, + { + "x": 1001, + "y": 887, + "radius": 1 + }, + { + "x": 1227, + "y": 889, + "radius": 2 + }, + { + "x": 1487, + "y": 889, + "radius": 2 + }, + { + "x": 1513, + "y": 887, + "radius": 1 + }, + { + "x": 675, + "y": 889, + "radius": 2 + }, + { + "x": 726, + "y": 889, + "radius": 2 + }, + { + "x": 703, + "y": 892, + "radius": 2 + }, + { + "x": 775, + "y": 893, + "radius": 1 + }, + { + "x": 1058, + "y": 892, + "radius": 2 + }, + { + "x": 1474, + "y": 892, + "radius": 1 + }, + { + "x": 1639, + "y": 892, + "radius": 2 + }, + { + "x": 824, + "y": 896, + "radius": 2 + }, + { + "x": 1369, + "y": 894, + "radius": 2 + }, + { + "x": 1483, + "y": 894, + "radius": 1 + }, + { + "x": 1543, + "y": 893, + "radius": 2 + }, + { + "x": 1684, + "y": 895, + "radius": 2 + }, + { + "x": 656, + "y": 899, + "radius": 2 + }, + { + "x": 927, + "y": 896, + "radius": 1 + }, + { + "x": 1482, + "y": 899, + "radius": 2 + }, + { + "x": 813, + "y": 902, + "radius": 2 + }, + { + "x": 1394, + "y": 901, + "radius": 2 + }, + { + "x": 1472, + "y": 904, + "radius": 2 + }, + { + "x": 1497, + "y": 900, + "radius": 2 + }, + { + "x": 697, + "y": 903, + "radius": 1 + }, + { + "x": 1086, + "y": 902, + "radius": 1 + }, + { + "x": 1107, + "y": 903, + "radius": 2 + }, + { + "x": 1156, + "y": 901, + "radius": 1 + }, + { + "x": 1056, + "y": 904, + "radius": 2 + }, + { + "x": 1238, + "y": 905, + "radius": 1 + }, + { + "x": 741, + "y": 910, + "radius": 2 + }, + { + "x": 1188, + "y": 907, + "radius": 2 + }, + { + "x": 1340, + "y": 906, + "radius": 2 + }, + { + "x": 1555, + "y": 906, + "radius": 1 + }, + { + "x": 1666, + "y": 907, + "radius": 2 + }, + { + "x": 1691, + "y": 908, + "radius": 2 + }, + { + "x": 663, + "y": 910, + "radius": 2 + }, + { + "x": 1529, + "y": 909, + "radius": 1 + }, + { + "x": 693, + "y": 911, + "radius": 2 + }, + { + "x": 723, + "y": 910, + "radius": 1 + }, + { + "x": 838, + "y": 910, + "radius": 1 + }, + { + "x": 972, + "y": 909, + "radius": 1 + }, + { + "x": 1036, + "y": 910, + "radius": 2 + }, + { + "x": 1142, + "y": 910, + "radius": 1 + }, + { + "x": 1384, + "y": 911, + "radius": 1 + }, + { + "x": 1455, + "y": 911, + "radius": 2 + }, + { + "x": 1496, + "y": 910, + "radius": 2 + }, + { + "x": 1122, + "y": 915, + "radius": 2 + }, + { + "x": 1243, + "y": 913, + "radius": 2 + }, + { + "x": 1053, + "y": 914, + "radius": 1 + }, + { + "x": 1517, + "y": 915, + "radius": 1 + }, + { + "x": 1631, + "y": 913, + "radius": 1 + }, + { + "x": 975, + "y": 916, + "radius": 2 + }, + { + "x": 1155, + "y": 916, + "radius": 2 + }, + { + "x": 1300, + "y": 916, + "radius": 1 + }, + { + "x": 1445, + "y": 915, + "radius": 2 + }, + { + "x": 1561, + "y": 917, + "radius": 1 + }, + { + "x": 1656, + "y": 919, + "radius": 2 + }, + { + "x": 718, + "y": 919, + "radius": 2 + }, + { + "x": 900, + "y": 919, + "radius": 2 + }, + { + "x": 1211, + "y": 920, + "radius": 2 + }, + { + "x": 1265, + "y": 921, + "radius": 2 + }, + { + "x": 1277, + "y": 919, + "radius": 1 + }, + { + "x": 1312, + "y": 920, + "radius": 2 + }, + { + "x": 1502, + "y": 923, + "radius": 2 + }, + { + "x": 1681, + "y": 920, + "radius": 2 + }, + { + "x": 1423, + "y": 921, + "radius": 2 + }, + { + "x": 658, + "y": 924, + "radius": 2 + }, + { + "x": 952, + "y": 921, + "radius": 2 + }, + { + "x": 1089, + "y": 921, + "radius": 2 + }, + { + "x": 1438, + "y": 924, + "radius": 2 + }, + { + "x": 1574, + "y": 924, + "radius": 1 + }, + { + "x": 726, + "y": 924, + "radius": 1 + }, + { + "x": 1456, + "y": 924, + "radius": 2 + }, + { + "x": 1542, + "y": 925, + "radius": 2 + }, + { + "x": 773, + "y": 929, + "radius": 2 + }, + { + "x": 1061, + "y": 926, + "radius": 2 + }, + { + "x": 747, + "y": 928, + "radius": 1 + }, + { + "x": 895, + "y": 932, + "radius": 2 + }, + { + "x": 1155, + "y": 929, + "radius": 2 + }, + { + "x": 1373, + "y": 929, + "radius": 2 + }, + { + "x": 1527, + "y": 927, + "radius": 2 + }, + { + "x": 1661, + "y": 929, + "radius": 2 + }, + { + "x": 1022, + "y": 930, + "radius": 2 + }, + { + "x": 1113, + "y": 931, + "radius": 1 + }, + { + "x": 1172, + "y": 930, + "radius": 1 + }, + { + "x": 1401, + "y": 929, + "radius": 1 + }, + { + "x": 1676, + "y": 930, + "radius": 1 + }, + { + "x": 789, + "y": 933, + "radius": 2 + }, + { + "x": 840, + "y": 931, + "radius": 1 + }, + { + "x": 1183, + "y": 934, + "radius": 2 + }, + { + "x": 1311, + "y": 932, + "radius": 2 + }, + { + "x": 1423, + "y": 932, + "radius": 2 + }, + { + "x": 1504, + "y": 931, + "radius": 1 + }, + { + "x": 667, + "y": 934, + "radius": 2 + }, + { + "x": 700, + "y": 937, + "radius": 2 + }, + { + "x": 816, + "y": 933, + "radius": 2 + }, + { + "x": 1049, + "y": 934, + "radius": 2 + }, + { + "x": 1209, + "y": 935, + "radius": 1 + }, + { + "x": 1594, + "y": 934, + "radius": 1 + }, + { + "x": 1611, + "y": 935, + "radius": 1 + }, + { + "x": 604, + "y": 936, + "radius": 2 + }, + { + "x": 1530, + "y": 937, + "radius": 2 + }, + { + "x": 1688, + "y": 937, + "radius": 2 + }, + { + "x": 768, + "y": 937, + "radius": 1 + }, + { + "x": 928, + "y": 938, + "radius": 1 + }, + { + "x": 958, + "y": 937, + "radius": 1 + }, + { + "x": 967, + "y": 939, + "radius": 2 + }, + { + "x": 979, + "y": 937, + "radius": 1 + }, + { + "x": 1124, + "y": 938, + "radius": 2 + }, + { + "x": 1327, + "y": 937, + "radius": 1 + }, + { + "x": 1574, + "y": 937, + "radius": 1 + }, + { + "x": 781, + "y": 942, + "radius": 2 + }, + { + "x": 885, + "y": 941, + "radius": 2 + }, + { + "x": 1391, + "y": 941, + "radius": 1 + }, + { + "x": 1439, + "y": 943, + "radius": 2 + }, + { + "x": 1660, + "y": 944, + "radius": 2 + }, + { + "x": 996, + "y": 948, + "radius": 2 + }, + { + "x": 1457, + "y": 945, + "radius": 2 + }, + { + "x": 1630, + "y": 945, + "radius": 2 + }, + { + "x": 859, + "y": 948, + "radius": 2 + }, + { + "x": 1489, + "y": 947, + "radius": 1 + }, + { + "x": 1497, + "y": 945, + "radius": 1 + }, + { + "x": 875, + "y": 949, + "radius": 1 + }, + { + "x": 921, + "y": 948, + "radius": 1 + }, + { + "x": 953, + "y": 948, + "radius": 2 + }, + { + "x": 1144, + "y": 947, + "radius": 1 + }, + { + "x": 1515, + "y": 949, + "radius": 2 + }, + { + "x": 685, + "y": 952, + "radius": 1 + }, + { + "x": 691, + "y": 952, + "radius": 2 + }, + { + "x": 1027, + "y": 951, + "radius": 2 + }, + { + "x": 1183, + "y": 951, + "radius": 2 + }, + { + "x": 1245, + "y": 951, + "radius": 1 + }, + { + "x": 1312, + "y": 950, + "radius": 2 + }, + { + "x": 1382, + "y": 950, + "radius": 2 + }, + { + "x": 1628, + "y": 950, + "radius": 2 + }, + { + "x": 894, + "y": 952, + "radius": 1 + }, + { + "x": 1067, + "y": 956, + "radius": 2 + }, + { + "x": 1132, + "y": 953, + "radius": 2 + }, + { + "x": 1365, + "y": 952, + "radius": 1 + }, + { + "x": 1436, + "y": 951, + "radius": 2 + }, + { + "x": 700, + "y": 953, + "radius": 1 + }, + { + "x": 818, + "y": 957, + "radius": 1 + }, + { + "x": 1211, + "y": 956, + "radius": 2 + }, + { + "x": 755, + "y": 958, + "radius": 2 + }, + { + "x": 1087, + "y": 959, + "radius": 2 + }, + { + "x": 1150, + "y": 958, + "radius": 2 + }, + { + "x": 1373, + "y": 959, + "radius": 1 + }, + { + "x": 711, + "y": 961, + "radius": 2 + }, + { + "x": 732, + "y": 960, + "radius": 2 + }, + { + "x": 765, + "y": 961, + "radius": 1 + }, + { + "x": 790, + "y": 960, + "radius": 1 + }, + { + "x": 1099, + "y": 963, + "radius": 2 + }, + { + "x": 1353, + "y": 961, + "radius": 2 + }, + { + "x": 833, + "y": 964, + "radius": 2 + }, + { + "x": 905, + "y": 961, + "radius": 2 + }, + { + "x": 958, + "y": 961, + "radius": 1 + }, + { + "x": 978, + "y": 963, + "radius": 2 + }, + { + "x": 1526, + "y": 962, + "radius": 1 + }, + { + "x": 1308, + "y": 964, + "radius": 2 + }, + { + "x": 1515, + "y": 964, + "radius": 2 + }, + { + "x": 1547, + "y": 964, + "radius": 2 + }, + { + "x": 1622, + "y": 966, + "radius": 2 + }, + { + "x": 715, + "y": 968, + "radius": 1 + }, + { + "x": 1026, + "y": 969, + "radius": 2 + }, + { + "x": 1533, + "y": 969, + "radius": 2 + }, + { + "x": 1707, + "y": 970, + "radius": 2 + }, + { + "x": 1089, + "y": 971, + "radius": 2 + }, + { + "x": 1139, + "y": 972, + "radius": 2 + }, + { + "x": 1218, + "y": 970, + "radius": 2 + }, + { + "x": 1527, + "y": 970, + "radius": 1 + }, + { + "x": 1584, + "y": 972, + "radius": 2 + }, + { + "x": 1005, + "y": 973, + "radius": 1 + }, + { + "x": 1209, + "y": 974, + "radius": 2 + }, + { + "x": 1267, + "y": 971, + "radius": 1 + }, + { + "x": 1375, + "y": 974, + "radius": 2 + }, + { + "x": 1387, + "y": 972, + "radius": 2 + }, + { + "x": 1464, + "y": 972, + "radius": 2 + }, + { + "x": 1489, + "y": 971, + "radius": 2 + }, + { + "x": 1555, + "y": 974, + "radius": 1 + }, + { + "x": 856, + "y": 975, + "radius": 2 + }, + { + "x": 962, + "y": 977, + "radius": 2 + }, + { + "x": 1290, + "y": 977, + "radius": 2 + }, + { + "x": 1433, + "y": 976, + "radius": 2 + }, + { + "x": 1400, + "y": 977, + "radius": 1 + }, + { + "x": 1593, + "y": 978, + "radius": 2 + }, + { + "x": 850, + "y": 980, + "radius": 1 + }, + { + "x": 1087, + "y": 979, + "radius": 2 + }, + { + "x": 1111, + "y": 979, + "radius": 1 + }, + { + "x": 1448, + "y": 981, + "radius": 2 + }, + { + "x": 1604, + "y": 980, + "radius": 1 + }, + { + "x": 1610, + "y": 979, + "radius": 1 + }, + { + "x": 1639, + "y": 982, + "radius": 2 + }, + { + "x": 932, + "y": 982, + "radius": 2 + }, + { + "x": 1633, + "y": 984, + "radius": 1 + }, + { + "x": 816, + "y": 985, + "radius": 1 + }, + { + "x": 826, + "y": 988, + "radius": 2 + }, + { + "x": 1241, + "y": 988, + "radius": 2 + }, + { + "x": 1525, + "y": 987, + "radius": 1 + }, + { + "x": 1669, + "y": 986, + "radius": 1 + }, + { + "x": 665, + "y": 988, + "radius": 1 + }, + { + "x": 686, + "y": 990, + "radius": 2 + }, + { + "x": 958, + "y": 991, + "radius": 2 + }, + { + "x": 1037, + "y": 991, + "radius": 2 + }, + { + "x": 1064, + "y": 991, + "radius": 2 + }, + { + "x": 1607, + "y": 992, + "radius": 2 + }, + { + "x": 1660, + "y": 990, + "radius": 1 + }, + { + "x": 650, + "y": 993, + "radius": 1 + }, + { + "x": 717, + "y": 993, + "radius": 2 + }, + { + "x": 763, + "y": 994, + "radius": 2 + }, + { + "x": 878, + "y": 993, + "radius": 2 + }, + { + "x": 993, + "y": 996, + "radius": 2 + }, + { + "x": 1196, + "y": 992, + "radius": 2 + }, + { + "x": 1302, + "y": 991, + "radius": 2 + }, + { + "x": 1649, + "y": 995, + "radius": 2 + }, + { + "x": 1006, + "y": 995, + "radius": 2 + }, + { + "x": 1134, + "y": 994, + "radius": 2 + }, + { + "x": 1160, + "y": 994, + "radius": 1 + }, + { + "x": 940, + "y": 997, + "radius": 2 + }, + { + "x": 972, + "y": 997, + "radius": 2 + }, + { + "x": 1207, + "y": 996, + "radius": 2 + }, + { + "x": 1359, + "y": 996, + "radius": 1 + }, + { + "x": 770, + "y": 999, + "radius": 2 + }, + { + "x": 845, + "y": 998, + "radius": 1 + }, + { + "x": 1019, + "y": 1001, + "radius": 2 + }, + { + "x": 858, + "y": 1001, + "radius": 2 + }, + { + "x": 987, + "y": 1003, + "radius": 2 + }, + { + "x": 1220, + "y": 1002, + "radius": 2 + }, + { + "x": 1281, + "y": 1004, + "radius": 2 + }, + { + "x": 787, + "y": 1008, + "radius": 2 + }, + { + "x": 932, + "y": 1003, + "radius": 1 + }, + { + "x": 1032, + "y": 1004, + "radius": 2 + }, + { + "x": 1130, + "y": 1005, + "radius": 1 + }, + { + "x": 1488, + "y": 1004, + "radius": 2 + }, + { + "x": 1655, + "y": 1004, + "radius": 1 + }, + { + "x": 667, + "y": 1008, + "radius": 2 + }, + { + "x": 1068, + "y": 1007, + "radius": 1 + }, + { + "x": 1164, + "y": 1009, + "radius": 2 + }, + { + "x": 1189, + "y": 1007, + "radius": 2 + }, + { + "x": 1288, + "y": 1008, + "radius": 2 + }, + { + "x": 1595, + "y": 1007, + "radius": 2 + }, + { + "x": 1083, + "y": 1008, + "radius": 1 + }, + { + "x": 1235, + "y": 1008, + "radius": 2 + }, + { + "x": 1380, + "y": 1009, + "radius": 2 + }, + { + "x": 1448, + "y": 1009, + "radius": 2 + }, + { + "x": 758, + "y": 1010, + "radius": 1 + }, + { + "x": 1154, + "y": 1010, + "radius": 2 + }, + { + "x": 1199, + "y": 1009, + "radius": 1 + }, + { + "x": 778, + "y": 1012, + "radius": 2 + }, + { + "x": 1011, + "y": 1014, + "radius": 2 + }, + { + "x": 1434, + "y": 1013, + "radius": 1 + }, + { + "x": 1552, + "y": 1013, + "radius": 1 + }, + { + "x": 649, + "y": 1014, + "radius": 1 + }, + { + "x": 967, + "y": 1014, + "radius": 1 + }, + { + "x": 1132, + "y": 1014, + "radius": 2 + }, + { + "x": 931, + "y": 1015, + "radius": 2 + }, + { + "x": 1043, + "y": 1017, + "radius": 1 + }, + { + "x": 1222, + "y": 1016, + "radius": 2 + }, + { + "x": 1487, + "y": 1016, + "radius": 1 + }, + { + "x": 717, + "y": 1018, + "radius": 1 + }, + { + "x": 1185, + "y": 1017, + "radius": 1 + }, + { + "x": 1507, + "y": 1017, + "radius": 2 + }, + { + "x": 828, + "y": 1019, + "radius": 2 + }, + { + "x": 1007, + "y": 1021, + "radius": 2 + }, + { + "x": 1149, + "y": 1024, + "radius": 2 + }, + { + "x": 1516, + "y": 1021, + "radius": 2 + }, + { + "x": 1084, + "y": 1024, + "radius": 2 + }, + { + "x": 1244, + "y": 1025, + "radius": 2 + }, + { + "x": 1659, + "y": 1024, + "radius": 1 + }, + { + "x": 900, + "y": 1026, + "radius": 2 + }, + { + "x": 1185, + "y": 1027, + "radius": 1 + }, + { + "x": 1438, + "y": 1027, + "radius": 2 + }, + { + "x": 1119, + "y": 1027, + "radius": 1 + }, + { + "x": 1228, + "y": 1029, + "radius": 2 + }, + { + "x": 1273, + "y": 1029, + "radius": 1 + }, + { + "x": 1286, + "y": 1028, + "radius": 1 + }, + { + "x": 698, + "y": 1031, + "radius": 2 + }, + { + "x": 847, + "y": 1030, + "radius": 1 + }, + { + "x": 1413, + "y": 1031, + "radius": 2 + }, + { + "x": 1559, + "y": 1029, + "radius": 2 + }, + { + "x": 1652, + "y": 1033, + "radius": 2 + }, + { + "x": 1142, + "y": 1034, + "radius": 2 + }, + { + "x": 1250, + "y": 1033, + "radius": 2 + }, + { + "x": 1464, + "y": 1032, + "radius": 1 + }, + { + "x": 1524, + "y": 1032, + "radius": 1 + }, + { + "x": 1032, + "y": 1035, + "radius": 2 + }, + { + "x": 1583, + "y": 1034, + "radius": 2 + }, + { + "x": 971, + "y": 1037, + "radius": 1 + }, + { + "x": 1010, + "y": 1036, + "radius": 2 + }, + { + "x": 1218, + "y": 1039, + "radius": 2 + }, + { + "x": 1277, + "y": 1037, + "radius": 1 + }, + { + "x": 1400, + "y": 1035, + "radius": 1 + }, + { + "x": 731, + "y": 1039, + "radius": 2 + }, + { + "x": 1058, + "y": 1038, + "radius": 2 + }, + { + "x": 1114, + "y": 1038, + "radius": 2 + }, + { + "x": 1209, + "y": 1039, + "radius": 2 + }, + { + "x": 1347, + "y": 1039, + "radius": 2 + }, + { + "x": 1435, + "y": 1038, + "radius": 1 + }, + { + "x": 1447, + "y": 1038, + "radius": 1 + }, + { + "x": 1499, + "y": 1038, + "radius": 2 + }, + { + "x": 1606, + "y": 1039, + "radius": 1 + }, + { + "x": 791, + "y": 1040, + "radius": 1 + }, + { + "x": 836, + "y": 1040, + "radius": 2 + }, + { + "x": 846, + "y": 1040, + "radius": 2 + }, + { + "x": 1190, + "y": 1040, + "radius": 2 + }, + { + "x": 708, + "y": 1043, + "radius": 1 + }, + { + "x": 778, + "y": 1043, + "radius": 1 + }, + { + "x": 1050, + "y": 1043, + "radius": 2 + }, + { + "x": 815, + "y": 1046, + "radius": 2 + }, + { + "x": 857, + "y": 1045, + "radius": 2 + }, + { + "x": 877, + "y": 1045, + "radius": 1 + }, + { + "x": 920, + "y": 1043, + "radius": 1 + }, + { + "x": 963, + "y": 1048, + "radius": 2 + }, + { + "x": 1270, + "y": 1048, + "radius": 1 + }, + { + "x": 1288, + "y": 1047, + "radius": 2 + }, + { + "x": 1510, + "y": 1047, + "radius": 1 + }, + { + "x": 824, + "y": 1050, + "radius": 2 + }, + { + "x": 1076, + "y": 1052, + "radius": 2 + }, + { + "x": 1254, + "y": 1050, + "radius": 2 + }, + { + "x": 709, + "y": 1054, + "radius": 2 + }, + { + "x": 918, + "y": 1053, + "radius": 1 + }, + { + "x": 1180, + "y": 1054, + "radius": 2 + }, + { + "x": 1240, + "y": 1053, + "radius": 2 + }, + { + "x": 799, + "y": 1056, + "radius": 2 + }, + { + "x": 879, + "y": 1058, + "radius": 2 + }, + { + "x": 1047, + "y": 1054, + "radius": 2 + }, + { + "x": 1072, + "y": 1058, + "radius": 1 + }, + { + "x": 1052, + "y": 1060, + "radius": 1 + }, + { + "x": 1227, + "y": 1062, + "radius": 2 + }, + { + "x": 964, + "y": 1063, + "radius": 2 + }, + { + "x": 1154, + "y": 1062, + "radius": 2 + }, + { + "x": 1024, + "y": 1066, + "radius": 2 + }, + { + "x": 1246, + "y": 1065, + "radius": 2 + }, + { + "x": 928, + "y": 1067, + "radius": 1 + }, + { + "x": 972, + "y": 1068, + "radius": 2 + }, + { + "x": 1005, + "y": 1069, + "radius": 1 + }, + { + "x": 1223, + "y": 1071, + "radius": 1 + }, + { + "x": 1257, + "y": 1071, + "radius": 1 + }, + { + "x": 1273, + "y": 1072, + "radius": 2 + }, + { + "x": 960, + "y": 1074, + "radius": 2 + }, + { + "x": 1211, + "y": 1072, + "radius": 1 + }, + { + "x": 1002, + "y": 1074, + "radius": 1 + }, + { + "x": 1083, + "y": 1073, + "radius": 2 + }, + { + "x": 1248, + "y": 1075, + "radius": 2 + }, + { + "x": 1032, + "y": 1077, + "radius": 2 + }, + { + "x": 1188, + "y": 1078, + "radius": 2 + }, + { + "x": 1265, + "y": 1080, + "radius": 2 + }, + { + "x": 972, + "y": 1082, + "radius": 2 + }, + { + "x": 1204, + "y": 1080, + "radius": 2 + }, + { + "x": 1312, + "y": 1085, + "radius": 2 + }, + { + "x": 1078, + "y": 1082, + "radius": 2 + }, + { + "x": 1275, + "y": 1083, + "radius": 2 + }, + { + "x": 933, + "y": 1086, + "radius": 1 + }, + { + "x": 1214, + "y": 1092, + "radius": 2 + }, + { + "x": 1265, + "y": 1091, + "radius": 2 + }, + { + "x": 1240, + "y": 1094, + "radius": 2 + }, + { + "x": 1284, + "y": 1098, + "radius": 2 + }, + { + "x": 1034, + "y": 1100, + "radius": 1 + }, + { + "x": 1226, + "y": 1100, + "radius": 1 + }, + { + "x": 1310, + "y": 1100, + "radius": 1 + }, + { + "x": 1000, + "y": 1101, + "radius": 1 + }, + { + "x": 1239, + "y": 1102, + "radius": 2 + }, + { + "x": 1056, + "y": 1103, + "radius": 2 + }, + { + "x": 1203, + "y": 1107, + "radius": 2 + }, + { + "x": 1281, + "y": 1109, + "radius": 2 + }, + { + "x": 1042, + "y": 1113, + "radius": 2 + }, + { + "x": 1207, + "y": 1118, + "radius": 2 + }, + { + "x": 1224, + "y": 1119, + "radius": 1 + }, + { + "x": 1291, + "y": 1119, + "radius": 1 + }, + { + "x": 1015, + "y": 1126, + "radius": 1 + }, + { + "x": 1198, + "y": 1125, + "radius": 1 + }, + { + "x": 1116, + "y": 1129, + "radius": 2 + }, + { + "x": 1174, + "y": 1132, + "radius": 2 + }, + { + "x": 1187, + "y": 1130, + "radius": 1 + }, + { + "x": 1209, + "y": 1132, + "radius": 1 + }, + { + "x": 1145, + "y": 1134, + "radius": 1 + }, + { + "x": 1243, + "y": 1134, + "radius": 2 + }, + { + "x": 1035, + "y": 1138, + "radius": 2 + }, + { + "x": 1157, + "y": 1138, + "radius": 2 + }, + { + "x": 1149, + "y": 1147, + "radius": 2 + }, + { + "x": 1245, + "y": 1145, + "radius": 1 + }, + { + "x": 1083, + "y": 1156, + "radius": 2 + }, + { + "x": 1060, + "y": 1156, + "radius": 2 + }, + { + "x": 1148, + "y": 1158, + "radius": 2 + }, + { + "x": 1219, + "y": 1156, + "radius": 1 + }, + { + "x": 1188, + "y": 1162, + "radius": 2 + }, + { + "x": 1030, + "y": 1161, + "radius": 1 + }, + { + "x": 1139, + "y": 1163, + "radius": 1 + }, + { + "x": 1197, + "y": 1163, + "radius": 2 + }, + { + "x": 1257, + "y": 1162, + "radius": 2 + }, + { + "x": 1303, + "y": 1163, + "radius": 2 + }, + { + "x": 1081, + "y": 1172, + "radius": 1 + }, + { + "x": 1225, + "y": 1173, + "radius": 1 + }, + { + "x": 1165, + "y": 1175, + "radius": 1 + }, + { + "x": 1193, + "y": 1177, + "radius": 1 + }, + { + "x": 1269, + "y": 1177, + "radius": 2 + }, + { + "x": 1168, + "y": 1181, + "radius": 1 + }, + { + "x": 1162, + "y": 1184, + "radius": 2 + }, + { + "x": 1230, + "y": 1186, + "radius": 2 + }, + { + "x": 1108, + "y": 1187, + "radius": 2 + }, + { + "x": 1191, + "y": 1191, + "radius": 1 + }, + { + "x": 1013, + "y": 1192, + "radius": 2 + }, + { + "x": 1128, + "y": 1195, + "radius": 2 + }, + { + "x": 1072, + "y": 1206, + "radius": 2 + }, + { + "x": 1035, + "y": 1211, + "radius": 1 + }, + { + "x": 1202, + "y": 1210, + "radius": 1 + }, + { + "x": 1101, + "y": 1215, + "radius": 2 + }, + { + "x": 1119, + "y": 1215, + "radius": 2 + }, + { + "x": 1142, + "y": 1217, + "radius": 1 + }, + { + "x": 1058, + "y": 1220, + "radius": 1 + }, + { + "x": 1210, + "y": 1218, + "radius": 2 + }, + { + "x": 1265, + "y": 1225, + "radius": 1 + }, + { + "x": 1217, + "y": 1228, + "radius": 2 + }, + { + "x": 1057, + "y": 1233, + "radius": 1 + }, + { + "x": 1115, + "y": 1238, + "radius": 2 + }, + { + "x": 1125, + "y": 1239, + "radius": 2 + }, + { + "x": 1250, + "y": 1237, + "radius": 2 + }, + { + "x": 1080, + "y": 1244, + "radius": 2 + }, + { + "x": 1126, + "y": 1249, + "radius": 2 + }, + { + "x": 1244, + "y": 1249, + "radius": 2 + }, + { + "x": 1071, + "y": 1254, + "radius": 1 + }, + { + "x": 1142, + "y": 1255, + "radius": 1 + }, + { + "x": 1234, + "y": 1259, + "radius": 1 + }, + { + "x": 1266, + "y": 1260, + "radius": 1 + }, + { + "x": 1276, + "y": 1262, + "radius": 1 + }, + { + "x": 1069, + "y": 1263, + "radius": 1 + }, + { + "x": 1263, + "y": 1269, + "radius": 2 + }, + { + "x": 1080, + "y": 1271, + "radius": 2 + }, + { + "x": 1236, + "y": 1270, + "radius": 1 + }, + { + "x": 1105, + "y": 1276, + "radius": 2 + }, + { + "x": 1188, + "y": 1272, + "radius": 2 + }, + { + "x": 1054, + "y": 1275, + "radius": 1 + }, + { + "x": 1072, + "y": 1276, + "radius": 2 + }, + { + "x": 1273, + "y": 1277, + "radius": 2 + }, + { + "x": 1131, + "y": 1279, + "radius": 1 + }, + { + "x": 1285, + "y": 1279, + "radius": 1 + }, + { + "x": 1036, + "y": 1279, + "radius": 1 + }, + { + "x": 1293, + "y": 1285, + "radius": 1 + }, + { + "x": 1102, + "y": 1285, + "radius": 1 + }, + { + "x": 1243, + "y": 1288, + "radius": 2 + }, + { + "x": 1109, + "y": 1288, + "radius": 2 + }, + { + "x": 1193, + "y": 1292, + "radius": 2 + }, + { + "x": 1203, + "y": 1294, + "radius": 2 + }, + { + "x": 1035, + "y": 1297, + "radius": 2 + }, + { + "x": 1067, + "y": 1299, + "radius": 2 + }, + { + "x": 1130, + "y": 1299, + "radius": 2 + }, + { + "x": 1196, + "y": 1300, + "radius": 2 + }, + { + "x": 1304, + "y": 1302, + "radius": 2 + }, + { + "x": 1118, + "y": 1303, + "radius": 2 + }, + { + "x": 1102, + "y": 1307, + "radius": 2 + }, + { + "x": 1170, + "y": 1307, + "radius": 2 + }, + { + "x": 1291, + "y": 1308, + "radius": 2 + }, + { + "x": 1039, + "y": 1313, + "radius": 2 + }, + { + "x": 1243, + "y": 1311, + "radius": 2 + }, + { + "x": 1174, + "y": 1313, + "radius": 2 + }, + { + "x": 1263, + "y": 1313, + "radius": 1 + }, + { + "x": 1226, + "y": 1316, + "radius": 1 + }, + { + "x": 1088, + "y": 1320, + "radius": 1 + }, + { + "x": 1129, + "y": 1317, + "radius": 1 + }, + { + "x": 1182, + "y": 1321, + "radius": 2 + }, + { + "x": 1198, + "y": 1318, + "radius": 2 + }, + { + "x": 1299, + "y": 1320, + "radius": 2 + }, + { + "x": 1054, + "y": 1323, + "radius": 2 + }, + { + "x": 1079, + "y": 1322, + "radius": 1 + }, + { + "x": 1205, + "y": 1326, + "radius": 2 + }, + { + "x": 1144, + "y": 1325, + "radius": 1 + }, + { + "x": 1284, + "y": 1327, + "radius": 1 + }, + { + "x": 1055, + "y": 1331, + "radius": 2 + }, + { + "x": 1266, + "y": 1330, + "radius": 1 + }, + { + "x": 1086, + "y": 1333, + "radius": 2 + }, + { + "x": 1149, + "y": 1337, + "radius": 2 + }, + { + "x": 1240, + "y": 1337, + "radius": 2 + }, + { + "x": 1167, + "y": 1342, + "radius": 2 + }, + { + "x": 1269, + "y": 1343, + "radius": 2 + }, + { + "x": 1064, + "y": 1344, + "radius": 2 + }, + { + "x": 1102, + "y": 1343, + "radius": 1 + }, + { + "x": 1184, + "y": 1342, + "radius": 1 + }, + { + "x": 1217, + "y": 1343, + "radius": 1 + }, + { + "x": 1253, + "y": 1343, + "radius": 1 + }, + { + "x": 1056, + "y": 1345, + "radius": 2 + }, + { + "x": 1292, + "y": 1347, + "radius": 2 + }, + { + "x": 1101, + "y": 1350, + "radius": 1 + }, + { + "x": 1113, + "y": 1351, + "radius": 2 + }, + { + "x": 1120, + "y": 1350, + "radius": 1 + }, + { + "x": 1206, + "y": 1350, + "radius": 2 + }, + { + "x": 1246, + "y": 1352, + "radius": 2 + }, + { + "x": 1259, + "y": 1351, + "radius": 2 + }, + { + "x": 1166, + "y": 1352, + "radius": 2 + }, + { + "x": 1090, + "y": 1354, + "radius": 1 + }, + { + "x": 1200, + "y": 1358, + "radius": 1 + }, + { + "x": 1057, + "y": 1362, + "radius": 2 + }, + { + "x": 1130, + "y": 1366, + "radius": 2 + }, + { + "x": 1147, + "y": 1367, + "radius": 2 + }, + { + "x": 1265, + "y": 1368, + "radius": 1 + }, + { + "x": 1084, + "y": 1372, + "radius": 2 + }, + { + "x": 1104, + "y": 1375, + "radius": 2 + }, + { + "x": 1153, + "y": 1378, + "radius": 1 + }, + { + "x": 1126, + "y": 1379, + "radius": 1 + }, + { + "x": 1171, + "y": 1383, + "radius": 1 + }, + { + "x": 1252, + "y": 1387, + "radius": 2 + }, + { + "x": 1238, + "y": 1385, + "radius": 2 + }, + { + "x": 1053, + "y": 1387, + "radius": 1 + }, + { + "x": 1064, + "y": 1388, + "radius": 1 + }, + { + "x": 1227, + "y": 1389, + "radius": 2 + }, + { + "x": 1278, + "y": 1395, + "radius": 2 + }, + { + "x": 1195, + "y": 1395, + "radius": 2 + }, + { + "x": 1088, + "y": 1404, + "radius": 1 + }, + { + "x": 1108, + "y": 1405, + "radius": 2 + }, + { + "x": 1147, + "y": 1410, + "radius": 2 + }, + { + "x": 1195, + "y": 1408, + "radius": 1 + }, + { + "x": 1283, + "y": 1412, + "radius": 2 + }, + { + "x": 1058, + "y": 1412, + "radius": 2 + }, + { + "x": 1097, + "y": 1416, + "radius": 1 + }, + { + "x": 1086, + "y": 1421, + "radius": 1 + }, + { + "x": 1217, + "y": 1420, + "radius": 1 + }, + { + "x": 1046, + "y": 1424, + "radius": 1 + }, + { + "x": 1141, + "y": 1427, + "radius": 2 + }, + { + "x": 1103, + "y": 1437, + "radius": 2 + }, + { + "x": 1167, + "y": 1445, + "radius": 2 + }, + { + "x": 1280, + "y": 1441, + "radius": 1 + }, + { + "x": 1241, + "y": 1446, + "radius": 1 + }, + { + "x": 1203, + "y": 1450, + "radius": 2 + }, + { + "x": 1086, + "y": 1450, + "radius": 1 + }, + { + "x": 1155, + "y": 1451, + "radius": 1 + }, + { + "x": 1099, + "y": 1452, + "radius": 2 + }, + { + "x": 1250, + "y": 1454, + "radius": 1 + }, + { + "x": 1222, + "y": 1458, + "radius": 2 + }, + { + "x": 1208, + "y": 1466, + "radius": 2 + }, + { + "x": 1231, + "y": 1465, + "radius": 1 + }, + { + "x": 1264, + "y": 1469, + "radius": 2 + }, + { + "x": 1091, + "y": 1471, + "radius": 1 + }, + { + "x": 1226, + "y": 1471, + "radius": 1 + }, + { + "x": 1142, + "y": 1476, + "radius": 1 + }, + { + "x": 1072, + "y": 1480, + "radius": 1 + }, + { + "x": 1242, + "y": 1481, + "radius": 1 + }, + { + "x": 1152, + "y": 1488, + "radius": 1 + }, + { + "x": 1166, + "y": 1490, + "radius": 2 + }, + { + "x": 1212, + "y": 1488, + "radius": 1 + }, + { + "x": 1235, + "y": 1494, + "radius": 2 + }, + { + "x": 1109, + "y": 1500, + "radius": 2 + }, + { + "x": 1138, + "y": 1502, + "radius": 2 + }, + { + "x": 1155, + "y": 1504, + "radius": 2 + }, + { + "x": 1116, + "y": 1510, + "radius": 2 + }, + { + "x": 1219, + "y": 1511, + "radius": 1 + }, + { + "x": 1137, + "y": 1514, + "radius": 2 + }, + { + "x": 1320, + "y": 1521, + "radius": 1 + }, + { + "x": 1117, + "y": 1525, + "radius": 2 + }, + { + "x": 1139, + "y": 1533, + "radius": 2 + }, + { + "x": 1159, + "y": 1537, + "radius": 2 + }, + { + "x": 1403, + "y": 1536, + "radius": 1 + }, + { + "x": 1223, + "y": 1541, + "radius": 1 + }, + { + "x": 1155, + "y": 1552, + "radius": 2 + }, + { + "x": 1409, + "y": 1555, + "radius": 1 + }, + { + "x": 968, + "y": 1561, + "radius": 2 + }, + { + "x": 1420, + "y": 1563, + "radius": 2 + }, + { + "x": 1276, + "y": 1564, + "radius": 1 + }, + { + "x": 1128, + "y": 1567, + "radius": 2 + }, + { + "x": 1454, + "y": 1569, + "radius": 1 + }, + { + "x": 931, + "y": 1573, + "radius": 2 + }, + { + "x": 918, + "y": 1581, + "radius": 1 + }, + { + "x": 941, + "y": 1579, + "radius": 1 + }, + { + "x": 1218, + "y": 1583, + "radius": 2 + }, + { + "x": 958, + "y": 1582, + "radius": 2 + }, + { + "x": 966, + "y": 1581, + "radius": 2 + }, + { + "x": 1456, + "y": 1582, + "radius": 1 + }, + { + "x": 975, + "y": 1585, + "radius": 2 + }, + { + "x": 953, + "y": 1588, + "radius": 2 + }, + { + "x": 1134, + "y": 1592, + "radius": 2 + }, + { + "x": 1108, + "y": 1594, + "radius": 1 + }, + { + "x": 1494, + "y": 1597, + "radius": 2 + }, + { + "x": 1171, + "y": 1599, + "radius": 1 + }, + { + "x": 955, + "y": 1605, + "radius": 1 + }, + { + "x": 971, + "y": 1607, + "radius": 2 + }, + { + "x": 1141, + "y": 1608, + "radius": 2 + }, + { + "x": 1470, + "y": 1609, + "radius": 1 + }, + { + "x": 1335, + "y": 1611, + "radius": 1 + }, + { + "x": 889, + "y": 1612, + "radius": 2 + }, + { + "x": 922, + "y": 1612, + "radius": 2 + }, + { + "x": 945, + "y": 1612, + "radius": 1 + }, + { + "x": 1208, + "y": 1613, + "radius": 1 + }, + { + "x": 1440, + "y": 1613, + "radius": 2 + }, + { + "x": 913, + "y": 1618, + "radius": 2 + }, + { + "x": 1168, + "y": 1617, + "radius": 2 + }, + { + "x": 941, + "y": 1620, + "radius": 2 + }, + { + "x": 1516, + "y": 1622, + "radius": 1 + }, + { + "x": 923, + "y": 1628, + "radius": 2 + }, + { + "x": 1222, + "y": 1628, + "radius": 2 + }, + { + "x": 1212, + "y": 1633, + "radius": 1 + }, + { + "x": 880, + "y": 1634, + "radius": 1 + }, + { + "x": 1226, + "y": 1638, + "radius": 1 + }, + { + "x": 1235, + "y": 1643, + "radius": 1 + }, + { + "x": 1245, + "y": 1647, + "radius": 2 + }, + { + "x": 868, + "y": 1646, + "radius": 1 + }, + { + "x": 1421, + "y": 1646, + "radius": 1 + }, + { + "x": 1523, + "y": 1648, + "radius": 2 + }, + { + "x": 902, + "y": 1650, + "radius": 2 + }, + { + "x": 1261, + "y": 1650, + "radius": 1 + }, + { + "x": 1595, + "y": 1651, + "radius": 1 + }, + { + "x": 1130, + "y": 1652, + "radius": 1 + }, + { + "x": 1141, + "y": 1653, + "radius": 1 + }, + { + "x": 1043, + "y": 1657, + "radius": 1 + }, + { + "x": 1161, + "y": 1659, + "radius": 2 + }, + { + "x": 1145, + "y": 1663, + "radius": 2 + }, + { + "x": 1169, + "y": 1662, + "radius": 2 + }, + { + "x": 1263, + "y": 1665, + "radius": 2 + }, + { + "x": 1275, + "y": 1668, + "radius": 2 + }, + { + "x": 1305, + "y": 1668, + "radius": 1 + }, + { + "x": 832, + "y": 1675, + "radius": 1 + }, + { + "x": 1169, + "y": 1674, + "radius": 1 + }, + { + "x": 1180, + "y": 1676, + "radius": 2 + }, + { + "x": 1553, + "y": 1674, + "radius": 1 + }, + { + "x": 934, + "y": 1677, + "radius": 2 + }, + { + "x": 1510, + "y": 1675, + "radius": 1 + }, + { + "x": 808, + "y": 1679, + "radius": 2 + }, + { + "x": 1134, + "y": 1681, + "radius": 2 + }, + { + "x": 1255, + "y": 1683, + "radius": 2 + }, + { + "x": 869, + "y": 1682, + "radius": 1 + }, + { + "x": 783, + "y": 1685, + "radius": 2 + }, + { + "x": 1204, + "y": 1686, + "radius": 2 + }, + { + "x": 1266, + "y": 1684, + "radius": 1 + }, + { + "x": 1433, + "y": 1684, + "radius": 2 + }, + { + "x": 1621, + "y": 1683, + "radius": 1 + }, + { + "x": 1147, + "y": 1688, + "radius": 2 + }, + { + "x": 1611, + "y": 1688, + "radius": 2 + }, + { + "x": 923, + "y": 1689, + "radius": 2 + }, + { + "x": 833, + "y": 1691, + "radius": 1 + }, + { + "x": 1244, + "y": 1692, + "radius": 1 + }, + { + "x": 1459, + "y": 1690, + "radius": 2 + }, + { + "x": 1473, + "y": 1692, + "radius": 1 + }, + { + "x": 1697, + "y": 1690, + "radius": 1 + }, + { + "x": 1577, + "y": 1695, + "radius": 2 + }, + { + "x": 840, + "y": 1696, + "radius": 2 + }, + { + "x": 1566, + "y": 1695, + "radius": 2 + }, + { + "x": 1261, + "y": 1700, + "radius": 2 + }, + { + "x": 1436, + "y": 1700, + "radius": 2 + }, + { + "x": 1229, + "y": 1700, + "radius": 2 + }, + { + "x": 1274, + "y": 1700, + "radius": 1 + }, + { + "x": 1683, + "y": 1700, + "radius": 1 + }, + { + "x": 743, + "y": 1702, + "radius": 1 + }, + { + "x": 1603, + "y": 1702, + "radius": 1 + }, + { + "x": 1248, + "y": 1707, + "radius": 2 + }, + { + "x": 1730, + "y": 1708, + "radius": 1 + }, + { + "x": 775, + "y": 1709, + "radius": 1 + }, + { + "x": 948, + "y": 1711, + "radius": 2 + }, + { + "x": 1487, + "y": 1711, + "radius": 1 + }, + { + "x": 1622, + "y": 1712, + "radius": 2 + }, + { + "x": 816, + "y": 1714, + "radius": 2 + }, + { + "x": 711, + "y": 1717, + "radius": 2 + }, + { + "x": 791, + "y": 1717, + "radius": 2 + }, + { + "x": 1649, + "y": 1718, + "radius": 1 + }, + { + "x": 1723, + "y": 1716, + "radius": 2 + }, + { + "x": 691, + "y": 1719, + "radius": 2 + }, + { + "x": 959, + "y": 1721, + "radius": 1 + }, + { + "x": 1144, + "y": 1720, + "radius": 2 + }, + { + "x": 1469, + "y": 1723, + "radius": 2 + }, + { + "x": 1596, + "y": 1719, + "radius": 1 + }, + { + "x": 721, + "y": 1722, + "radius": 2 + }, + { + "x": 874, + "y": 1724, + "radius": 2 + }, + { + "x": 1635, + "y": 1722, + "radius": 1 + }, + { + "x": 1584, + "y": 1725, + "radius": 2 + }, + { + "x": 1278, + "y": 1727, + "radius": 2 + }, + { + "x": 1350, + "y": 1728, + "radius": 2 + }, + { + "x": 1542, + "y": 1728, + "radius": 1 + }, + { + "x": 793, + "y": 1729, + "radius": 1 + }, + { + "x": 838, + "y": 1733, + "radius": 2 + }, + { + "x": 1600, + "y": 1730, + "radius": 1 + }, + { + "x": 1185, + "y": 1732, + "radius": 2 + }, + { + "x": 1448, + "y": 1733, + "radius": 2 + }, + { + "x": 753, + "y": 1735, + "radius": 2 + }, + { + "x": 761, + "y": 1733, + "radius": 1 + }, + { + "x": 1127, + "y": 1735, + "radius": 2 + }, + { + "x": 1164, + "y": 1734, + "radius": 1 + }, + { + "x": 1550, + "y": 1733, + "radius": 1 + }, + { + "x": 1573, + "y": 1735, + "radius": 1 + }, + { + "x": 1283, + "y": 1738, + "radius": 1 + }, + { + "x": 1692, + "y": 1735, + "radius": 1 + }, + { + "x": 951, + "y": 1738, + "radius": 2 + }, + { + "x": 1059, + "y": 1738, + "radius": 2 + }, + { + "x": 826, + "y": 1740, + "radius": 1 + }, + { + "x": 1466, + "y": 1744, + "radius": 2 + }, + { + "x": 1483, + "y": 1745, + "radius": 2 + }, + { + "x": 1497, + "y": 1741, + "radius": 1 + }, + { + "x": 1194, + "y": 1744, + "radius": 1 + }, + { + "x": 747, + "y": 1746, + "radius": 1 + }, + { + "x": 1307, + "y": 1746, + "radius": 2 + }, + { + "x": 1611, + "y": 1747, + "radius": 2 + }, + { + "x": 809, + "y": 1748, + "radius": 2 + }, + { + "x": 649, + "y": 1751, + "radius": 1 + }, + { + "x": 700, + "y": 1751, + "radius": 1 + }, + { + "x": 1204, + "y": 1750, + "radius": 1 + }, + { + "x": 1641, + "y": 1751, + "radius": 2 + }, + { + "x": 1133, + "y": 1754, + "radius": 2 + }, + { + "x": 1664, + "y": 1753, + "radius": 2 + }, + { + "x": 709, + "y": 1754, + "radius": 1 + }, + { + "x": 1121, + "y": 1755, + "radius": 1 + }, + { + "x": 1172, + "y": 1755, + "radius": 2 + }, + { + "x": 1276, + "y": 1756, + "radius": 2 + }, + { + "x": 1759, + "y": 1753, + "radius": 1 + }, + { + "x": 796, + "y": 1757, + "radius": 1 + }, + { + "x": 1695, + "y": 1757, + "radius": 2 + }, + { + "x": 1742, + "y": 1756, + "radius": 1 + }, + { + "x": 1305, + "y": 1758, + "radius": 1 + }, + { + "x": 736, + "y": 1761, + "radius": 2 + }, + { + "x": 1242, + "y": 1763, + "radius": 2 + }, + { + "x": 1503, + "y": 1761, + "radius": 2 + }, + { + "x": 1569, + "y": 1763, + "radius": 2 + }, + { + "x": 756, + "y": 1763, + "radius": 2 + }, + { + "x": 1130, + "y": 1763, + "radius": 1 + }, + { + "x": 1273, + "y": 1763, + "radius": 2 + }, + { + "x": 1444, + "y": 1765, + "radius": 1 + }, + { + "x": 1557, + "y": 1764, + "radius": 1 + }, + { + "x": 1680, + "y": 1765, + "radius": 2 + }, + { + "x": 775, + "y": 1767, + "radius": 1 + }, + { + "x": 851, + "y": 1765, + "radius": 2 + }, + { + "x": 1261, + "y": 1767, + "radius": 2 + }, + { + "x": 1707, + "y": 1767, + "radius": 2 + }, + { + "x": 1087, + "y": 1767, + "radius": 1 + }, + { + "x": 957, + "y": 1770, + "radius": 1 + }, + { + "x": 1483, + "y": 1771, + "radius": 1 + }, + { + "x": 1656, + "y": 1770, + "radius": 2 + }, + { + "x": 1696, + "y": 1773, + "radius": 2 + }, + { + "x": 1167, + "y": 1774, + "radius": 1 + }, + { + "x": 1527, + "y": 1777, + "radius": 1 + }, + { + "x": 1637, + "y": 1779, + "radius": 2 + }, + { + "x": 617, + "y": 1781, + "radius": 2 + }, + { + "x": 1045, + "y": 1782, + "radius": 2 + }, + { + "x": 1089, + "y": 1781, + "radius": 2 + }, + { + "x": 729, + "y": 1783, + "radius": 2 + }, + { + "x": 1098, + "y": 1783, + "radius": 2 + }, + { + "x": 909, + "y": 1785, + "radius": 1 + }, + { + "x": 949, + "y": 1785, + "radius": 1 + }, + { + "x": 1680, + "y": 1785, + "radius": 2 + }, + { + "x": 1142, + "y": 1788, + "radius": 1 + }, + { + "x": 1267, + "y": 1785, + "radius": 1 + }, + { + "x": 1287, + "y": 1787, + "radius": 1 + }, + { + "x": 1371, + "y": 1785, + "radius": 1 + }, + { + "x": 699, + "y": 1789, + "radius": 2 + }, + { + "x": 1623, + "y": 1789, + "radius": 2 + }, + { + "x": 687, + "y": 1791, + "radius": 2 + }, + { + "x": 897, + "y": 1789, + "radius": 2 + }, + { + "x": 1597, + "y": 1792, + "radius": 1 + }, + { + "x": 1546, + "y": 1793, + "radius": 2 + }, + { + "x": 1562, + "y": 1793, + "radius": 2 + }, + { + "x": 660, + "y": 1795, + "radius": 1 + }, + { + "x": 717, + "y": 1793, + "radius": 2 + }, + { + "x": 1121, + "y": 1794, + "radius": 1 + }, + { + "x": 786, + "y": 1798, + "radius": 2 + }, + { + "x": 874, + "y": 1797, + "radius": 1 + }, + { + "x": 1519, + "y": 1797, + "radius": 2 + }, + { + "x": 1630, + "y": 1797, + "radius": 1 + }, + { + "x": 1643, + "y": 1798, + "radius": 2 + }, + { + "x": 1142, + "y": 1800, + "radius": 2 + }, + { + "x": 915, + "y": 1802, + "radius": 1 + }, + { + "x": 944, + "y": 1801, + "radius": 1 + }, + { + "x": 1248, + "y": 1804, + "radius": 2 + }, + { + "x": 831, + "y": 1805, + "radius": 2 + }, + { + "x": 889, + "y": 1803, + "radius": 2 + }, + { + "x": 1465, + "y": 1807, + "radius": 2 + }, + { + "x": 1502, + "y": 1806, + "radius": 1 + }, + { + "x": 692, + "y": 1806, + "radius": 2 + }, + { + "x": 1087, + "y": 1806, + "radius": 1 + }, + { + "x": 1102, + "y": 1807, + "radius": 2 + }, + { + "x": 1662, + "y": 1805, + "radius": 2 + }, + { + "x": 922, + "y": 1809, + "radius": 1 + }, + { + "x": 1560, + "y": 1808, + "radius": 2 + }, + { + "x": 703, + "y": 1811, + "radius": 2 + }, + { + "x": 668, + "y": 1812, + "radius": 1 + }, + { + "x": 743, + "y": 1812, + "radius": 1 + }, + { + "x": 820, + "y": 1813, + "radius": 1 + }, + { + "x": 912, + "y": 1815, + "radius": 2 + }, + { + "x": 1163, + "y": 1813, + "radius": 2 + }, + { + "x": 1174, + "y": 1813, + "radius": 1 + }, + { + "x": 1194, + "y": 1811, + "radius": 1 + }, + { + "x": 1600, + "y": 1813, + "radius": 1 + }, + { + "x": 1652, + "y": 1813, + "radius": 2 + }, + { + "x": 1741, + "y": 1813, + "radius": 1 + }, + { + "x": 777, + "y": 1817, + "radius": 2 + }, + { + "x": 729, + "y": 1816, + "radius": 2 + }, + { + "x": 1470, + "y": 1819, + "radius": 2 + }, + { + "x": 1721, + "y": 1815, + "radius": 1 + }, + { + "x": 1480, + "y": 1818, + "radius": 1 + }, + { + "x": 1631, + "y": 1818, + "radius": 2 + }, + { + "x": 920, + "y": 1822, + "radius": 2 + }, + { + "x": 1243, + "y": 1821, + "radius": 1 + }, + { + "x": 1456, + "y": 1819, + "radius": 1 + }, + { + "x": 798, + "y": 1823, + "radius": 2 + }, + { + "x": 1736, + "y": 1822, + "radius": 2 + }, + { + "x": 601, + "y": 1824, + "radius": 1 + }, + { + "x": 931, + "y": 1826, + "radius": 2 + }, + { + "x": 1088, + "y": 1824, + "radius": 2 + }, + { + "x": 1514, + "y": 1825, + "radius": 2 + }, + { + "x": 853, + "y": 1827, + "radius": 2 + }, + { + "x": 1495, + "y": 1827, + "radius": 2 + }, + { + "x": 1615, + "y": 1827, + "radius": 1 + }, + { + "x": 692, + "y": 1830, + "radius": 2 + }, + { + "x": 719, + "y": 1829, + "radius": 2 + }, + { + "x": 938, + "y": 1830, + "radius": 1 + }, + { + "x": 1582, + "y": 1827, + "radius": 1 + }, + { + "x": 1744, + "y": 1829, + "radius": 2 + }, + { + "x": 1285, + "y": 1831, + "radius": 1 + }, + { + "x": 1334, + "y": 1831, + "radius": 2 + }, + { + "x": 675, + "y": 1833, + "radius": 1 + }, + { + "x": 795, + "y": 1834, + "radius": 2 + }, + { + "x": 1366, + "y": 1833, + "radius": 2 + }, + { + "x": 1596, + "y": 1833, + "radius": 1 + }, + { + "x": 703, + "y": 1835, + "radius": 2 + }, + { + "x": 838, + "y": 1836, + "radius": 2 + }, + { + "x": 1292, + "y": 1834, + "radius": 2 + }, + { + "x": 1710, + "y": 1834, + "radius": 2 + }, + { + "x": 1752, + "y": 1835, + "radius": 2 + }, + { + "x": 1607, + "y": 1836, + "radius": 1 + }, + { + "x": 1319, + "y": 1839, + "radius": 2 + }, + { + "x": 1498, + "y": 1839, + "radius": 1 + }, + { + "x": 1732, + "y": 1839, + "radius": 2 + }, + { + "x": 853, + "y": 1842, + "radius": 2 + }, + { + "x": 924, + "y": 1842, + "radius": 2 + }, + { + "x": 1110, + "y": 1839, + "radius": 1 + }, + { + "x": 1202, + "y": 1841, + "radius": 2 + }, + { + "x": 1573, + "y": 1842, + "radius": 2 + }, + { + "x": 898, + "y": 1843, + "radius": 2 + }, + { + "x": 933, + "y": 1844, + "radius": 2 + }, + { + "x": 1084, + "y": 1844, + "radius": 1 + }, + { + "x": 1289, + "y": 1846, + "radius": 1 + }, + { + "x": 1301, + "y": 1845, + "radius": 1 + }, + { + "x": 641, + "y": 1846, + "radius": 2 + }, + { + "x": 1495, + "y": 1845, + "radius": 1 + }, + { + "x": 1674, + "y": 1846, + "radius": 1 + }, + { + "x": 604, + "y": 1848, + "radius": 2 + }, + { + "x": 741, + "y": 1847, + "radius": 1 + }, + { + "x": 757, + "y": 1848, + "radius": 1 + }, + { + "x": 1122, + "y": 1849, + "radius": 2 + }, + { + "x": 1267, + "y": 1851, + "radius": 1 + }, + { + "x": 1159, + "y": 1853, + "radius": 1 + }, + { + "x": 1597, + "y": 1854, + "radius": 2 + }, + { + "x": 1724, + "y": 1852, + "radius": 1 + }, + { + "x": 1111, + "y": 1854, + "radius": 2 + }, + { + "x": 1569, + "y": 1854, + "radius": 2 + }, + { + "x": 1631, + "y": 1856, + "radius": 2 + }, + { + "x": 1696, + "y": 1855, + "radius": 1 + }, + { + "x": 1219, + "y": 1857, + "radius": 2 + }, + { + "x": 1745, + "y": 1856, + "radius": 2 + }, + { + "x": 1314, + "y": 1859, + "radius": 2 + }, + { + "x": 861, + "y": 1859, + "radius": 1 + }, + { + "x": 1164, + "y": 1862, + "radius": 2 + }, + { + "x": 1245, + "y": 1861, + "radius": 2 + }, + { + "x": 1493, + "y": 1862, + "radius": 2 + }, + { + "x": 1503, + "y": 1861, + "radius": 1 + }, + { + "x": 676, + "y": 1862, + "radius": 1 + }, + { + "x": 1052, + "y": 1863, + "radius": 2 + }, + { + "x": 1580, + "y": 1864, + "radius": 1 + }, + { + "x": 841, + "y": 1865, + "radius": 1 + }, + { + "x": 1272, + "y": 1864, + "radius": 1 + }, + { + "x": 1345, + "y": 1863, + "radius": 1 + }, + { + "x": 1566, + "y": 1865, + "radius": 2 + }, + { + "x": 869, + "y": 1866, + "radius": 2 + }, + { + "x": 1034, + "y": 1868, + "radius": 1 + }, + { + "x": 1124, + "y": 1866, + "radius": 2 + }, + { + "x": 1146, + "y": 1865, + "radius": 2 + }, + { + "x": 1529, + "y": 1866, + "radius": 1 + }, + { + "x": 1535, + "y": 1865, + "radius": 1 + }, + { + "x": 1760, + "y": 1867, + "radius": 1 + }, + { + "x": 1105, + "y": 1870, + "radius": 2 + }, + { + "x": 1182, + "y": 1868, + "radius": 1 + }, + { + "x": 1390, + "y": 1868, + "radius": 2 + }, + { + "x": 1644, + "y": 1867, + "radius": 2 + }, + { + "x": 1681, + "y": 1869, + "radius": 1 + }, + { + "x": 1711, + "y": 1868, + "radius": 2 + }, + { + "x": 743, + "y": 1870, + "radius": 2 + }, + { + "x": 859, + "y": 1873, + "radius": 2 + }, + { + "x": 921, + "y": 1870, + "radius": 1 + }, + { + "x": 1094, + "y": 1871, + "radius": 2 + }, + { + "x": 1304, + "y": 1870, + "radius": 1 + }, + { + "x": 636, + "y": 1871, + "radius": 2 + }, + { + "x": 1544, + "y": 1874, + "radius": 1 + }, + { + "x": 931, + "y": 1874, + "radius": 1 + }, + { + "x": 1403, + "y": 1874, + "radius": 1 + }, + { + "x": 1689, + "y": 1874, + "radius": 2 + }, + { + "x": 764, + "y": 1878, + "radius": 2 + }, + { + "x": 849, + "y": 1876, + "radius": 1 + }, + { + "x": 1587, + "y": 1877, + "radius": 2 + }, + { + "x": 606, + "y": 1878, + "radius": 1 + }, + { + "x": 924, + "y": 1878, + "radius": 1 + }, + { + "x": 670, + "y": 1881, + "radius": 2 + }, + { + "x": 753, + "y": 1880, + "radius": 2 + }, + { + "x": 1106, + "y": 1881, + "radius": 2 + }, + { + "x": 1533, + "y": 1882, + "radius": 2 + }, + { + "x": 1661, + "y": 1879, + "radius": 2 + }, + { + "x": 1672, + "y": 1879, + "radius": 1 + }, + { + "x": 911, + "y": 1881, + "radius": 1 + }, + { + "x": 1516, + "y": 1885, + "radius": 2 + }, + { + "x": 653, + "y": 1885, + "radius": 2 + }, + { + "x": 662, + "y": 1884, + "radius": 2 + }, + { + "x": 730, + "y": 1885, + "radius": 2 + }, + { + "x": 798, + "y": 1884, + "radius": 1 + }, + { + "x": 688, + "y": 1888, + "radius": 1 + }, + { + "x": 814, + "y": 1886, + "radius": 2 + }, + { + "x": 1164, + "y": 1886, + "radius": 1 + }, + { + "x": 1379, + "y": 1885, + "radius": 2 + }, + { + "x": 1627, + "y": 1886, + "radius": 1 + }, + { + "x": 1078, + "y": 1889, + "radius": 1 + }, + { + "x": 1489, + "y": 1887, + "radius": 2 + }, + { + "x": 1500, + "y": 1888, + "radius": 1 + }, + { + "x": 1547, + "y": 1888, + "radius": 2 + }, + { + "x": 889, + "y": 1890, + "radius": 1 + }, + { + "x": 925, + "y": 1892, + "radius": 2 + }, + { + "x": 1057, + "y": 1891, + "radius": 1 + }, + { + "x": 1087, + "y": 1891, + "radius": 1 + }, + { + "x": 1638, + "y": 1890, + "radius": 2 + }, + { + "x": 1784, + "y": 1891, + "radius": 2 + }, + { + "x": 1160, + "y": 1894, + "radius": 2 + }, + { + "x": 1271, + "y": 1894, + "radius": 1 + }, + { + "x": 1608, + "y": 1893, + "radius": 2 + }, + { + "x": 1021, + "y": 1896, + "radius": 1 + }, + { + "x": 716, + "y": 1897, + "radius": 2 + }, + { + "x": 1307, + "y": 1896, + "radius": 2 + }, + { + "x": 1412, + "y": 1897, + "radius": 2 + }, + { + "x": 1727, + "y": 1895, + "radius": 1 + }, + { + "x": 1568, + "y": 1899, + "radius": 2 + }, + { + "x": 790, + "y": 1901, + "radius": 2 + }, + { + "x": 779, + "y": 1902, + "radius": 1 + }, + { + "x": 811, + "y": 1903, + "radius": 2 + }, + { + "x": 923, + "y": 1902, + "radius": 1 + }, + { + "x": 901, + "y": 1904, + "radius": 2 + }, + { + "x": 1161, + "y": 1904, + "radius": 1 + }, + { + "x": 1548, + "y": 1905, + "radius": 2 + }, + { + "x": 1515, + "y": 1907, + "radius": 1 + }, + { + "x": 1731, + "y": 1907, + "radius": 1 + }, + { + "x": 762, + "y": 1909, + "radius": 2 + }, + { + "x": 1185, + "y": 1910, + "radius": 2 + }, + { + "x": 1622, + "y": 1911, + "radius": 2 + }, + { + "x": 1654, + "y": 1909, + "radius": 1 + }, + { + "x": 703, + "y": 1911, + "radius": 2 + }, + { + "x": 786, + "y": 1911, + "radius": 1 + }, + { + "x": 1055, + "y": 1910, + "radius": 2 + }, + { + "x": 1639, + "y": 1911, + "radius": 2 + }, + { + "x": 1044, + "y": 1914, + "radius": 2 + }, + { + "x": 1509, + "y": 1917, + "radius": 2 + }, + { + "x": 670, + "y": 1915, + "radius": 1 + }, + { + "x": 911, + "y": 1917, + "radius": 2 + }, + { + "x": 998, + "y": 1915, + "radius": 1 + }, + { + "x": 1094, + "y": 1920, + "radius": 1 + }, + { + "x": 779, + "y": 1920, + "radius": 2 + }, + { + "x": 830, + "y": 1923, + "radius": 2 + }, + { + "x": 1110, + "y": 1919, + "radius": 2 + }, + { + "x": 1589, + "y": 1923, + "radius": 2 + }, + { + "x": 1037, + "y": 1923, + "radius": 2 + }, + { + "x": 1291, + "y": 1922, + "radius": 1 + }, + { + "x": 1055, + "y": 1923, + "radius": 1 + }, + { + "x": 1736, + "y": 1924, + "radius": 2 + }, + { + "x": 1103, + "y": 1926, + "radius": 1 + }, + { + "x": 799, + "y": 1930, + "radius": 2 + }, + { + "x": 896, + "y": 1929, + "radius": 1 + }, + { + "x": 1683, + "y": 1929, + "radius": 2 + }, + { + "x": 1316, + "y": 1930, + "radius": 1 + }, + { + "x": 1325, + "y": 1931, + "radius": 2 + }, + { + "x": 1361, + "y": 1932, + "radius": 2 + }, + { + "x": 1693, + "y": 1931, + "radius": 2 + }, + { + "x": 718, + "y": 1932, + "radius": 2 + }, + { + "x": 793, + "y": 1933, + "radius": 2 + }, + { + "x": 834, + "y": 1931, + "radius": 2 + }, + { + "x": 886, + "y": 1933, + "radius": 2 + }, + { + "x": 1281, + "y": 1931, + "radius": 1 + }, + { + "x": 669, + "y": 1933, + "radius": 1 + }, + { + "x": 1198, + "y": 1935, + "radius": 1 + }, + { + "x": 1508, + "y": 1935, + "radius": 2 + }, + { + "x": 1053, + "y": 1935, + "radius": 2 + }, + { + "x": 1082, + "y": 1937, + "radius": 2 + }, + { + "x": 737, + "y": 1939, + "radius": 2 + }, + { + "x": 1580, + "y": 1942, + "radius": 2 + }, + { + "x": 1672, + "y": 1941, + "radius": 1 + }, + { + "x": 697, + "y": 1941, + "radius": 2 + }, + { + "x": 1745, + "y": 1945, + "radius": 2 + }, + { + "x": 1113, + "y": 1948, + "radius": 1 + }, + { + "x": 1349, + "y": 1947, + "radius": 2 + }, + { + "x": 1193, + "y": 1948, + "radius": 1 + }, + { + "x": 1334, + "y": 1951, + "radius": 2 + }, + { + "x": 1617, + "y": 1950, + "radius": 2 + }, + { + "x": 1569, + "y": 1950, + "radius": 1 + }, + { + "x": 744, + "y": 1953, + "radius": 2 + }, + { + "x": 820, + "y": 1952, + "radius": 1 + }, + { + "x": 1584, + "y": 1954, + "radius": 2 + }, + { + "x": 1637, + "y": 1952, + "radius": 2 + }, + { + "x": 1061, + "y": 1953, + "radius": 1 + }, + { + "x": 1661, + "y": 1955, + "radius": 1 + }, + { + "x": 676, + "y": 1958, + "radius": 2 + }, + { + "x": 831, + "y": 1956, + "radius": 2 + }, + { + "x": 840, + "y": 1958, + "radius": 2 + }, + { + "x": 1401, + "y": 1958, + "radius": 2 + }, + { + "x": 1557, + "y": 1957, + "radius": 1 + }, + { + "x": 727, + "y": 1959, + "radius": 1 + }, + { + "x": 772, + "y": 1961, + "radius": 2 + }, + { + "x": 806, + "y": 1959, + "radius": 1 + }, + { + "x": 900, + "y": 1958, + "radius": 1 + }, + { + "x": 997, + "y": 1960, + "radius": 2 + }, + { + "x": 1324, + "y": 1959, + "radius": 2 + }, + { + "x": 1040, + "y": 1963, + "radius": 2 + }, + { + "x": 1089, + "y": 1964, + "radius": 2 + }, + { + "x": 1163, + "y": 1964, + "radius": 2 + }, + { + "x": 1625, + "y": 1964, + "radius": 1 + }, + { + "x": 983, + "y": 1966, + "radius": 2 + }, + { + "x": 1215, + "y": 1967, + "radius": 2 + }, + { + "x": 883, + "y": 1967, + "radius": 1 + }, + { + "x": 1180, + "y": 1971, + "radius": 2 + }, + { + "x": 1683, + "y": 1969, + "radius": 1 + }, + { + "x": 867, + "y": 1974, + "radius": 2 + }, + { + "x": 1106, + "y": 1974, + "radius": 2 + }, + { + "x": 1403, + "y": 1976, + "radius": 2 + }, + { + "x": 1617, + "y": 1973, + "radius": 2 + }, + { + "x": 1660, + "y": 1974, + "radius": 2 + }, + { + "x": 1703, + "y": 1975, + "radius": 2 + }, + { + "x": 1023, + "y": 1978, + "radius": 2 + }, + { + "x": 1270, + "y": 1978, + "radius": 1 + }, + { + "x": 861, + "y": 1980, + "radius": 2 + }, + { + "x": 1325, + "y": 1985, + "radius": 2 + }, + { + "x": 717, + "y": 1984, + "radius": 1 + }, + { + "x": 986, + "y": 1985, + "radius": 2 + }, + { + "x": 1763, + "y": 1988, + "radius": 2 + }, + { + "x": 782, + "y": 1987, + "radius": 2 + }, + { + "x": 842, + "y": 1987, + "radius": 2 + }, + { + "x": 1166, + "y": 1987, + "radius": 2 + }, + { + "x": 1313, + "y": 1988, + "radius": 2 + }, + { + "x": 1345, + "y": 1986, + "radius": 2 + }, + { + "x": 1673, + "y": 1988, + "radius": 2 + }, + { + "x": 1134, + "y": 1993, + "radius": 2 + }, + { + "x": 671, + "y": 1989, + "radius": 1 + }, + { + "x": 1121, + "y": 1992, + "radius": 2 + }, + { + "x": 1370, + "y": 1991, + "radius": 1 + }, + { + "x": 1412, + "y": 1992, + "radius": 2 + }, + { + "x": 1009, + "y": 1993, + "radius": 2 + }, + { + "x": 1105, + "y": 1993, + "radius": 1 + }, + { + "x": 872, + "y": 1995, + "radius": 2 + }, + { + "x": 1030, + "y": 1996, + "radius": 2 + }, + { + "x": 712, + "y": 1996, + "radius": 1 + }, + { + "x": 1653, + "y": 1996, + "radius": 2 + }, + { + "x": 803, + "y": 2001, + "radius": 2 + }, + { + "x": 823, + "y": 1999, + "radius": 2 + }, + { + "x": 706, + "y": 2001, + "radius": 2 + }, + { + "x": 855, + "y": 2000, + "radius": 2 + }, + { + "x": 900, + "y": 2002, + "radius": 2 + }, + { + "x": 984, + "y": 2001, + "radius": 2 + }, + { + "x": 1057, + "y": 2002, + "radius": 2 + }, + { + "x": 1127, + "y": 2002, + "radius": 2 + }, + { + "x": 1314, + "y": 2001, + "radius": 2 + }, + { + "x": 1674, + "y": 2001, + "radius": 2 + }, + { + "x": 693, + "y": 2003, + "radius": 2 + }, + { + "x": 832, + "y": 2003, + "radius": 2 + }, + { + "x": 1596, + "y": 2004, + "radius": 2 + }, + { + "x": 1641, + "y": 2003, + "radius": 1 + }, + { + "x": 1001, + "y": 2005, + "radius": 1 + }, + { + "x": 1085, + "y": 2004, + "radius": 1 + }, + { + "x": 1659, + "y": 2005, + "radius": 1 + }, + { + "x": 685, + "y": 2006, + "radius": 1 + }, + { + "x": 862, + "y": 2009, + "radius": 2 + }, + { + "x": 886, + "y": 2009, + "radius": 1 + }, + { + "x": 1619, + "y": 2009, + "radius": 1 + }, + { + "x": 767, + "y": 2010, + "radius": 1 + }, + { + "x": 1676, + "y": 2012, + "radius": 2 + }, + { + "x": 779, + "y": 2012, + "radius": 1 + }, + { + "x": 851, + "y": 2012, + "radius": 1 + }, + { + "x": 1104, + "y": 2015, + "radius": 2 + }, + { + "x": 1543, + "y": 2013, + "radius": 2 + }, + { + "x": 826, + "y": 2017, + "radius": 2 + }, + { + "x": 760, + "y": 2020, + "radius": 1 + }, + { + "x": 1037, + "y": 2021, + "radius": 1 + }, + { + "x": 781, + "y": 2024, + "radius": 2 + }, + { + "x": 831, + "y": 2025, + "radius": 2 + }, + { + "x": 797, + "y": 2023, + "radius": 2 + }, + { + "x": 869, + "y": 2027, + "radius": 2 + }, + { + "x": 1128, + "y": 2028, + "radius": 2 + }, + { + "x": 1271, + "y": 2029, + "radius": 2 + }, + { + "x": 1662, + "y": 2025, + "radius": 2 + }, + { + "x": 790, + "y": 2028, + "radius": 2 + }, + { + "x": 1587, + "y": 2034, + "radius": 2 + }, + { + "x": 1138, + "y": 2037, + "radius": 1 + }, + { + "x": 1619, + "y": 2036, + "radius": 2 + }, + { + "x": 1728, + "y": 2037, + "radius": 1 + }, + { + "x": 832, + "y": 2037, + "radius": 1 + }, + { + "x": 891, + "y": 2040, + "radius": 1 + }, + { + "x": 1265, + "y": 2039, + "radius": 2 + }, + { + "x": 1696, + "y": 2039, + "radius": 2 + }, + { + "x": 703, + "y": 2043, + "radius": 2 + }, + { + "x": 1647, + "y": 2042, + "radius": 2 + }, + { + "x": 802, + "y": 2042, + "radius": 2 + }, + { + "x": 1145, + "y": 2043, + "radius": 1 + }, + { + "x": 1225, + "y": 2049, + "radius": 2 + }, + { + "x": 726, + "y": 2049, + "radius": 1 + }, + { + "x": 1291, + "y": 2050, + "radius": 2 + }, + { + "x": 831, + "y": 2053, + "radius": 1 + }, + { + "x": 848, + "y": 2052, + "radius": 1 + }, + { + "x": 803, + "y": 2056, + "radius": 2 + }, + { + "x": 865, + "y": 2054, + "radius": 1 + }, + { + "x": 889, + "y": 2055, + "radius": 2 + }, + { + "x": 712, + "y": 2057, + "radius": 2 + }, + { + "x": 1185, + "y": 2055, + "radius": 2 + }, + { + "x": 728, + "y": 2058, + "radius": 2 + }, + { + "x": 1214, + "y": 2058, + "radius": 2 + }, + { + "x": 1567, + "y": 2062, + "radius": 2 + }, + { + "x": 1713, + "y": 2059, + "radius": 1 + }, + { + "x": 867, + "y": 2061, + "radius": 2 + }, + { + "x": 746, + "y": 2065, + "radius": 2 + }, + { + "x": 1171, + "y": 2067, + "radius": 2 + }, + { + "x": 759, + "y": 2070, + "radius": 1 + }, + { + "x": 886, + "y": 2068, + "radius": 2 + }, + { + "x": 854, + "y": 2077, + "radius": 2 + }, + { + "x": 1615, + "y": 2078, + "radius": 2 + }, + { + "x": 704, + "y": 2077, + "radius": 1 + }, + { + "x": 758, + "y": 2077, + "radius": 1 + }, + { + "x": 1258, + "y": 2079, + "radius": 2 + }, + { + "x": 1581, + "y": 2082, + "radius": 1 + }, + { + "x": 728, + "y": 2087, + "radius": 1 + }, + { + "x": 804, + "y": 2089, + "radius": 1 + }, + { + "x": 818, + "y": 2090, + "radius": 2 + }, + { + "x": 1613, + "y": 2093, + "radius": 1 + }, + { + "x": 761, + "y": 2097, + "radius": 1 + }, + { + "x": 771, + "y": 2097, + "radius": 1 + }, + { + "x": 849, + "y": 2100, + "radius": 2 + }, + { + "x": 1638, + "y": 2098, + "radius": 1 + }, + { + "x": 787, + "y": 2107, + "radius": 1 + }, + { + "x": 1702, + "y": 2105, + "radius": 1 + }, + { + "x": 776, + "y": 2109, + "radius": 2 + }, + { + "x": 859, + "y": 2110, + "radius": 2 + }, + { + "x": 1652, + "y": 2108, + "radius": 1 + }, + { + "x": 750, + "y": 2114, + "radius": 2 + }, + { + "x": 761, + "y": 2114, + "radius": 2 + }, + { + "x": 1602, + "y": 2117, + "radius": 1 + }, + { + "x": 734, + "y": 2118, + "radius": 1 + }, + { + "x": 720, + "y": 2120, + "radius": 1 + }, + { + "x": 787, + "y": 2124, + "radius": 2 + }, + { + "x": 864, + "y": 2125, + "radius": 2 + }, + { + "x": 825, + "y": 2134, + "radius": 2 + }, + { + "x": 739, + "y": 2134, + "radius": 1 + }, + { + "x": 777, + "y": 2140, + "radius": 2 + }, + { + "x": 767, + "y": 2141, + "radius": 1 + }, + { + "x": 727, + "y": 2147, + "radius": 1 + }, + { + "x": 797, + "y": 2151, + "radius": 2 + }, + { + "x": 713, + "y": 2153, + "radius": 1 + }, + { + "x": 760, + "y": 2153, + "radius": 2 + }, + { + "x": 1616, + "y": 2156, + "radius": 2 + }, + { + "x": 1606, + "y": 2156, + "radius": 1 + }, + { + "x": 821, + "y": 2170, + "radius": 1 + }, + { + "x": 835, + "y": 2177, + "radius": 2 + }, + { + "x": 822, + "y": 2184, + "radius": 1 + }, + { + "x": 774, + "y": 2189, + "radius": 1 + }, + { + "x": 1424, + "y": 2633, + "radius": 2 + }, + { + "x": 1270, + "y": 2670, + "radius": 2 + }, + { + "x": 1022, + "y": 2676, + "radius": 2 + }, + { + "x": 1243, + "y": 2684, + "radius": 1 + }, + { + "x": 965, + "y": 2694, + "radius": 2 + }, + { + "x": 1303, + "y": 2701, + "radius": 2 + }, + { + "x": 1328, + "y": 2711, + "radius": 1 + }, + { + "x": 1321, + "y": 2717, + "radius": 2 + }, + { + "x": 1448, + "y": 2717, + "radius": 1 + }, + { + "x": 1350, + "y": 2719, + "radius": 2 + }, + { + "x": 1178, + "y": 2723, + "radius": 1 + }, + { + "x": 1293, + "y": 2725, + "radius": 1 + }, + { + "x": 1084, + "y": 2728, + "radius": 1 + }, + { + "x": 1389, + "y": 2728, + "radius": 2 + }, + { + "x": 1124, + "y": 2730, + "radius": 2 + }, + { + "x": 1351, + "y": 2734, + "radius": 2 + }, + { + "x": 1170, + "y": 2736, + "radius": 1 + }, + { + "x": 1335, + "y": 2745, + "radius": 2 + }, + { + "x": 1171, + "y": 2751, + "radius": 2 + }, + { + "x": 1192, + "y": 2747, + "radius": 2 + }, + { + "x": 1372, + "y": 2753, + "radius": 1 + }, + { + "x": 1326, + "y": 2757, + "radius": 2 + }, + { + "x": 1201, + "y": 2758, + "radius": 2 + }, + { + "x": 1124, + "y": 2760, + "radius": 2 + }, + { + "x": 1218, + "y": 2762, + "radius": 1 + }, + { + "x": 1295, + "y": 2764, + "radius": 2 + }, + { + "x": 1179, + "y": 2768, + "radius": 2 + }, + { + "x": 1085, + "y": 2772, + "radius": 2 + }, + { + "x": 1072, + "y": 2773, + "radius": 1 + }, + { + "x": 1307, + "y": 2773, + "radius": 1 + }, + { + "x": 1178, + "y": 2780, + "radius": 2 + }, + { + "x": 1272, + "y": 2778, + "radius": 2 + }, + { + "x": 1169, + "y": 2782, + "radius": 2 + }, + { + "x": 1155, + "y": 2784, + "radius": 2 + }, + { + "x": 1334, + "y": 2783, + "radius": 1 + }, + { + "x": 1112, + "y": 2784, + "radius": 1 + } + ] + } + }, + "face_rectangle": { + "top": 1100, + "left": 322, + "width": 1753, + "height": 1753 + } +} \ No newline at end of file diff --git a/shape_predictor_68_face_landmarks.dat b/shape_predictor_68_face_landmarks.dat new file mode 100644 index 0000000..e0ec20d Binary files /dev/null and b/shape_predictor_68_face_landmarks.dat differ diff --git a/wanzheng.py b/wanzheng.py new file mode 100644 index 0000000..7ca4d10 --- /dev/null +++ b/wanzheng.py @@ -0,0 +1,1275 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +皮肤分析工具 - 后端 API 版 +输出 JSON 数据到文件夹,完成后回调后端接口 +""" + +import cv2 +import numpy as np +from ultralytics import YOLO +from pathlib import Path + +import os +import argparse +import threading + +from concurrent.futures import ThreadPoolExecutor, as_completed +from tqdm import tqdm +import logging +from typing import Dict, List, Optional, Tuple +import requests + +from config.settings import API_HOST, API_CALLBACK, PORE_PREDICTOR_PATH + +try: + import dlib + DLIB_IMPORT_ERROR = None +except (ImportError, OSError) as exc: + dlib = None + DLIB_IMPORT_ERROR = exc + +# ==================== 日志配置 ==================== +def setup_logging(verbose: bool = False): + level = logging.DEBUG if verbose else logging.INFO + logging.basicConfig( + level=level, + format='%(asctime)s | %(levelname)s | %(message)s', + datefmt='%H:%M:%S' + ) + return logging.getLogger(__name__) + +logger = setup_logging() + +# Ultralytics model instances are not safe to invoke from multiple threads. +# Keep the OpenCV stages concurrent while serializing only model inference. +MODEL_INFERENCE_LOCK = threading.Lock() +PORE_MODEL_LOAD_LOCK = threading.Lock() + +PORE_COLOR = (207, 95, 123) +PORE_MIN_RADIUS = 3 +PORE_MAX_RADIUS = 8 +PORE_CONTRAST_THRESHOLD = 22.0 +PORE_CIRCULARITY_THRESHOLD = 0.55 +PORE_MAX_COUNT = 2000 +PORE_FOREHEAD_RATIO = 0.38 +IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.bmp', '.webp'} +PORE_IMAGE_NAMES = ( + 'cross_left.jpg', + 'cross_mid.jpg', + 'cross_right.jpg', +) + +# ==================== 支持中文路径的读写函数 ==================== +def cv2_imread_chinese(file_path: str) -> Optional[np.ndarray]: + if not os.path.exists(file_path): + logger.warning(f"文件不存在:{file_path}") + return None + try: + img = cv2.imdecode(np.fromfile(file_path, dtype=np.uint8), cv2.IMREAD_COLOR) + return img + except Exception as e: + logger.error(f"读取失败 {file_path}: {e}") + return None + +def cv2_imwrite_chinese(file_path: str, img: np.ndarray) -> bool: + try: + ext = os.path.splitext(file_path)[1] + if ext.lower() not in ['.jpg', '.jpeg', '.png', '.bmp']: + ext = '.jpg' + encoded_ok, encoded = cv2.imencode(ext, img) + if not encoded_ok: + logger.error(f"图像编码失败 {file_path}") + return False + encoded.tofile(file_path) + return True + except Exception as e: + logger.error(f"写入失败 {file_path}: {e}") + return False + +# ==================== YOLO 痤疮检测 ==================== +def detect_acne(img: np.ndarray, image_path: str, model: YOLO, output_path: str) -> Tuple[bool, Dict, str]: + try: + # 直接全图检测 + with MODEL_INFERENCE_LOCK: + results = model(img, conf=0.2, verbose=False) + + annotated_img = img.copy() + class_counts = {} + + for result in results: + if result.boxes is not None: + for box in result.boxes: + cls_id = int(box.cls[0]) + class_name = model.names[cls_id] + x1, y1, x2, y2 = map(int, box.xyxy[0]) + conf = float(box.conf[0]) + + color = (0, 0, 255) + cv2.rectangle(annotated_img, (x1, y1), (x2, y2), color, 2) + label = f"{class_name} {conf:.2f}" + (text_width, text_height), baseline = cv2.getTextSize( + label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 2 + ) + cv2.rectangle(annotated_img, (x1, y1 - text_height - baseline), + (x1 + text_width, y1), color, -1) + cv2.putText(annotated_img, label, (x1, y1 - baseline), + cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2) + + class_counts[class_name] = class_counts.get(class_name, 0) + 1 + + if cv2_imwrite_chinese(output_path, annotated_img): + logger.debug(f"YOLO 完成:{os.path.basename(output_path)}") + return True, class_counts, "" + else: + return False, {}, "写入失败" + + except Exception as e: + logger.error(f"YOLO 失败:{e}") + return False, {}, str(e) + +def build_lut(anchors: np.ndarray) -> np.ndarray: + """从锚点构建自定义 256 级颜色映射 LUT""" + lut = np.zeros((256, 3), dtype=np.uint8) + for c in range(3): + lut[:, c] = np.interp(np.arange(256), anchors[:, 0], anchors[:, c + 1]) + return lut.reshape(256, 1, 3) + + +def detect_skin_region(img: np.ndarray): + """多色彩空间皮肤检测,返回 (skin_soft, skin_binary, a_float)""" + lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB) + _, a_ch, _ = cv2.split(lab) + a_float = a_ch.astype(np.float32) + + mask_lab = cv2.inRange(lab, (40, 120, 105), (235, 185, 175)) + ycrcb = cv2.cvtColor(img, cv2.COLOR_BGR2YCrCb) + mask_ycrcb = cv2.inRange(ycrcb, (40, 133, 77), (240, 175, 135)) + skin_mask = cv2.bitwise_or(mask_lab, mask_ycrcb) + + kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (11, 11)) + skin_mask = cv2.morphologyEx(skin_mask, cv2.MORPH_CLOSE, kernel, iterations=3) + skin_mask = cv2.morphologyEx(skin_mask, cv2.MORPH_OPEN, kernel, iterations=1) + skin_mask = cv2.dilate(skin_mask, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7)), iterations=1) + + contours, _ = cv2.findContours(skin_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + if contours: + largest = max(contours, key=cv2.contourArea) + skin_mask = np.zeros_like(skin_mask) + cv2.drawContours(skin_mask, [largest], -1, 255, -1) + hull = cv2.convexHull(largest) + cv2.drawContours(skin_mask, [hull], -1, 255, -1) + + skin_soft = cv2.GaussianBlur(skin_mask.astype(np.float32), (21, 21), 0) / 255.0 + return skin_soft, skin_mask, a_float + + +def compute_heatmap_idx(a_float: np.ndarray, skin_mask: np.ndarray, + baseline_percentile: float, sensitivity: float) -> np.ndarray: + """计算红度热力图索引(0-255)""" + skin_pixels = a_float[skin_mask > 128] + if len(skin_pixels) > 100: + baseline = np.percentile(skin_pixels, baseline_percentile) + else: + baseline = np.median(a_float) + redness = np.maximum(0, a_float - baseline) * sensitivity + vmax = np.percentile(redness, 98) + if vmax > 0: + idx = np.clip(redness, 0, vmax) / vmax * 255.0 + else: + idx = np.zeros_like(a_float) + return np.clip(idx, 0, 255).astype(np.uint8) + + +def add_dark_details(result: np.ndarray, img: np.ndarray, + intensity: float = 0.50) -> np.ndarray: + """叠加原图暗部细节(眉毛/头发/眼睛),保持五官可见""" + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + dark = (255.0 - gray.astype(np.float32)) / 255.0 + dark = np.clip(dark * intensity, 0, 1) + dark = cv2.GaussianBlur(dark, (5, 5), 0) + return (result.astype(np.float32) * (1 - np.dstack([dark] * 3))).astype(np.uint8) + + +# ==================== 大毛孔检测 ==================== +def detect_pore_haar_face(gray: np.ndarray): + if not hasattr(cv2, "CascadeClassifier"): + return None + cascade_names = [ + "haarcascade_frontalface_default.xml", + "haarcascade_frontalface_alt2.xml", + "haarcascade_profileface.xml", + ] + for name in cascade_names: + cascade = cv2.CascadeClassifier(cv2.data.haarcascades + name) + faces = cascade.detectMultiScale( + gray, + scaleFactor=1.05, + minNeighbors=3, + minSize=(120, 120), + ) + if len(faces) > 0: + x, y, w, h = max(faces, key=lambda rect: rect[2] * rect[3]) + margin = int(w * 0.18) + return dlib.rectangle( + max(0, x - margin), + max(0, y - margin), + min(gray.shape[1], x + w + margin), + min(gray.shape[0], y + h + margin), + ) + return None + + +def detect_pore_landmarks(img: np.ndarray, detector, predictor): + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + faces = detector(gray, 1) + if not faces: + # 侧脸在常规上采样下可能漏检,仅在失败时提高一次精度。 + faces = detector(gray, 2) + if faces: + face = max(faces, key=lambda rect: rect.width() * rect.height()) + else: + face = detect_pore_haar_face(gray) + if face is None: + return None + shape = predictor(gray, face) + return np.array( + [(shape.part(index).x, shape.part(index).y) for index in range(68)], + dtype=np.int32, + ) + + +def build_pore_face_mask(img: np.ndarray, landmarks: np.ndarray, + forehead_ratio: float) -> np.ndarray: + jaw = landmarks[0:17] + brow = landmarks[17:27] + chin = landmarks[8] + brow_center_y = int(np.mean(brow[:, 1])) + face_height = max(1, chin[1] - brow_center_y) + top_y = max(0, brow_center_y - int(face_height * forehead_ratio)) + top_points = np.array([ + [landmarks[16][0], top_y], + [landmarks[27][0], top_y], + [landmarks[0][0], top_y], + ], dtype=np.int32) + face_poly = np.vstack([jaw, top_points]) + mask = np.zeros(img.shape[:2], np.uint8) + cv2.fillPoly(mask, [face_poly], 255, cv2.LINE_AA) + kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (31, 31)) + mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=2) + mask = cv2.GaussianBlur(mask, (11, 11), 0) + _, mask = cv2.threshold(mask, 20, 255, cv2.THRESH_BINARY) + return mask + + +def build_pore_exclusion_mask(img: np.ndarray, + landmarks: np.ndarray) -> np.ndarray: + exclude = np.zeros(img.shape[:2], np.uint8) + regions = [ + landmarks[17:22], + landmarks[22:27], + landmarks[36:42], + landmarks[42:48], + landmarks[48:60], + landmarks[[33, 48, 54, 8, 6, 10]], + ] + scales = [1.8, 1.8, 2.0, 2.0, 1.7, 1.4] + for points, scale in zip(regions, scales): + center = points.mean(axis=0) + expanded = center + (points - center) * scale + hull = cv2.convexHull(expanded.astype(np.int32)) + cv2.fillConvexPoly(exclude, hull, 255, cv2.LINE_AA) + + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + dark = (gray < 55).astype(np.uint8) * 255 + dark_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (17, 17)) + dark = cv2.dilate(dark, dark_kernel, iterations=1) + exclude = cv2.bitwise_or(exclude, dark) + exclude_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (9, 9)) + return cv2.dilate(exclude, exclude_kernel, iterations=1) + + +def pore_catmull_rom(points: np.ndarray, num: int = 12) -> np.ndarray: + points = np.array(points, dtype=np.float64) + count = len(points) + result = [] + for index in range(count): + p0 = points[(index - 1) % count] + p1 = points[index] + p2 = points[(index + 1) % count] + p3 = points[(index + 2) % count] + for t in np.linspace(0, 1, num, endpoint=False): + t2 = t * t + t3 = t2 * t + x = 0.5 * ( + 2 * p1[0] + (-p0[0] + p2[0]) * t + + (2 * p0[0] - 5 * p1[0] + 4 * p2[0] - p3[0]) * t2 + + (-p0[0] + 3 * p1[0] - 3 * p2[0] + p3[0]) * t3 + ) + y = 0.5 * ( + 2 * p1[1] + (-p0[1] + p2[1]) * t + + (2 * p0[1] - 5 * p1[1] + 4 * p2[1] - p3[1]) * t2 + + (-p0[1] + 3 * p1[1] - 3 * p2[1] + p3[1]) * t3 + ) + result.append((x, y)) + return np.array(result, dtype=np.int32) + + +def build_pore_roi_mask(img: np.ndarray, + landmarks: np.ndarray) -> np.ndarray: + height, width = img.shape[:2] + roi = np.zeros((height, width), np.uint8) + nose_bottom_y = max(landmarks[31][1], landmarks[35][1]) + right_34 = landmarks[34].copy() + right_34[1] = min(right_34[1], nose_bottom_y) + right_32 = landmarks[32].copy() + right_32[1] = min(right_32[1], nose_bottom_y) + right_41 = landmarks[41].copy() + right_41[1] += 5 + right_46 = landmarks[46].copy() + right_46[1] += 5 + + point_35 = landmarks[35] + point_14 = landmarks[14] + point_31 = landmarks[31] + point_2 = landmarks[2] + cheek_right = np.array([ + int(point_35[0] * 0.6 + point_14[0] * 0.4), + int(point_35[1] * 0.6 + point_14[1] * 0.4) + 12, + ], dtype=np.int32) + cheek_left = np.array([ + int(point_31[0] * 0.6 + point_2[0] * 0.4), + int(point_31[1] * 0.6 + point_2[1] * 0.4) + 12, + ], dtype=np.int32) + contour = np.array([ + landmarks[2], right_41, landmarks[28], right_46, landmarks[14], + cheek_right, landmarks[35], right_34, landmarks[30], right_32, + landmarks[31], cheek_left, + ], dtype=np.int32) + cv2.fillPoly(roi, [pore_catmull_rom(contour)], 255, cv2.LINE_AA) + + eye_bottom_y = int(np.max(landmarks[36:48, 1])) + 5 + roi[:eye_bottom_y, :] = 0 + roi = cv2.GaussianBlur(roi, (9, 9), 0) + _, roi = cv2.threshold(roi, 20, 255, cv2.THRESH_BINARY) + return roi + + +def build_pore_skin_mask(img: np.ndarray, face_mask: np.ndarray, + exclude_mask: np.ndarray, + roi_mask: np.ndarray) -> np.ndarray: + lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB) + ycrcb = cv2.cvtColor(img, cv2.COLOR_BGR2YCrCb) + skin = cv2.bitwise_or( + cv2.inRange(lab, (35, 118, 105), (245, 188, 178)), + cv2.inRange(ycrcb, (35, 130, 75), (245, 180, 140)), + ) + skin = cv2.bitwise_and(skin, face_mask) + skin = cv2.bitwise_and(skin, cv2.bitwise_not(exclude_mask)) + skin = cv2.bitwise_and(skin, roi_mask) + kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (13, 13)) + skin = cv2.morphologyEx(skin, cv2.MORPH_CLOSE, kernel, iterations=2) + return cv2.morphologyEx(skin, cv2.MORPH_OPEN, kernel, iterations=1) + + +def detect_big_pores_dog(img: np.ndarray, skin_mask: np.ndarray, + min_radius: int, max_radius: int, + contrast_threshold: float, + circularity_threshold: float, + max_pores: int) -> List[Dict]: + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(12, 12)) + gray_eq = clahe.apply(gray) + candidates = [] + height, width = gray_eq.shape + + for radius in range(min_radius, max_radius + 1): + first = cv2.GaussianBlur(gray_eq, (0, 0), sigmaX=radius * 0.6) + second = cv2.GaussianBlur(gray_eq, (0, 0), sigmaX=radius * 1.6) + dog = second.astype(np.float32) - first.astype(np.float32) + local_max = dog == cv2.dilate( + dog, + np.ones((radius * 2 + 1, radius * 2 + 1), np.uint8), + ) + valid = local_max & (skin_mask > 0) & (dog >= contrast_threshold) + + ys, xs = np.where(valid) + for y, x in zip(ys, xs): + patch_radius = min( + radius + 2, + min(x, y, width - 1 - x, height - 1 - y), + ) + if patch_radius < 2: + continue + patch = gray_eq[ + y - patch_radius:y + patch_radius + 1, + x - patch_radius:x + patch_radius + 1, + ] + if patch.shape[0] < 5 or patch.shape[1] < 5: + continue + + _, binary = cv2.threshold( + patch, + 0, + 255, + cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU, + ) + contours, _ = cv2.findContours( + binary, + cv2.RETR_EXTERNAL, + cv2.CHAIN_APPROX_SIMPLE, + ) + if not contours: + continue + contour = max(contours, key=cv2.contourArea) + area = cv2.contourArea(contour) + if area < 3 or area > np.pi * (radius + 2) ** 2: + continue + perimeter = cv2.arcLength(contour, True) + if perimeter < 1: + continue + circularity = 4 * np.pi * area / (perimeter * perimeter) + if circularity < circularity_threshold: + continue + + local_background = float(np.mean(gray_eq[ + max(0, y - radius * 2):y + radius * 2 + 1, + max(0, x - radius * 2):x + radius * 2 + 1, + ])) + real_contrast = local_background - float(gray_eq[y, x]) + if real_contrast < contrast_threshold * 0.6: + continue + candidates.append({ + "x": int(x), + "y": int(y), + "radius": radius, + "score": float(dog[y, x]), + "contrast": real_contrast, + "circularity": float(circularity), + "area": float(area), + }) + + candidates.sort(key=lambda candidate: -candidate["contrast"]) + used = np.zeros((height, width), bool) + selected = [] + for candidate in candidates: + if len(selected) >= max_pores: + break + x, y = candidate["x"], candidate["y"] + spacing = candidate["radius"] + 2 + y0, y1 = max(0, y - spacing), min(height, y + spacing + 1) + x0, x1 = max(0, x - spacing), min(width, x + spacing + 1) + if used[y0:y1, x0:x1].any(): + continue + selected.append(candidate) + used[y0:y1, x0:x1] = True + return selected + + +def draw_big_pores(img: np.ndarray, pores: List[Dict]) -> np.ndarray: + output = img.copy() + overlay = output.copy() + for pore in pores: + radius = 3 if pore["contrast"] > 22 else 2 + cv2.circle( + overlay, + (pore["x"], pore["y"]), + radius, + PORE_COLOR, + 1, + cv2.LINE_AA, + ) + return cv2.addWeighted(overlay, 0.85, output, 0.15, 0) + + +def load_pore_predictor(predictor_path: str): + """加载 dlib 模型,并兼容 Windows 下包含中文的绝对路径。""" + path = Path(predictor_path).resolve() + with PORE_MODEL_LOAD_LOCK: + try: + return dlib.shape_predictor(str(path)) + except RuntimeError: + if str(path).isascii() or not path.name.isascii(): + raise + + original_directory = Path.cwd() + try: + os.chdir(str(path.parent)) + return dlib.shape_predictor(path.name) + finally: + os.chdir(str(original_directory)) + + +class PoreAnalyzer: + """每个任务复用一次 dlib 模型,毛孔失败不影响原有分析阶段。""" + + def __init__(self, predictor_path: str): + if dlib is None: + raise RuntimeError(f"dlib 不可用:{DLIB_IMPORT_ERROR}") + if not Path(predictor_path).is_file(): + raise FileNotFoundError(f"缺少 dlib 关键点模型:{predictor_path}") + self.detector = dlib.get_frontal_face_detector() + self.predictor = load_pore_predictor(predictor_path) + self.landmark_lock = threading.Lock() + + def analyze(self, img: np.ndarray, pores_dir: Path, + stem: str) -> Tuple[bool, Dict, str]: + try: + with self.landmark_lock: + landmarks = detect_pore_landmarks( + img, + self.detector, + self.predictor, + ) + if landmarks is None: + return False, {}, "未检测到人脸" + + face_mask = build_pore_face_mask( + img, + landmarks, + PORE_FOREHEAD_RATIO, + ) + exclude_mask = build_pore_exclusion_mask(img, landmarks) + roi_mask = build_pore_roi_mask(img, landmarks) + skin_mask = build_pore_skin_mask( + img, + face_mask, + exclude_mask, + roi_mask, + ) + pores = detect_big_pores_dog( + img, + skin_mask, + PORE_MIN_RADIUS, + PORE_MAX_RADIUS, + PORE_CONTRAST_THRESHOLD, + PORE_CIRCULARITY_THRESHOLD, + PORE_MAX_COUNT, + ) + roi_area = int(np.sum(skin_mask > 0)) + average_size = ( + float(np.mean([pore["radius"] * 2 for pore in pores])) + if pores else 0.0 + ) + + annotated = draw_big_pores(img, pores) + outline_path = pores_dir / f"pore_outline_{stem}.jpg" + if not cv2_imwrite_chinese(str(outline_path), annotated): + return False, {}, "毛孔标注图写入失败" + + stats_path = pores_dir / f"pore_stats_{stem}.txt" + with open(stats_path, "w", encoding="utf-8") as file: + file.write(f"毛孔数量: {len(pores)}\n") + file.write(f"平均毛孔大小: {average_size:.1f} px(直径)\n") + file.write(f"ROI: {roi_area} px\n") + + stats = { + "pore_count": len(pores), + "pore_average_size": average_size, + "pore_roi_area": roi_area, + } + return True, stats, "" + except Exception as exc: + return False, {}, str(exc) + + +# ==================== RBX 红区分析 ==================== +def analyze_red_zone(img: np.ndarray, output_path: str) -> Tuple[bool, Dict, str]: + try: + # 直接全图皮肤检测 + lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB) + _, a_ch, _ = cv2.split(lab) + a_float = a_ch.astype(np.float32) + + mask_lab = cv2.inRange(lab, (40, 120, 105), (235, 185, 175)) + ycrcb = cv2.cvtColor(img, cv2.COLOR_BGR2YCrCb) + mask_ycrcb = cv2.inRange(ycrcb, (40, 133, 77), (240, 175, 135)) + skin_mask = cv2.bitwise_or(mask_lab, mask_ycrcb) + + kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (11, 11)) + skin_mask = cv2.morphologyEx(skin_mask, cv2.MORPH_CLOSE, kernel, iterations=3) + skin_mask = cv2.morphologyEx(skin_mask, cv2.MORPH_OPEN, kernel, iterations=1) + skin_mask = cv2.dilate(skin_mask, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7)), iterations=1) + + contours, _ = cv2.findContours(skin_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + if contours: + largest = max(contours, key=cv2.contourArea) + skin_mask = np.zeros_like(skin_mask) + cv2.drawContours(skin_mask, [largest], -1, 255, -1) + hull = cv2.convexHull(largest) + cv2.drawContours(skin_mask, [hull], -1, 255, -1) + + skin_soft = cv2.GaussianBlur(skin_mask.astype(np.float32), (31, 31), 0) / 255.0 + skin_pixels = a_float[skin_mask > 128] + if len(skin_pixels) > 100: + baseline_a = np.percentile(skin_pixels, 15) + else: + baseline_a = np.median(a_float) + + redness_map = np.maximum(0, a_float - baseline_a) * 1.8 + vmax = np.percentile(redness_map, 98) + if vmax > 0: + redness_map = np.clip(redness_map, 0, vmax) + heatmap_idx = (redness_map / vmax) * 255.0 + else: + heatmap_idx = np.zeros_like(a_float) + heatmap_idx = np.clip(heatmap_idx, 0, 255).astype(np.uint8) + heatmap_idx = cv2.bilateralFilter(heatmap_idx, 9, 75, 75) + heatmap_idx = cv2.GaussianBlur(heatmap_idx, (7, 7), 0) + + # 皮肤健康度统计 + skin_area = np.sum(skin_mask > 128) + if skin_area > 0: + within_skin = heatmap_idx[skin_mask > 128] + normal_pct = float(np.sum(within_skin < 55) / skin_area * 100) + mild_pct = float(np.sum((within_skin >= 55) & (within_skin < 110)) / skin_area * 100) + warning_pct = float(np.sum((within_skin >= 110) & (within_skin < 170)) / skin_area * 100) + severe_pct = float(np.sum(within_skin >= 170) / skin_area * 100) + health_score = float(normal_pct * 1.0 + mild_pct * 0.7 + warning_pct * 0.4 + severe_pct * 0.1) + else: + normal_pct = mild_pct = warning_pct = severe_pct = health_score = 0.0 + health_stats = { + 'health_score': health_score, 'normal_pct': normal_pct, + 'mild_pct': mild_pct, 'warning_pct': warning_pct, 'severe_pct': severe_pct, + } + + alpha_red = heatmap_idx.astype(np.float32) / 255.0 + alpha_red = np.clip(alpha_red * 1.5, 0, 1) + alpha_red = cv2.GaussianBlur(alpha_red, (5, 5), 0) + + anchors = np.array([ + [0, 232, 228, 242], + [3, 205, 170, 220], + [6, 150, 90, 215], + [10, 70, 30, 210], + [15, 35, 15, 225], + [20, 18, 14, 242], + [255, 2, 2, 97 ], + ], dtype=np.float32) + heatmap_color = cv2.applyColorMap(heatmap_idx, build_lut(anchors)) + heatmap_color = cv2.GaussianBlur(heatmap_color, (3, 3), 0) + + hsv = cv2.cvtColor(heatmap_color, cv2.COLOR_BGR2HSV).astype(np.float32) + hsv[:, :, 1] = np.clip(hsv[:, :, 1] * 1.5, 0, 255) + heatmap_color = cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR) + heatmap_float = heatmap_color.astype(np.float32) / 255.0 + + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY).astype(np.float32) + skin_texture = cv2.GaussianBlur(gray, (5, 5), 0) + skin_texture = np.clip(215 + skin_texture * 0.12, 215, 252) + skin_texture = np.dstack([skin_texture] * 3) / 255.0 + + alpha_3 = np.dstack([alpha_red] * 3) + result = skin_texture * (1 - alpha_3 * 0.8) + heatmap_float * (alpha_3 * 0.8) + + skin_alpha = np.dstack([skin_soft] * 3) + + highpass = gray - cv2.GaussianBlur(gray, (11, 11), 0) + highpass = np.clip(highpass * 0.08, -8, 8) + highpass = cv2.GaussianBlur(highpass, (3, 3), 0) + highpass_3 = np.dstack([highpass] * 3) / 255.0 + result = np.clip(result + highpass_3 * skin_alpha * 0.5, 0, 1) + + dark_mask = np.clip(1.0 - gray / 55.0, 0, 1) + dark_mask = cv2.GaussianBlur(dark_mask, (11, 11), 0) + dark_3 = np.dstack([dark_mask] * 3) + white = np.ones_like(heatmap_float) + result = result * (1 - dark_3) + white * dark_3 + + result = (result * 255).astype(np.uint8) + + if cv2_imwrite_chinese(output_path, result): + logger.debug(f"红区完成:{os.path.basename(output_path)}") + return True, health_stats, "" + return False, {}, "写入失败" + except Exception as e: + logger.error(f"红区失败:{e}") + return False, {}, str(e) + +# ==================== 人脸红区检测 ==================== +def analyze_face_red(img: np.ndarray, output_path: str) -> Tuple[bool, str]: + """交叉偏振人脸皮肤分析:红度 + 色素""" + try: + # 直接全图皮肤检测 + lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB) + l_ch, a_ch, b_ch = cv2.split(lab) + a_float = a_ch.astype(np.float32) + l_float = l_ch.astype(np.float32) + + mask_lab = cv2.inRange(lab, (40, 120, 105), (235, 185, 175)) + ycrcb = cv2.cvtColor(img, cv2.COLOR_BGR2YCrCb) + mask_ycrcb = cv2.inRange(ycrcb, (40, 133, 77), (240, 175, 135)) + skin = cv2.bitwise_or(mask_lab, mask_ycrcb) + k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (11, 11)) + skin = cv2.morphologyEx(skin, cv2.MORPH_CLOSE, k, iterations=3) + + contours, _ = cv2.findContours(skin, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + if contours: + skin[:] = 0 + cv2.drawContours(skin, [max(contours, key=cv2.contourArea)], -1, 255, -1) + + skin_soft = cv2.GaussianBlur(skin.astype(np.float32), (15, 15), 0) / 255.0 + + # 3. 红度分析 + skin_pixels = a_float[skin > 128] + baseline = np.percentile(skin_pixels, 20) if len(skin_pixels) > 100 else np.median(a_float) + redness = np.maximum(0, a_float - baseline) * 2.0 + vmax = np.percentile(redness[skin > 128], 97) if np.sum(skin > 128) > 100 else 0 + red_map = np.clip(redness / vmax * 255.0, 0, 255).astype(np.uint8) if vmax > 0 else np.zeros_like(a_float, dtype=np.uint8) + red_map = cv2.bilateralFilter(red_map, 7, 50, 50) + + # 4. 色素分析 + l_blur = cv2.GaussianBlur(l_float, (31, 31), 0) + pigment = np.abs(l_float - l_blur) + p_max = np.percentile(pigment[skin > 128], 95) if np.sum(skin > 128) > 100 else 0 + pig_map = np.clip(pigment / p_max * 255.0, 0, 255).astype(np.uint8) if p_max > 0 else np.zeros_like(l_float, dtype=np.uint8) + + # 5. 可视化:原图 + 红度(红) + 色素(蓝) + img_f = img.astype(np.float32) + skin_3 = np.dstack([skin_soft] * 3) + + red_alpha = np.clip(red_map.astype(np.float32) / 200.0, 0, 1) + red_alpha = cv2.GaussianBlur(red_alpha, (5, 5), 0) + red_ol = np.zeros_like(img) + red_ol[:, :, 2] = 180 + red_3 = np.dstack([red_alpha] * 3) + result = img_f * (1 - red_3 * 0.5) + red_ol.astype(np.float32) * (red_3 * 0.5) + + pig_alpha = np.clip(pig_map.astype(np.float32) / 200.0, 0, 1) + pig_alpha = cv2.GaussianBlur(pig_alpha, (5, 5), 0) + pig_ol = np.zeros_like(img) + pig_ol[:, :, 0] = 160 + pig_3 = np.dstack([pig_alpha] * 3) + result = result * (1 - pig_3 * 0.4) + pig_ol.astype(np.float32) * (pig_3 * 0.4) + + # 非皮肤压暗 + dark = img_f * 0.3 + 50 + result = result * skin_3 + dark * (1 - skin_3) + result = np.clip(result, 0, 255).astype(np.uint8) + + if cv2_imwrite_chinese(output_path, result): + logger.debug(f"交叉偏振分析完成:{os.path.basename(output_path)}") + return True, "" + return False, "写入失败" + except Exception as e: + logger.error(f"交叉偏振分析失败:{e}") + return False, str(e) + +# ==================== 热力敏感图分析 ==================== +def analyze_heat_sensitivity(img: np.ndarray, output_path: str) -> Tuple[bool, str]: + try: + # 直接全图皮肤检测 + skin_soft, skin_binary, a_float = detect_skin_region(img) + heatmap_idx = compute_heatmap_idx(a_float, skin_binary, 30, 1.8) + heatmap_idx = cv2.GaussianBlur(heatmap_idx, (7, 7), 0) + heatmap_idx = cv2.bilateralFilter(heatmap_idx, 7, 60, 60) + + anchors = np.array([ + [0, 192, 97, 16 ], + [1, 233, 241, 86 ], + [60, 8, 213, 204], + [100, 0, 250, 245], + [160, 0, 150, 255], + [245, 33, 38, 200], + [255, 4, 38, 152], + ], dtype=np.float32) + heatmap_color = cv2.applyColorMap(heatmap_idx, build_lut(anchors)) + heatmap_color = cv2.convertScaleAbs(heatmap_color, alpha=1.05, beta=0) + + result = (heatmap_color.astype(np.float32) * 0.92 + + np.full_like(img, 255, dtype=np.float32) * 0.08) + result = np.clip(result, 0, 255).astype(np.uint8) + + result = add_dark_details(result, img) + + if cv2_imwrite_chinese(output_path, result): + logger.debug(f"热力敏感图完成:{os.path.basename(output_path)}") + return True, "" + return False, "写入失败" + except Exception as e: + logger.error(f"热力敏感图失败:{e}") + return False, str(e) + +# ==================== UV 荧光分析 ==================== +def detect_skin_uv(img: np.ndarray): + """UV 照片皮肤检测 — 亮度阈值 + 形态学""" + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + + # UV 照片: 皮肤有微弱的 UV 反射/荧光, 背景几乎全黑 + _, skin = cv2.threshold(gray, 18, 255, cv2.THRESH_BINARY) + + kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (11, 11)) + skin = cv2.morphologyEx(skin, cv2.MORPH_CLOSE, kernel, iterations=3) + skin = cv2.morphologyEx(skin, cv2.MORPH_OPEN, kernel, iterations=1) + skin = cv2.dilate(skin, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7)), iterations=1) + + contours, _ = cv2.findContours(skin, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + if contours: + largest = max(contours, key=cv2.contourArea) + skin = np.zeros_like(skin) + cv2.drawContours(skin, [largest], -1, 255, -1) + hull = cv2.convexHull(largest) + cv2.drawContours(skin, [hull], -1, 255, -1) + + skin_soft = cv2.GaussianBlur(skin.astype(np.float32), (21, 21), 0) / 255.0 + return skin_soft, skin + + +def analyze_pigment(img: np.ndarray, output_path: str) -> Tuple[bool, str]: + """UV 荧光分析 — 黑底 + 皮肤灰度 + 荧光绿点""" + try: + skin_soft, skin_binary = detect_skin_uv(img) + + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY).astype(np.float32) + clahe = cv2.createCLAHE(clipLimit=2.5, tileGridSize=(16, 16)) + skin_base = clahe.apply(gray.astype(np.uint8)).astype(np.float32) + + skin_base = skin_base * skin_soft + skin_base = np.power(skin_base / 255.0, 1.2) * 255.0 + + skin_base_3 = np.dstack([skin_base / 255.0] * 3) + result_u8 = (np.clip(skin_base_3, 0, 1) * 255).astype(np.uint8) + + if cv2_imwrite_chinese(output_path, result_u8): + logger.debug(f"UV 荧光完成:{os.path.basename(output_path)}") + return True, "" + else: + return False, "写入失败" + + except Exception as e: + logger.error(f"UV 荧光失败:{e}") + return False, str(e) + +def analyze_pores_for_image(result: Dict, img: np.ndarray, + output_base: Path, + pore_analyzer: Optional[PoreAnalyzer]) -> None: + """执行单张图片的毛孔分析,并将结果写回统一结果对象。""" + if pore_analyzer is None: + result['pore_status'] = 'error' + result['pore_error'] = '毛孔检测未初始化' + return + + try: + success, pore_stats, error = pore_analyzer.analyze( + img, + output_base / "pores", + result['stem'], + ) + if success: + result.update(pore_stats) + result['pore_status'] = 'success' + logger.info( + f"{result['filename']} 毛孔完成:" + f"{result['pore_count']} 个" + ) + else: + result['pore_status'] = 'error' + result['pore_error'] = error + logger.warning(f"{result['filename']} 毛孔检测失败:{error}") + except Exception as exc: + result['pore_status'] = 'error' + result['pore_error'] = str(exc) + logger.exception(f"{result['filename']} 毛孔检测异常") + + +# ==================== 单张图片处理 ==================== +def process_single_image(args: Tuple) -> Dict: + if len(args) == 4: + img_path, model, output_base, verbose = args + pore_analyzer = None + task_type = None + elif len(args) == 5: + img_path, model, output_base, verbose, pore_analyzer = args + task_type = None + else: + img_path, model, output_base, verbose, pore_analyzer, task_type = args + + pore_only = task_type == 'pore' + + if img_path.suffix.lower() not in IMAGE_EXTENSIONS: + return {'status': 'skip', 'filename': img_path.name} + + logger.info(f"处理:{img_path.name}") + + img = cv2_imread_chinese(str(img_path)) + if img is None: + return {'status': 'error', 'filename': img_path.name, 'error': '读取失败'} + + img = cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE) + + result = { + 'status': 'success', + 'filename': img_path.name, + 'stem': img_path.stem, + 'acne_counts': {}, + 'health_score': 0, + 'normal_pct': 0, + 'mild_pct': 0, + 'warning_pct': 0, + 'severe_pct': 0, + 'errors': [], + 'pore_status': 'disabled' if pore_analyzer is None else 'pending', + 'pore_count': 0, + 'pore_average_size': 0, + 'pore_roi_area': 0, + 'pore_error': None, + } + completed_stages = 0 + + if pore_only: + analyze_pores_for_image(result, img, output_base, pore_analyzer) + if result['pore_status'] == 'success': + result['status'] = 'success' + else: + result['status'] = 'error' + logger.error( + f"{img_path.name} 毛孔处理失败:{result['pore_error']}" + ) + return result + + # 1. YOLO 痤疮检测 + try: + yolo_out = output_base / "yolo" / f"yolo_{img_path.stem}.jpg" + success, acne_counts, error = detect_acne(img, str(img_path), model, str(yolo_out)) + if success: + result['acne_counts'] = acne_counts + completed_stages += 1 + else: + result['errors'].append(f"YOLO: {error}") + except Exception as e: + result['errors'].append(f"YOLO: {e}") + + # 3. RBX 红区分析 + try: + red_out = output_base / "redzone" / f"red_{img_path.stem}.jpg" + success, health_stats, error = analyze_red_zone(img, str(red_out)) + if success: + result.update(health_stats) + completed_stages += 1 + else: + result['errors'].append(f"红区:{error}") + except Exception as e: + result['errors'].append(f"红区:{e}") + + # 4. 人脸红区检测 + try: + face_red_out = output_base / "face_red" / f"face_red_{img_path.stem}.jpg" + success, error = analyze_face_red(img, str(face_red_out)) + if success: + completed_stages += 1 + else: + result['errors'].append(f"人脸红区:{error}") + except Exception as e: + result['errors'].append(f"人脸红区:{e}") + + # 6. 热力敏感图分析 + try: + sensitivity_out = output_base / "sensitivity" / f"sensitivity_{img_path.stem}.jpg" + success, error = analyze_heat_sensitivity(img, str(sensitivity_out)) + if success: + completed_stages += 1 + else: + result['errors'].append(f"热力敏感图:{error}") + except Exception as e: + result['errors'].append(f"热力敏感图:{e}") + + # 7. 色素铅图分析 + try: + pig_out = output_base / "pigment" / f"pigment_{img_path.stem}.jpg" + success, error = analyze_pigment(img, str(pig_out)) + if success: + completed_stages += 1 + else: + result['errors'].append(f"色素:{error}") + except Exception as e: + result['errors'].append(f"色素:{e}") + + # 8. 大毛孔检测是附加阶段,不改变原五项分析的成功状态。 + if pore_analyzer is not None: + analyze_pores_for_image(result, img, output_base, pore_analyzer) + + if result['errors']: + if completed_stages > 0: + result['status'] = 'partial' + logger.warning(f"{img_path.name} 部分失败:{result['errors']}") + else: + result['status'] = 'error' + logger.error(f"{img_path.name} 处理失败:{result['errors']}") + else: + logger.info(f"{img_path.name} 完成") + + return result + +# ==================== 保存文本报告 ==================== +def save_text_report(results: List[Dict], texts_dir: Path) -> bool: + try: + # 确保texts目录存在 + if not texts_dir.exists(): + texts_dir.mkdir(parents=True, exist_ok=True) + + text_path = texts_dir / "analysis_report.txt" + + valid = [r for r in results if r.get('health_score', 0) > 0] + if valid: + avg_health = np.mean([r['health_score'] for r in valid]) + avg_normal = np.mean([r['normal_pct'] for r in valid]) + avg_mild = np.mean([r['mild_pct'] for r in valid]) + avg_warning = np.mean([r['warning_pct'] for r in valid]) + avg_severe = np.mean([r['severe_pct'] for r in valid]) + else: + avg_health = avg_normal = avg_mild = avg_warning = avg_severe = 0.0 + + with open(text_path, 'w', encoding='utf-8') as f: + f.write(f"Health Score::{avg_health:.1f} / 100;;\n") + f.write(f"Normal area::{avg_normal:.1f}%;;\n") + f.write(f"Mild abnormality::{avg_mild:.1f}%;;\n") + f.write(f"Moderate abnormality::{avg_warning:.1f}%;;\n") + f.write(f"Severe abnormality::{avg_severe:.1f}%;;\n") + + logger.info(f"文本报告已保存:{text_path}") + return True + + except Exception as e: + logger.error(f"文本报告生成失败:{e}") + return False + +# ==================== 回调后端接口 ==================== +def callback_api(task_id: str, status: int, api_url: str = API_HOST+API_CALLBACK): + """完成后回调后端接口""" + try: + payload = { + "task_id": task_id, + "status": status # 1=成功,0=失败 + } + response = requests.post(api_url, json=payload, timeout=10) + if response.status_code == 200: + logger.info(f"回调成功:{task_id}") + return True + else: + logger.warning(f"回调失败:{response.status_code, api_url}") + return False + except Exception as e: + logger.error(f"回调异常:{e}") + return False + + +def resolve_input_dir(input_dir: Path, analysis_root: Optional[str] = None) -> Path: + """解析输入目录;API 调用时额外限制在共享分析目录内。""" + if analysis_root is None: + return input_dir + + resolved = input_dir.resolve() + root = Path(analysis_root).resolve() + try: + resolved.relative_to(root) + except ValueError as exc: + raise ValueError(f"输入目录必须位于 {root} 内") from exc + if resolved == root: + raise ValueError("输入目录不能是分析数据根目录本身") + return resolved + +# ==================== 主函数 ==================== +def main() -> int: + global logger + + parser = argparse.ArgumentParser(description='皮肤分析工具 - 后端 API 版') + parser.add_argument('--task-id', type=str, + help='任务 ID') + parser.add_argument('--task-type', choices=['pore'], default=None, + help='任务类型,pore 表示仅执行大毛孔检测') + parser.add_argument('input_dir', type=str, + help='输入目录完整路径(后端传入的 folder_name)') + parser.add_argument('--model', '-m', type=str, default='best.pt', + help='YOLO 痤疮模型路径 (默认:cuochuang.pt)') + parser.add_argument('--workers', '-w', type=int, default=3, + help='并发工作线程数 (默认:3)') + parser.add_argument('--pore-predictor', type=str, + default=PORE_PREDICTOR_PATH, + help='dlib 68 点人脸关键点模型路径') + parser.add_argument('--disable-pores', action='store_true', + help='关闭附加的大毛孔检测') + parser.add_argument('--analysis-root', type=str, + help='限制输入目录所在的分析数据根目录(API 调用时使用)') + parser.add_argument('--verbose', '-v', action='store_true', + help='详细输出模式') + parser.add_argument('--api-url', type=str, default=API_HOST+API_CALLBACK, + help='回调 API 地址') + args = parser.parse_args() + pore_only = args.task_type == 'pore' + + # 初始化日志 + logger = setup_logging(args.verbose) + + # 输入输出路径 + raw_input_dir = Path(args.input_dir) + try: + input_dir = resolve_input_dir(raw_input_dir, args.analysis_root) + except ValueError as e: + task_id = args.task_id or raw_input_dir.name + logger.error(f"输入目录校验失败:{e}") + callback_api(task_id, status=0, api_url=args.api_url) + return 1 + task_id = args.task_id or input_dir.name + output_base = input_dir.parent / 'output' + + logger.info("=" * 60) + logger.info("皮肤分析工具 - 后端 API 版") + logger.info("=" * 60) + logger.info(f"任务 ID: {task_id}") + logger.info(f"输入目录:{input_dir}") + logger.info(f"输出目录:{output_base}") + logger.info(f"模型路径:{args.model}") + logger.info(f"并发线程:{args.workers}") + logger.info(f"任务类型:{args.task_type or '完整分析'}") + logger.info( + f"毛孔检测:{'开启' if pore_only or not args.disable_pores else '关闭'}" + ) + if pore_only or not args.disable_pores: + logger.info(f"毛孔关键点模型:{args.pore_predictor}") + logger.info(f"回调地址:{args.api_url}") + + model_path = args.model + + if not input_dir.is_dir(): + logger.error(f"输入目录不存在或不是目录:{input_dir}") + callback_api(task_id, status=0, api_url=args.api_url) + return 1 + + if args.workers < 1: + logger.error(f"并发线程数必须大于 0:{args.workers}") + callback_api(task_id, status=0, api_url=args.api_url) + return 1 + + # 创建输出目录 + texts_dir = output_base / 'texts' + + try: + # 首先创建输出目录 + output_base.mkdir(exist_ok=True, parents=True) + logger.info(f"输出目录创建成功:{output_base}") + + # 毛孔专用任务不创建其他分析阶段的目录。 + subdirs = ( + ['pores'] + if pore_only + else ['yolo', 'redzone', 'face_red', 'sensitivity', 'pigment', 'texts'] + ) + for subdir in subdirs: + subdir_path = output_base / subdir + subdir_path.mkdir(exist_ok=True) + logger.info(f"子目录创建成功:{subdir_path}") + except Exception as e: + logger.error(f"目录创建失败:{e}") + callback_api(task_id, status=0, api_url=args.api_url) + return 1 + + # 完整分析中毛孔是附加能力;毛孔专用任务则要求初始化成功。 + pore_analyzer = None + if pore_only or not args.disable_pores: + try: + pores_dir = output_base / 'pores' + pores_dir.mkdir(exist_ok=True) + pore_analyzer = PoreAnalyzer(args.pore_predictor) + logger.info("大毛孔检测模型加载成功") + except Exception as e: + if pore_only: + logger.error(f"大毛孔检测模型加载失败:{e}") + callback_api(task_id, status=0, api_url=args.api_url) + return 1 + logger.warning(f"大毛孔检测已跳过:{e}") + + model = None + if not pore_only: + # 加载痤疮模型 + logger.info("加载 YOLO 痤疮模型...") + try: + model = YOLO(model_path) + logger.info("痤疮模型加载成功") + except Exception as e: + logger.error(f"痤疮模型加载失败:{e}") + callback_api(task_id, status=0, api_url=args.api_url) + return 1 + + # 获取图片列表 + try: + directory_entries = list(input_dir.iterdir()) + if pore_only: + files_by_name = { + path.name.lower(): path + for path in directory_entries + if path.is_file() + } + image_files = [ + files_by_name[name] + for name in PORE_IMAGE_NAMES + if name in files_by_name + ] + missing_images = [ + name for name in PORE_IMAGE_NAMES + if name not in files_by_name + ] + if missing_images: + logger.warning( + f"毛孔任务缺少固定图片:{', '.join(missing_images)}" + ) + else: + image_files = directory_entries + total_images = len(image_files) + logger.info(f"发现 {total_images} 个待处理文件") + except Exception as e: + logger.error(f"获取图片列表失败:{e}") + callback_api(task_id, status=0, api_url=args.api_url) + return 1 + + # 并发处理 + results = [] + tasks = [(img_path, model, output_base, args.verbose, pore_analyzer, args.task_type) + for img_path in image_files] + + logger.info("开始并发处理...") + try: + with ThreadPoolExecutor(max_workers=args.workers) as executor: + futures = {executor.submit(process_single_image, task): task + for task in tasks} + + for future in tqdm(as_completed(futures), total=len(futures), + desc="处理进度", unit="图"): + result = future.result() + results.append(result) + except Exception as e: + logger.error(f"并发处理失败:{e}") + callback_api(task_id, status=0, api_url=args.api_url) + return 1 + + if not pore_only: + # 保存文本报告 + logger.info("\n保存文本报告中...") + logger.info(f"文本目录:{texts_dir}") + if not save_text_report(results, texts_dir): + callback_api(task_id, status=0, api_url=args.api_url) + return 1 + logger.info("文本报告保存成功") + + # 统计总结 + success = sum(1 for r in results if r['status'] == 'success') + partial = sum(1 for r in results if r['status'] == 'partial') + errors = sum(1 for r in results if r['status'] == 'error') + skipped = sum(1 for r in results if r['status'] == 'skip') + + logger.info("\n" + "=" * 60) + logger.info("处理结束") + logger.info(f"成功:{success} | 部分:{partial} | 失败:{errors} | 跳过:{skipped}") + logger.info(f"输出目录:{output_base}") + if not pore_only: + logger.info(f"文本报告:{texts_dir / 'analysis_report.txt'}") + logger.info("=" * 60) + + # 回调后端接口(状态 1=成功) + status = 1 if (success + partial) > 0 else 0 + if status == 0: + logger.error("没有任何图片成功完成分析") + callback_api(task_id, status=status, api_url=args.api_url) + return 0 if status == 1 else 1 + +if __name__ == "__main__": + raise SystemExit(main())