Repository navigation
Expand file tree
/
Copy pathapp.py
More file actions
235 lines (198 loc) · 7.95 KB
/
Copy pathapp.py
File metadata and controls
235 lines (198 loc) · 7.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
import argparse
import sqlite3
import time
from datetime import datetime, timezone
from pathlib import Path
import cv2
import numpy as np
BASE_DIR = Path(__file__).parent
DATA_DIR = BASE_DIR / "data"
FACES_DIR = DATA_DIR / "faces"
DB_PATH = DATA_DIR / "faces.db"
CAMERA_INDEX = 0
SAMPLES_PER_PERSON = 20
LOG_INTERVAL_SECONDS = 2
def now_iso():
return datetime.now(timezone.utc).isoformat(timespec="seconds")
def open_database():
DATA_DIR.mkdir(exist_ok=True)
connection = sqlite3.connect(DB_PATH)
connection.execute(
"""
CREATE TABLE IF NOT EXISTS people (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
last_seen TEXT
)
"""
)
connection.execute(
"""
CREATE TABLE IF NOT EXISTS sightings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
person_id INTEGER NOT NULL,
seen_at TEXT NOT NULL,
confidence REAL NOT NULL,
FOREIGN KEY (person_id) REFERENCES people(id)
)
"""
)
connection.commit()
return connection
def detector():
filename = "haarcascade_frontalface_default.xml"
candidates = [
Path(cv2.data.haarcascades) / filename,
Path(cv2.__file__).parent / "data" / filename,
]
path = next((candidate for candidate in candidates if candidate.is_file()), None)
if path is None:
raise RuntimeError(
"OpenCV's face detector file is missing. Reinstall the pinned dependency with "
"`python -m pip install --force-reinstall -r requirements.txt`."
)
model = cv2.CascadeClassifier(str(path))
if model.empty():
raise RuntimeError(f"Could not load OpenCV's face detector from {path}.")
return model
def largest_face(faces):
return max(faces, key=lambda face: face[2] * face[3], default=None)
def enroll(name):
name = name.strip()
if not name:
raise ValueError("Name cannot be empty.")
connection = open_database()
try:
person = connection.execute(
"SELECT id FROM people WHERE name = ?", (name,)
).fetchone()
if person is None:
cursor = connection.execute("INSERT INTO people (name) VALUES (?)", (name,))
person_id = cursor.lastrowid
connection.commit()
else:
person_id = person[0]
finally:
connection.close()
person_dir = FACES_DIR / str(person_id)
person_dir.mkdir(parents=True, exist_ok=True)
camera = cv2.VideoCapture(CAMERA_INDEX)
if not camera.isOpened():
raise RuntimeError("Could not open the webcam.")
face_detector = detector()
saved = 0
last_capture = 0.0
print("Look at the camera. Press q to stop enrollment.")
try:
while saved < SAMPLES_PER_PERSON:
success, frame = camera.read()
if not success:
raise RuntimeError("Could not read a frame from the webcam.")
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
faces = face_detector.detectMultiScale(gray, 1.2, 5, minSize=(100, 100))
face = largest_face(faces)
if face is not None:
x, y, width, height = face
if time.monotonic() - last_capture >= 0.25:
crop = gray[y : y + height, x : x + width]
crop = cv2.resize(crop, (200, 200))
saved += 1
cv2.imwrite(str(person_dir / f"{saved:03d}.png"), crop)
last_capture = time.monotonic()
cv2.rectangle(frame, (x, y), (x + width, y + height), (0, 220, 0), 2)
cv2.imshow("Face enrollment", frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
finally:
camera.release()
cv2.destroyAllWindows()
print(f"Saved {saved} face samples for {name}.")
def load_model(connection):
recognizer = cv2.face.LBPHFaceRecognizer_create()
images = []
labels = []
people = {}
for person_id, name in connection.execute("SELECT id, name FROM people ORDER BY id"):
files = sorted((FACES_DIR / str(person_id)).glob("*.png"))
for file in files:
image = cv2.imread(str(file), cv2.IMREAD_GRAYSCALE)
if image is not None:
images.append(image)
labels.append(person_id)
people[person_id] = name
if not images:
raise RuntimeError("No enrolled faces found. Run `python app.py enroll --name NAME` first.")
recognizer.train(images, np.asarray(labels, dtype=np.int32))
return recognizer, people
def run_scanner():
connection = open_database()
try:
recognizer, people = load_model(connection)
face_detector = detector()
camera = cv2.VideoCapture(CAMERA_INDEX)
if not camera.isOpened():
raise RuntimeError("Could not open the webcam.")
print("Scanning. Press q to stop.")
last_logged = {}
try:
while True:
success, frame = camera.read()
if not success:
raise RuntimeError("Could not read a frame from the webcam.")
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
faces = face_detector.detectMultiScale(gray, 1.2, 5, minSize=(100, 100))
for x, y, width, height in faces:
crop = cv2.resize(gray[y : y + height, x : x + width], (200, 200))
person_id, confidence = recognizer.predict(crop)
known = person_id in people and confidence < 75
cv2.rectangle(frame, (x, y), (x + width, y + height), (0, 220, 0), 2)
if known and time.monotonic() - last_logged.get(person_id, 0) >= LOG_INTERVAL_SECONDS:
seen_at = now_iso()
connection.execute(
"INSERT INTO sightings (person_id, seen_at, confidence) VALUES (?, ?, ?)",
(person_id, seen_at, confidence),
)
connection.execute(
"UPDATE people SET last_seen = ? WHERE id = ?",
(seen_at, person_id),
)
connection.commit()
last_logged[person_id] = time.monotonic()
print(f"{seen_at}: {people[person_id]}")
cv2.imshow("Face scanner", frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
finally:
camera.release()
cv2.destroyAllWindows()
finally:
connection.close()
def list_people():
connection = open_database()
try:
rows = connection.execute(
"SELECT name, COALESCE(last_seen, 'Never') FROM people ORDER BY name"
).fetchall()
finally:
connection.close()
if not rows:
print("No enrolled people.")
return
for name, last_seen in rows:
print(f"{name}: {last_seen}")
def main():
parser = argparse.ArgumentParser(description="Local webcam face enrollment and recognition.")
commands = parser.add_subparsers(dest="command", required=True)
enroll_command = commands.add_parser("enroll", help="Enroll a person using the webcam.")
enroll_command.add_argument("--name", required=True, help="Name associated with the face.")
commands.add_parser("run", help="Start recognition and log sightings.")
commands.add_parser("list", help="Show enrolled people and their last-seen time.")
args = parser.parse_args()
if args.command == "enroll":
enroll(args.name)
elif args.command == "run":
run_scanner()
else:
list_people()
if __name__ == "__main__":
main()