ofiqpy 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ofiqpy/__init__.py ADDED
@@ -0,0 +1,61 @@
1
+ """ofiqpy — a faithful Python port of OFIQ v1.1.0 (ISO/IEC 29794-5 face image quality).
2
+
3
+ Reuses OFIQ's own models and reproduces its exact algorithms, matching the reference
4
+ within +/- 1 quality point per component. Point ofiqpy at an OFIQ install's data/ dir:
5
+
6
+ export OFIQPY_OFIQ_DATA=/path/to/OFIQ-Project/data
7
+
8
+ Then:
9
+
10
+ from ofiqpy import assess
11
+ scores = assess("face.jpg") # {component_name: (raw, scalar)}
12
+ """
13
+ from __future__ import annotations
14
+
15
+ __version__ = "0.1.0"
16
+
17
+ _PIPE = None
18
+ _MEAS = None
19
+
20
+
21
+ def _lazy():
22
+ """Build (once) the shared pipeline + measures. Models load on first use."""
23
+ global _PIPE, _MEAS
24
+ if _PIPE is None:
25
+ from .config import OFIQConfig
26
+ from .measures.core import Measures
27
+ from .pipeline import OFIQPipeline
28
+ cfg = OFIQConfig()
29
+ _PIPE = OFIQPipeline(cfg)
30
+ _MEAS = Measures(cfg)
31
+ return _PIPE, _MEAS
32
+
33
+
34
+ def assess(image: "str | object") -> dict:
35
+ """Assess one image and return ``{component_name: (raw, scalar)}``.
36
+
37
+ Args:
38
+ image: path to an image file (str or PathLike), or a BGR uint8 numpy
39
+ array of shape (H, W, 3).
40
+
41
+ Returns:
42
+ Mapping of OFIQ component name to ``(raw native value, 0-100 scalar)``. Empty
43
+ if no face is detected.
44
+ """
45
+ import numpy as np
46
+
47
+ pipe, meas = _lazy()
48
+ if isinstance(image, np.ndarray):
49
+ bgr = image
50
+ else:
51
+ import cv2
52
+ bgr = cv2.imread(str(image))
53
+ if bgr is None:
54
+ raise FileNotFoundError(f"could not read image: {image}")
55
+ session = pipe.process(bgr)
56
+ if session.bbox is None:
57
+ return {}
58
+ return meas.compute(session)
59
+
60
+
61
+ __all__ = ["assess", "__version__"]
ofiqpy/align.py ADDED
@@ -0,0 +1,109 @@
1
+ """Alignment (616x616), landmarked-region mask, tmetric, luminance.
2
+
3
+ Faithful port of utils.cpp:236-331, FaceMeasures.cpp:98-228, image_utils.cpp:43-112.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import math
8
+
9
+ import cv2
10
+ import numpy as np
11
+
12
+ # ADNet FaceMap indices (adnet_FaceMap.h)
13
+ LEFT_EYE_CORNERS = (60, 64)
14
+ RIGHT_EYE_CORNERS = (68, 72)
15
+ NOSE = 54
16
+ RIGHT_MOUTH = 76
17
+ LEFT_MOUTH = 82
18
+ CHIN = 16
19
+
20
+ REF_POINTS = np.float32([
21
+ [251, 272], [364, 272], [308, 336], [262, 402], [355, 402],
22
+ ])
23
+
24
+
25
+ def _round(x):
26
+ return math.copysign(math.floor(abs(x) + 0.5), x)
27
+
28
+
29
+ def eye_centers(P: np.ndarray):
30
+ """Midpoints of eye-corner pairs, each coord rounded (utils.cpp:302-317)."""
31
+ l = (_round(P[LEFT_EYE_CORNERS[0], 0] + 0.5 * (P[LEFT_EYE_CORNERS[1], 0] - P[LEFT_EYE_CORNERS[0], 0])),
32
+ _round(P[LEFT_EYE_CORNERS[0], 1] + 0.5 * (P[LEFT_EYE_CORNERS[1], 1] - P[LEFT_EYE_CORNERS[0], 1])))
33
+ r = (_round(P[RIGHT_EYE_CORNERS[0], 0] + 0.5 * (P[RIGHT_EYE_CORNERS[1], 0] - P[RIGHT_EYE_CORNERS[0], 0])),
34
+ _round(P[RIGHT_EYE_CORNERS[0], 1] + 0.5 * (P[RIGHT_EYE_CORNERS[1], 1] - P[RIGHT_EYE_CORNERS[0], 1])))
35
+ return l, r
36
+
37
+
38
+ def align(img: np.ndarray, P: np.ndarray):
39
+ """Return (aligned 616x616 BGR, aligned_landmarks (98,2), affine 2x3)."""
40
+ Lc, Rc = eye_centers(P)
41
+ src = np.float32([
42
+ [Lc[0], Lc[1]], [Rc[0], Rc[1]],
43
+ [P[NOSE, 0], P[NOSE, 1]],
44
+ [P[RIGHT_MOUTH, 0], P[RIGHT_MOUTH, 1]],
45
+ [P[LEFT_MOUTH, 0], P[LEFT_MOUTH, 1]],
46
+ ])
47
+ M, _ = cv2.estimateAffinePartial2D(src, REF_POINTS, method=cv2.LMEDS)
48
+ aligned = cv2.warpAffine(img, M, (616, 616)) # default INTER_LINEAR, BORDER_CONSTANT 0
49
+ ap = cv2.transform(P.reshape(-1, 1, 2).astype(np.float32), M).reshape(-1, 2)
50
+ aligned_lms = np.array([[_round(x), _round(y)] for x, y in ap], np.float64)
51
+ return aligned, aligned_lms, M
52
+
53
+
54
+ def tmetric(P: np.ndarray) -> float:
55
+ """||chin - eye_midpoint|| (utils.cpp:319-331). Space-agnostic; pass the right landmarks."""
56
+ Lc, Rc = eye_centers(P)
57
+ eye_mid = ((Lc[0] + Rc[0]) / 2.0, (Lc[1] + Rc[1]) / 2.0)
58
+ chin = P[CHIN]
59
+ return float(math.hypot(chin[0] - eye_mid[0], chin[1] - eye_mid[1]))
60
+
61
+
62
+ def landmarked_region(aligned_lms: np.ndarray, height=616, width=616, alpha=0.0) -> np.ndarray:
63
+ """GetFaceMask at alpha=0 (FaceMeasures.cpp:173-226): convex hull -> 224 -> resize."""
64
+ pts = aligned_lms.astype(np.int32)
65
+ hull = cv2.convexHull(pts)
66
+ rx, ry, rw, rh = cv2.boundingRect(hull)
67
+ b = int(ry - rh * 0.05)
68
+ d = int(ry + rh * 1.05)
69
+ a = int(rx + rw / 2.0 - (d - b) / 2.0)
70
+ c = int(rx + rw / 2.0 + (d - b) / 2.0)
71
+ S = 224
72
+ hull_s = ((hull.reshape(-1, 2).astype(np.float64) - (a, b)) / (d - b) * S).astype(np.int32)
73
+ mask = np.zeros((S, S), np.uint8)
74
+ cv2.fillConvexPoly(mask, hull_s, 1)
75
+ mrs = cv2.resize(mask, (c - a, d - b), interpolation=cv2.INTER_NEAREST)
76
+ region = np.zeros((height, width), np.uint8)
77
+ left, top = 0, 0
78
+ right, bottom = mrs.shape[1], mrs.shape[0]
79
+ an, bn, cn, dn = a, b, c, d
80
+ if a < 0:
81
+ left = -a
82
+ an = 0
83
+ if c > width:
84
+ right = mrs.shape[1] - (c - width)
85
+ cn = width
86
+ if b < 0:
87
+ top = -b
88
+ bn = 0
89
+ if d > height:
90
+ bottom = mrs.shape[0] - (d - height)
91
+ dn = height
92
+ region[bn:dn, an:cn] = mrs[top:bottom, left:right]
93
+ return region
94
+
95
+
96
+ # --- luminance (image_utils.cpp:43-112) ---
97
+ _SRGB_LUT = np.array([
98
+ ((v / 255.0) / 12.92) if (v / 255.0) <= 0.04045 else (((v / 255.0) + 0.055) / 1.055) ** 2.4
99
+ for v in range(256)
100
+ ], np.float64)
101
+
102
+
103
+ def luminance(img_bgr: np.ndarray) -> np.ndarray:
104
+ """sRGB-linearized Rec.709 luma -> floor(y*255+0.5) uint8 (BGR input)."""
105
+ B = _SRGB_LUT[img_bgr[:, :, 0]]
106
+ G = _SRGB_LUT[img_bgr[:, :, 1]]
107
+ R = _SRGB_LUT[img_bgr[:, :, 2]]
108
+ y = 0.2126 * R + 0.7152 * G + 0.0722 * B
109
+ return np.floor(y * 255.0 + 0.5).astype(np.uint8)
ofiqpy/batch.py ADDED
@@ -0,0 +1,102 @@
1
+ """Parallel batch runner — assess a directory of images to an OFIQ-format CSV.
2
+
3
+ Each worker process builds its own pipeline (ONNX/cv2.ml models are not shared
4
+ across processes). Resumable: rows already present in the output CSV are skipped.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import argparse
9
+ import csv
10
+ import time
11
+ from concurrent.futures import ProcessPoolExecutor
12
+ from pathlib import Path
13
+
14
+ import cv2
15
+
16
+ from .output import header, row
17
+
18
+ EXTS = {".jpg", ".jpeg", ".png", ".bmp"}
19
+
20
+ # per-worker singletons
21
+ _PIPE = None
22
+ _MEAS = None
23
+
24
+
25
+ def _init_worker():
26
+ global _PIPE, _MEAS
27
+ from .config import OFIQConfig
28
+ from .measures.core import Measures
29
+ from .pipeline import OFIQPipeline
30
+ cfg = OFIQConfig()
31
+ _PIPE = OFIQPipeline(cfg)
32
+ _MEAS = Measures(cfg)
33
+
34
+
35
+ def _assess_one(path_str: str) -> str:
36
+ t0 = time.time()
37
+ bgr = cv2.imread(path_str)
38
+ try:
39
+ s = _PIPE.process(bgr)
40
+ res = _MEAS.compute(s) if s.bbox is not None else {}
41
+ except Exception:
42
+ res = {}
43
+ return row(path_str, res, (time.time() - t0) * 1000)
44
+
45
+
46
+ def discover(input_path: Path) -> list[Path]:
47
+ p = Path(input_path)
48
+ if p.is_file():
49
+ return [p]
50
+ return sorted(f for f in p.rglob("*") if f.suffix.lower() in EXTS)
51
+
52
+
53
+ def _already_done(out_csv: Path) -> set[str]:
54
+ if not out_csv.exists():
55
+ return set()
56
+ done = set()
57
+ with open(out_csv, newline="") as f:
58
+ r = csv.reader(f, delimiter=";")
59
+ next(r, None) # header
60
+ for line in r:
61
+ if line:
62
+ done.add(line[0])
63
+ return done
64
+
65
+
66
+ def run_batch(input_path, output_csv, workers=None, resume=False, progress=True):
67
+ images = discover(Path(input_path))
68
+ out = Path(output_csv)
69
+ done = _already_done(out) if resume else set()
70
+ todo = [im for im in images if im.name not in done]
71
+
72
+ write_header = not (resume and out.exists())
73
+ mode = "a" if (resume and out.exists()) else "w"
74
+ n_done = 0
75
+ t0 = time.time()
76
+ with open(out, mode) as fh, ProcessPoolExecutor(max_workers=workers,
77
+ initializer=_init_worker) as ex:
78
+ if write_header:
79
+ fh.write(header() + "\n")
80
+ for line in ex.map(_assess_one, [str(im) for im in todo], chunksize=1):
81
+ fh.write(line + "\n")
82
+ fh.flush()
83
+ n_done += 1
84
+ if progress and n_done % 25 == 0:
85
+ rate = n_done / max(time.time() - t0, 1e-6)
86
+ print(f" {n_done}/{len(todo)} {rate:.1f} img/s", flush=True)
87
+ print(f"done: {n_done} images -> {out} ({len(done)} pre-existing skipped)")
88
+ return out
89
+
90
+
91
+ def main():
92
+ ap = argparse.ArgumentParser(description="ofiqpy parallel batch runner")
93
+ ap.add_argument("-i", "--input", required=True, help="image directory (recursive) or file")
94
+ ap.add_argument("-o", "--output", required=True, help="output CSV")
95
+ ap.add_argument("-w", "--workers", type=int, default=None, help="worker processes (default: CPUs)")
96
+ ap.add_argument("--resume", action="store_true", help="skip images already in the output CSV")
97
+ args = ap.parse_args()
98
+ run_batch(args.input, args.output, args.workers, args.resume)
99
+
100
+
101
+ if __name__ == "__main__":
102
+ main()
ofiqpy/cli.py ADDED
@@ -0,0 +1,47 @@
1
+ """ofiqpy CLI — OFIQSampleApp-compatible: -i <file|dir> -o <out.csv>."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import time
6
+ from pathlib import Path
7
+
8
+ import cv2
9
+
10
+ from .config import OFIQConfig
11
+ from .measures.core import Measures
12
+ from .output import row, write_csv
13
+ from .pipeline import OFIQPipeline
14
+
15
+ EXTS = {".jpg", ".jpeg", ".png", ".bmp"}
16
+
17
+
18
+ def main():
19
+ ap = argparse.ArgumentParser(description="ofiqpy — faithful OFIQ Python port")
20
+ ap.add_argument("-i", "--input", required=True, help="image file or directory")
21
+ ap.add_argument("-o", "--output", required=True, help="output CSV")
22
+ args = ap.parse_args()
23
+
24
+ inp = Path(args.input)
25
+ imgs = ([inp] if inp.is_file()
26
+ else sorted(p for p in inp.rglob("*") if p.suffix.lower() in EXTS))
27
+
28
+ cfg = OFIQConfig()
29
+ pipe = OFIQPipeline(cfg)
30
+ meas = Measures(cfg)
31
+
32
+ rows = []
33
+ for img in imgs:
34
+ t0 = time.time()
35
+ bgr = cv2.imread(str(img))
36
+ try:
37
+ s = pipe.process(bgr)
38
+ res = meas.compute(s) if s.bbox is not None else {}
39
+ except Exception:
40
+ res = {}
41
+ rows.append(row(str(img), res, (time.time() - t0) * 1000))
42
+ write_csv(args.output, rows)
43
+ print(f"wrote {args.output} ({len(rows)} images)")
44
+
45
+
46
+ if __name__ == "__main__":
47
+ main()
ofiqpy/config.py ADDED
@@ -0,0 +1,101 @@
1
+ """OFIQ config loader — parses ofiq_config.jaxn and resolves model paths.
2
+
3
+ Faithful port of OFIQ v1.1.0. Model files and config are reused directly from the
4
+ reference OFIQ-Project checkout so the port loads the *same* weights OFIQ uses.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import os
10
+ from pathlib import Path
11
+
12
+ # OFIQ's models + config are reused directly (they are separately licensed and NOT bundled).
13
+ # Point ofiqpy at an OFIQ checkout's data/ directory via the OFIQPY_OFIQ_DATA env var, e.g.:
14
+ # export OFIQPY_OFIQ_DATA=/path/to/OFIQ-Project/data
15
+ # Falls back to ./OFIQ-Project/data (relative to the current directory) if unset.
16
+ _DEFAULT_DATA = Path("OFIQ-Project/data")
17
+ OFIQ_DATA = Path(os.environ.get("OFIQPY_OFIQ_DATA", _DEFAULT_DATA))
18
+ OFIQ_CONFIG = OFIQ_DATA / "ofiq_config.jaxn"
19
+
20
+
21
+ def _strip_jaxn(text: str) -> str:
22
+ """Strip // and /* */ comments and trailing commas from JAXN, respecting strings."""
23
+ out = []
24
+ i, n = 0, len(text)
25
+ in_str = False
26
+ while i < n:
27
+ c = text[i]
28
+ if in_str:
29
+ out.append(c)
30
+ if c == "\\" and i + 1 < n: # escape
31
+ out.append(text[i + 1])
32
+ i += 2
33
+ continue
34
+ if c == '"':
35
+ in_str = False
36
+ i += 1
37
+ continue
38
+ if c == '"':
39
+ in_str = True
40
+ out.append(c)
41
+ i += 1
42
+ continue
43
+ if c == "/" and i + 1 < n and text[i + 1] == "/": # line comment
44
+ while i < n and text[i] != "\n":
45
+ i += 1
46
+ continue
47
+ if c == "/" and i + 1 < n and text[i + 1] == "*": # block comment
48
+ i += 2
49
+ while i + 1 < n and not (text[i] == "*" and text[i + 1] == "/"):
50
+ i += 1
51
+ i += 2
52
+ continue
53
+ out.append(c)
54
+ i += 1
55
+ s = "".join(out)
56
+ # remove trailing commas before } or ]
57
+ import re
58
+
59
+ s = re.sub(r",(\s*[}\]])", r"\1", s)
60
+ return s
61
+
62
+
63
+ def load_config(path: Path = OFIQ_CONFIG) -> dict:
64
+ raw = Path(path).read_text()
65
+ data = json.loads(_strip_jaxn(raw))
66
+ return data["config"]
67
+
68
+
69
+ class OFIQConfig:
70
+ """Parsed OFIQ config with model-path resolution against the OFIQ data root."""
71
+
72
+ def __init__(self, path: Path = OFIQ_CONFIG, data_root: Path = OFIQ_DATA):
73
+ self.cfg = load_config(path)
74
+ self.data_root = Path(data_root)
75
+ self.params = self.cfg["params"]
76
+
77
+ def resolve(self, rel_path: str) -> Path:
78
+ p = self.data_root / rel_path
79
+ if not p.exists():
80
+ raise FileNotFoundError(f"OFIQ model not found: {p}")
81
+ return p
82
+
83
+ def measure(self, name: str) -> dict:
84
+ return self.params["measures"].get(name, {})
85
+
86
+ def sigmoid_params(self, measure: str) -> dict:
87
+ return self.measure(measure).get("Sigmoid", {})
88
+
89
+ def detector(self) -> dict:
90
+ return self.params["detector"]["ssd"]
91
+
92
+ def landmarks_model(self) -> Path:
93
+ return self.resolve(self.params["landmarks"]["ADNet"]["model_path"])
94
+
95
+
96
+ if __name__ == "__main__":
97
+ c = OFIQConfig()
98
+ print("detector:", c.detector())
99
+ print("ADNet:", c.landmarks_model())
100
+ print("UnifiedQualityScore sigmoid:", c.sigmoid_params("UnifiedQualityScore"))
101
+ print("measures listed:", len(c.cfg["measures"]))
File without changes
@@ -0,0 +1,48 @@
1
+ """SSD face detector — faithful port of opencv_ssd_face_detector.cpp.
2
+
3
+ Caffe model via OpenCV DNN (NOT onnxruntime): 300x300, BGR mean (104,117,123),
4
+ pad 20%, conf>=0.4, min rel face size 0.05, primary = largest area.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import math
9
+
10
+ import cv2
11
+ import numpy as np
12
+
13
+
14
+ def _round(x: float) -> int:
15
+ return int(math.copysign(math.floor(abs(x) + 0.5), x))
16
+
17
+
18
+ class SSDDetector:
19
+ def __init__(self, caffemodel, prototxt, confidence_thr=0.4,
20
+ min_rel_face_size=0.05, padding=0.2):
21
+ self.net = cv2.dnn.readNetFromCaffe(str(prototxt), str(caffemodel))
22
+ self.thr = confidence_thr
23
+ self.min_rel = min_rel_face_size
24
+ self.pad = padding
25
+
26
+ def detect(self, img: np.ndarray) -> list[tuple[int, int, int, int]]:
27
+ """Return face boxes [(left, top, w, h), ...] in ORIGINAL px, largest first."""
28
+ H, W = img.shape[:2]
29
+ padH = int(W * self.pad) # static_cast truncation
30
+ padV = int(H * self.pad)
31
+ padded = cv2.copyMakeBorder(img, padV, padV, padH, padH,
32
+ cv2.BORDER_CONSTANT, value=(0, 0, 0))
33
+ Ph, Pw = padded.shape[:2]
34
+ blob = cv2.dnn.blobFromImage(padded, 1.0, (300, 300), (104, 117, 123),
35
+ swapRB=False, crop=False)
36
+ self.net.setInput(blob)
37
+ det = self.net.forward() # (1,1,N,7)
38
+ faces = []
39
+ for row in det[0, 0]:
40
+ conf, l, t, r, b = float(row[2]), float(row[3]), float(row[4]), float(row[5]), float(row[6])
41
+ if conf >= self.thr and l > 0 and t > 0 and r < 1 and b < 1 and (r - l) > self.min_rel:
42
+ left = _round(l * Pw) - padH
43
+ top = _round(t * Ph) - padV
44
+ width = _round((r - l) * Pw)
45
+ height = _round((b - t) * Ph)
46
+ faces.append((left, top, width, height))
47
+ faces.sort(key=lambda f: f[2] * f[3], reverse=True)
48
+ return faces
File without changes
@@ -0,0 +1,84 @@
1
+ """ADNet 98-point landmarks — faithful port of adnet_landmarks.cpp + utils.cpp squaring.
2
+
3
+ ONNX input 'input' [1,3,256,256] BGR planes, norm 2p/255-1; output '3209' [1,98,2];
4
+ denorm (v+1)/2*255; back-map to original px via square-box scale.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import math
9
+
10
+ import cv2
11
+ import numpy as np
12
+ import onnxruntime as ort
13
+
14
+
15
+ def _floor(x):
16
+ return int(math.floor(x))
17
+
18
+
19
+ def _ceil(x):
20
+ return int(math.ceil(x))
21
+
22
+
23
+ def _round(x):
24
+ return int(math.copysign(math.floor(abs(x) + 0.5), x))
25
+
26
+
27
+ def make_square_bbox(left, top, w, h):
28
+ """utils.cpp:109-167 — extend the shorter side symmetrically to a square."""
29
+ xtl, ytl, xbr, ybr = left, top, left + w, top + h
30
+ if w < h:
31
+ xl = _floor((xbr + xtl - h) / 2.0)
32
+ xr = _ceil((xbr + xtl + h) / 2.0)
33
+ return xl, ytl, xr - xl, ybr - ytl # (x, y, side, side)
34
+ else:
35
+ yt = _floor((ybr + ytl - w) / 2.0)
36
+ yb = _ceil((ybr + ytl + w) / 2.0)
37
+ return xtl, yt, xbr - xtl, yb - yt
38
+
39
+
40
+ def make_square_with_padding(sq, img):
41
+ """utils.cpp:50-107 — pad image if the square box escapes bounds."""
42
+ x, y, sw, sh = sq
43
+ H, W = img.shape[:2]
44
+ xbr, ybr = x + sw, y + sh
45
+ top = max(0, -y)
46
+ bottom = max(0, ybr - H + 1)
47
+ left = max(0, -x)
48
+ right = max(0, xbr - W + 1)
49
+ if top or bottom or left or right:
50
+ padded = cv2.copyMakeBorder(img, top, bottom, left, right,
51
+ cv2.BORDER_CONSTANT, value=(0, 0, 0))
52
+ else:
53
+ padded = img
54
+ tx, ty = left, top
55
+ sq_pad = (x + tx, y + ty, sw, sh)
56
+ return padded, sq_pad, (tx, ty)
57
+
58
+
59
+ class ADNetLandmarker:
60
+ def __init__(self, onnx_path):
61
+ self.sess = ort.InferenceSession(str(onnx_path), providers=["CPUExecutionProvider"])
62
+ self.out_name = "3209"
63
+
64
+ def extract(self, img: np.ndarray, primary_box) -> np.ndarray:
65
+ """img BGR uint8, primary_box=(left,top,w,h). Returns (98,2) original-px coords."""
66
+ sq = make_square_bbox(*primary_box)
67
+ padded, sq_pad, (tx, ty) = make_square_with_padding(sq, img)
68
+ x, y, sw, sh = sq_pad
69
+ crop = padded[y:y + sh, x:x + sw]
70
+ if crop.shape[:2] != (256, 256):
71
+ crop = cv2.resize(crop, (256, 256), interpolation=cv2.INTER_LINEAR)
72
+ arr = crop.astype(np.float32) * (2.0 / 255.0) - 1.0 # [-1,1], BGR planes
73
+ blob = np.transpose(arr, (2, 0, 1))[None] # (1,3,256,256)
74
+ out = self.sess.run([self.out_name], {"input": blob})[0].reshape(-1)
75
+ out = (out + 1.0) / 2.0 * 255.0 # denorm to 256-space
76
+ scale = sq[3] / 256.0 # OFIQ uses square HEIGHT/256 (adnet_landmarks.cpp:313),
77
+ # not width — the floor/ceil square can be 1px non-square.
78
+ ox = sq_pad[0] - tx # original square xleft
79
+ oy = sq_pad[1] - ty
80
+ pts = np.empty((98, 2), np.float64)
81
+ for i in range(98):
82
+ pts[i, 0] = _round(out[2 * i] * scale + ox)
83
+ pts[i, 1] = _round(out[2 * i + 1] * scale + oy)
84
+ return pts
File without changes