Skip to content

Commit 8ee6363

Browse files
committed
face preprocessing finished
1 parent d56d8aa commit 8ee6363

2 files changed

Lines changed: 249 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# QZP
22
/data/
3+
/tmp/
34
# /src/_ivfpq/
45
# /src/**/_*
56

vec_data_load/bupt_cbface.py

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
"""
2+
BUPT-CBFace embedding loader (resumable).
3+
4+
按 `landmark.tsv` 的顺序逐行读取 (NAME + 5-point landmarks),对齐到 112x112,
5+
用 ArcFace 模型抽取 512-d embedding,并保存到 DuckDB(每个数据集一个库,放在各自目录下)。
6+
7+
为什么用 DuckDB:顺序处理过程中如果中断/报错,重新运行会自动从上次已写入的
8+
最后一行继续。
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import argparse
14+
import csv
15+
import os
16+
from itertools import islice
17+
from pathlib import Path
18+
from typing import Iterable, Optional, Sequence, Tuple
19+
20+
import cv2
21+
import duckdb
22+
import numpy as np
23+
24+
try:
25+
from tqdm import tqdm
26+
except Exception: # pragma: no cover
27+
tqdm = None
28+
29+
from insightface.model_zoo import get_model
30+
from insightface.utils import face_align
31+
32+
33+
EMBEDDING_DIM = 512
34+
TABLE_NAME = "bupt_cbface_embeddings"
35+
36+
37+
def _count_data_rows(tsv_path: Path) -> int:
38+
with tsv_path.open("r", encoding="utf-8", errors="replace") as f:
39+
line_count = sum(1 for _ in f)
40+
return max(0, line_count - 1) # minus header
41+
42+
43+
def _parse_name(name: str) -> Tuple[str, Optional[int]]:
44+
parts = name.split("/")
45+
if len(parts) != 2:
46+
return name, None
47+
person = parts[0]
48+
try:
49+
idx = int(parts[1])
50+
except ValueError:
51+
idx = None
52+
return person, idx
53+
54+
55+
def _parse_pts(row: dict) -> np.ndarray:
56+
pts = np.array(
57+
[
58+
[float(row["PTX1"]), float(row["PTY1"])],
59+
[float(row["PTX2"]), float(row["PTY2"])],
60+
[float(row["PTX3"]), float(row["PTY3"])],
61+
[float(row["PTX4"]), float(row["PTY4"])],
62+
[float(row["PTX5"]), float(row["PTY5"])],
63+
],
64+
dtype=np.float32,
65+
)
66+
if pts.shape != (5, 2):
67+
raise ValueError(f"Invalid landmark shape: {pts.shape}")
68+
return pts
69+
70+
71+
def _compute_embedding(
72+
rec,
73+
img_path: Path,
74+
pts: np.ndarray,
75+
image_size: int,
76+
) -> np.ndarray:
77+
img = cv2.imread(str(img_path))
78+
if img is None:
79+
raise FileNotFoundError(f"Failed to read image: {img_path}")
80+
81+
aligned = face_align.norm_crop(img, landmark=pts, image_size=image_size)
82+
emb = rec.get_feat(aligned).flatten()
83+
if emb.shape[0] != EMBEDDING_DIM:
84+
raise ValueError(f"Unexpected embedding dim: got {emb.shape[0]}, want {EMBEDDING_DIM}")
85+
emb = emb / (np.linalg.norm(emb) + 1e-12)
86+
return emb
87+
88+
89+
def _ensure_schema(conn: duckdb.DuckDBPyConnection) -> None:
90+
conn.execute(
91+
f"""
92+
CREATE TABLE IF NOT EXISTS {TABLE_NAME} (
93+
dataset TEXT,
94+
row_id BIGINT,
95+
name TEXT,
96+
person TEXT,
97+
img_idx INTEGER,
98+
vec FLOAT[{EMBEDDING_DIM}],
99+
PRIMARY KEY (dataset, row_id)
100+
);
101+
"""
102+
)
103+
104+
105+
def _last_row_id(conn: duckdb.DuckDBPyConnection, dataset: str) -> int:
106+
row = conn.execute(
107+
f"SELECT COALESCE(MAX(row_id), 0) FROM {TABLE_NAME} WHERE dataset=?;",
108+
[dataset],
109+
).fetchone()
110+
return int(row[0]) if row else 0
111+
112+
113+
def _iter_tsv_rows(tsv_path: Path, start_row_id: int, limit: Optional[int]) -> Iterable[Tuple[int, dict]]:
114+
with tsv_path.open("r", encoding="utf-8", errors="replace", newline="") as f:
115+
reader = csv.DictReader(f, delimiter="\t")
116+
117+
if start_row_id <= 1:
118+
it = reader
119+
row_id_start = 1
120+
else:
121+
it = islice(reader, start_row_id - 1, None)
122+
row_id_start = start_row_id
123+
124+
if limit is not None:
125+
it = islice(it, 0, limit)
126+
127+
for offset, row in enumerate(it, start=row_id_start):
128+
yield offset, row
129+
130+
131+
def process_dataset(
132+
conn: duckdb.DuckDBPyConnection,
133+
dataset_dir: Path,
134+
rec,
135+
image_size: int,
136+
limit: Optional[int],
137+
) -> None:
138+
dataset_dir = dataset_dir.resolve()
139+
dataset = dataset_dir.name
140+
tsv_path = dataset_dir / "landmark.tsv"
141+
images_dir = dataset_dir / "images"
142+
143+
if not tsv_path.exists():
144+
raise FileNotFoundError(f"[{dataset}] missing landmark.tsv: {tsv_path}")
145+
if not images_dir.exists():
146+
raise FileNotFoundError(f"[{dataset}] missing images/: {images_dir}")
147+
148+
total_rows = _count_data_rows(tsv_path)
149+
last_row = _last_row_id(conn, dataset)
150+
start_row = last_row + 1
151+
152+
if last_row > total_rows:
153+
raise ValueError(
154+
f"[{dataset}] db last_row={last_row} > tsv rows={total_rows}, "
155+
"dataset/DB可能不匹配 (建议换 db_path 或清空表)"
156+
)
157+
158+
remaining = max(0, total_rows - last_row)
159+
if limit is not None:
160+
remaining = min(remaining, limit)
161+
162+
rows_iter = _iter_tsv_rows(tsv_path, start_row_id=start_row, limit=limit)
163+
if tqdm is not None:
164+
rows_iter = tqdm(rows_iter, total=remaining, desc=f"{dataset}", unit="img")
165+
166+
for row_id, row in rows_iter:
167+
name = row["NAME"]
168+
img_path = images_dir / f"{name}.jpg"
169+
person, img_idx = _parse_name(name)
170+
pts = _parse_pts(row)
171+
emb = _compute_embedding(rec, img_path=img_path, pts=pts, image_size=image_size)
172+
173+
conn.execute(
174+
f"""
175+
INSERT INTO {TABLE_NAME} (dataset, row_id, name, person, img_idx, vec)
176+
VALUES (?, ?, ?, ?, ?, ?)
177+
ON CONFLICT DO NOTHING;
178+
""",
179+
[dataset, row_id, name, person, img_idx, emb.tolist()],
180+
)
181+
182+
183+
def _default_data_dirs() -> Sequence[str]:
184+
candidates = ["data/BUPT-CBFace-12", "data/BUPT-CBFace-50"]
185+
return [p for p in candidates if Path(p).exists()]
186+
187+
188+
def main(argv: Optional[Sequence[str]] = None) -> int:
189+
parser = argparse.ArgumentParser(description="BUPT-CBFace landmark.tsv -> ArcFace embeddings -> DuckDB")
190+
parser.add_argument(
191+
"--data-dirs",
192+
nargs="+",
193+
default=_default_data_dirs(),
194+
help="数据目录列表 (每个目录需包含 landmark.tsv + images/)",
195+
)
196+
parser.add_argument(
197+
"--db-name",
198+
default="arcface_embeddings.duckdb",
199+
help="每个数据目录下生成的 DuckDB 文件名 (用于断点续跑)",
200+
)
201+
parser.add_argument(
202+
"--model-path",
203+
default=os.path.expanduser("~/.insightface/models/buffalo_l/w600k_r50.onnx"),
204+
help="ArcFace ONNX 模型路径 (默认 buffalo_l/w600k_r50.onnx)",
205+
)
206+
parser.add_argument(
207+
"--providers",
208+
nargs="+",
209+
default=["CPUExecutionProvider"],
210+
help="onnxruntime providers (例如 CPUExecutionProvider 或 CUDAExecutionProvider)",
211+
)
212+
parser.add_argument("--ctx-id", type=int, default=0, help="insightface ctx_id (CPU 时通常无所谓)")
213+
parser.add_argument("--image-size", type=int, default=112, help="对齐输出尺寸 (ArcFace 通常 112)")
214+
parser.add_argument(
215+
"--limit",
216+
type=int,
217+
default=None,
218+
help="每个数据集最多处理多少行 (调试用,默认处理到文件结尾)",
219+
)
220+
221+
args = parser.parse_args(argv)
222+
223+
if not args.data_dirs:
224+
raise SystemExit("No data dirs found. 请传 --data-dirs ... 或确保 data/ 下有 BUPT-CBFace-12/50")
225+
226+
rec = get_model(args.model_path, providers=args.providers)
227+
rec.prepare(ctx_id=args.ctx_id)
228+
229+
for data_dir in args.data_dirs:
230+
dataset_dir = Path(data_dir).resolve()
231+
db_path = dataset_dir / args.db_name
232+
233+
conn = duckdb.connect(str(db_path))
234+
_ensure_schema(conn)
235+
process_dataset(
236+
conn=conn,
237+
dataset_dir=dataset_dir,
238+
rec=rec,
239+
image_size=args.image_size,
240+
limit=args.limit,
241+
)
242+
conn.close()
243+
244+
return 0
245+
246+
247+
if __name__ == "__main__":
248+
raise SystemExit(main())

0 commit comments

Comments
 (0)