Operations: Ad, Remove, Rename and Reencode People
Note: In this article, real people’s faces and names in illustrations have been intentionally obscured to protect privacy
In this series we developed a toolbox for managing people in photo collections based on automated face and body encoding & recognition. With it we built a Knowledge Base (KB) of known persons: a dataset of per-person folders plus their embeddings stored in encodings.pkl. We then created a PyQt GUI application as the framework to manage and use our Python code. Finally batch encoding (to initialize and extend the KB) and batch recognition (to sort unknown photos) were added as the primary operations.
Now we’ll make the KB editable in the app by adding the basic operation Add Person, Remove Person, Rename Person, and Re-encode Person.
We’ll touch just two of our source files.
Kb.py will hold the four operations and a tiny, generic worker.
Ui.py will get four new menu items, the corresponding small dialogs and a shared harness to initialize and finish the worker.
Each operation stays small because it delegates to our toolkit modules: face_encoder.py and reid_wrapper.py.
Design in one picture

This small diagram shows the processing flow. First a menu action in the GUI launches the shared worker. Then the worker runs the selected KB operation, calling the toolkit when needed. The GUI receives and shows logs and progress updates à on finish it shows a single summary.
Assumptions & settings
We use the existing settings keys for the KB, encoding model, thresholds, image formats, etc. from settings.json:
{
"dataset_dir": "D:/Coding/recognize/posts/app/persons_dataset",
"processed_dir": "D:/Coding/recognize/posts/persons_processed",
"process_dir": "D:/Coding/recognize/posts/persons_unknown",
"face_tol": 0.4,
"body_tol": 0.8,
"reid_model": "osnet_ain_x1_0",
"valid_exts": [".jpg", ".jpeg", ".png", ".webp"],
"encodings_filename": "encodings.pkl",
"resize_max": 800,
"lap_var_thresh": 80.0,
"kb_batch_size": 16
}
PythonAdd Two Small Helpers to KB.PY
Drop these two small helpers in kb.py directly below the imports.
# kb.py
from __future__ import annotations
from typing import Dict, Any, Iterable, Optional, List, Tuple
from collections import Counter
from pathlib import Path
import os, shutil, pickle
import numpy as np
from PIL import Image, ImageFile
from PyQt6 import QtCore
from PyQt6.QtCore import pyqtSignal
from face_encoder import detect_and_align_faces
# ----------------- KB layout -----------------
# KB = / (one subfolder per person)
# + /encodings.pkl
def _kb_load(kb_path: Path) -> dict:
if kb_path.exists():
return pickle.load(open(kb_path, "rb")) or {}
return {"face_encodings": [], "face_names": [], "body_encodings": [], "body_names": []}
def _kb_save(kb_path: Path, persons: dict) -> None:
pickle.dump(persons, open(kb_path, "wb"))
PythonAdd Four Person Operations plus Generic Worker to KB.PY
Drop this two tiny helpers, to call the toolkit, and the actual operations methods into kb.py, somewhere at the beginning of the file.
# use face encoder
def _encode_face(img) -> np.ndarray | None:
from face_encoder import detect_and_align_faces
chips = detect_and_align_faces(img, compute_embedding=True, embedding_model="small")
for r in chips or []:
emb = r.get("embedding")
if emb is not None and emb.size:
return np.asarray(emb, dtype=np.float32)
return None
# use body reid encoder
def _body_extractor(model_name: str, log_fn=None):
from reid_wrapper import TorchreidBodyExtractor
return TorchreidBodyExtractor(model_name=model_name, log_fn=log_fn)
# small utility to add a person from a folder of images-
# no interactive drag/drop, just process all images in the folder
def add_person_from_folder(main_dir: Path, kb_path: Path, reid_model: str,
name: str, src_folder: Path,
valid_exts=(".jpg",".jpeg",".png",".webp"),
log_fn=None, progress_fn=None) -> dict:
main_dir = Path(main_dir); (main_dir / name).mkdir(parents=True, exist_ok=True)
kb = _kb_load(kb_path)
files = sorted(p for p in src_folder.iterdir() if p.suffix.lower() in valid_exts)
total = len(files)
if total == 0:
return {"ok": False, "msg": "No images in source folder."}
reid = _body_extractor(reid_model, log_fn)
faces = bodies = 0
for i, p in enumerate(files, 1):
try:
with Image.open(p) as im:
img = im.convert("RGB")
# save/copy into KB dataset
dst = main_dir / name / p.name
if dst.exists():
stem, ext = dst.stem, dst.suffix; k=1
while (main_dir / name / f"{stem}_{k}{ext}").exists(): k += 1
dst = main_dir / name / f"{stem}_{k}{ext}"
img.save(dst)
# encodings
f = _encode_face(img)
if f is not None:
kb["face_encodings"].append(f); kb["face_names"].append(name); faces += 1
try:
e = reid(img).astype("float32")
kb["body_encodings"].append(e); kb["body_names"].append(name); bodies += 1
except Exception:
pass
if progress_fn: progress_fn(int(i*100/total))
if log_fn: log_fn(f"[Add] {name} — {p.name} face={'✓' if f is not None else '×'}")
except Exception as e:
if log_fn: log_fn(f"[Add] {p.name}: {e.__class__.__name__}")
_kb_save(kb_path, kb)
return {"ok": True, "msg": f"Added {name}", "faces": faces, "bodies": bodies}
# remove all data for a person
def remove_person(main_dir: Path, kb_path: Path, name: str) -> dict:
kb = _kb_load(kb_path)
# remove images folder if present
folder = Path(main_dir) / name
if folder.exists():
shutil.rmtree(folder, ignore_errors=True)
# drop encodings
keep_f = [(v,n) for v,n in zip(kb["face_encodings"], kb["face_names"]) if n != name]
kb["face_encodings"] = [v for v,_ in keep_f]; kb["face_names"] = [n for _,n in keep_f]
keep_b = [(v,n) for v,n in zip(kb["body_encodings"], kb["body_names"]) if n != name]
kb["body_encodings"] = [v for v,_ in keep_b]; kb["body_names"] = [n for _,n in keep_b]
_kb_save(kb_path, kb)
return {"ok": True, "msg": f"Removed {name}"}
# rename a person (folder + all encodings)
def rename_person(main_dir: Path, kb_path: Path, old: str, new: str) -> dict:
if not new or new == old:
return {"ok": False, "msg": "New name must differ."}
src, dst = Path(main_dir)/old, Path(main_dir)/new
if not src.exists(): return {"ok": False, "msg": f"Folder '{old}' not found."}
if dst.exists(): return {"ok": False, "msg": f"Target '{new}' exists."}
shutil.move(str(src), str(dst))
kb = _kb_load(kb_path)
kb["face_names"] = [new if n==old else n for n in kb["face_names"]]
kb["body_names"] = [new if n==old else n for n in kb["body_names"]]
_kb_save(kb_path, kb)
return {"ok": True, "msg": f"Renamed {old} → {new}"}
# re-encode all images for a person (after changing reid/face model, or improving image quality)
def reencode_person(main_dir: Path, kb_path: Path, reid_model: str,
name: str, valid_exts=(".jpg",".jpeg",".png",".webp"),
log_fn=None, progress_fn=None) -> dict:
kb = _kb_load(kb_path)
# clear old entries for this person
kb["face_encodings"] = [v for v,n in zip(kb["face_encodings"], kb["face_names"]) if n != name]
kb["face_names"] = [n for n in kb["face_names"] if n != name]
kb["body_encodings"] = [v for v,n in zip(kb["body_encodings"], kb["body_names"]) if n != name]
kb["body_names"] = [n for n in kb["body_names"] if n != name]
folder = Path(main_dir)/name
if not folder.exists():
return {"ok": False, "msg": f"No folder for '{name}'."}
files = sorted(p for p in folder.iterdir() if p.suffix.lower() in valid_exts)
total = len(files)
if total == 0:
_kb_save(kb_path, kb); return {"ok": True, "msg": f"Re-encoded {name}: 0 files."}
reid = _body_extractor(reid_model, log_fn)
faces = bodies = 0
for i, p in enumerate(files, 1):
with Image.open(p) as im:
img = im.convert("RGB")
f = _encode_face(img)
if f is not None:
kb["face_encodings"].append(f); kb["face_names"].append(name); faces += 1
try:
e = reid(img).astype("float32")
kb["body_encodings"].append(e); kb["body_names"].append(name); bodies += 1
except Exception:
pass
if progress_fn: progress_fn(int(i*100/total))
if log_fn: log_fn(f"[Reencode] {name} — {p.name} face={'✓' if f is not None else '×'}")
_kb_save(kb_path, kb)
return {"ok": True, "msg": f"Re-encoded {name}", "faces": faces, "bodies": bodies}
# tiny, all round KB operation worker
class KBOpWorker(QtCore.QObject):
progress = pyqtSignal(int)
log = pyqtSignal(str)
finished = pyqtSignal(dict)
def __init__(self, op: str, main_dir: Path, kb_path: Path, reid_model: str = "osnet_ain_x1_0",
name: str | None = None, src_folder: Path | None = None,
old_name: str | None = None, new_name: str | None = None,
valid_exts=(".jpg",".jpeg",".png",".webp"), parent=None):
super().__init__(parent)
self.op, self.main_dir, self.kb_path, self.reid_model = op, Path(main_dir), Path(kb_path), reid_model
self.name, self.src_folder = name, (Path(src_folder) if src_folder else None)
self.old_name, self.new_name = old_name, new_name
self.valid_exts = tuple(valid_exts)
@QtCore.pyqtSlot()
def run(self):
try:
if self.op == "add":
stats = add_person_from_folder(self.main_dir, self.kb_path, self.reid_model,
self.name, self.src_folder, self.valid_exts,
log_fn=self.log.emit, progress_fn=self.progress.emit)
elif self.op == "remove":
stats = remove_person(self.main_dir, self.kb_path, self.name)
elif self.op == "rename":
stats = rename_person(self.main_dir, self.kb_path, self.old_name, self.new_name)
elif self.op == "reencode":
stats = reencode_person(self.main_dir, self.kb_path, self.reid_model,
self.name, self.valid_exts, log_fn=self.log.emit, progress_fn=self.progress.emit)
else:
stats = {"ok": False, "msg": f"Unknown op '{self.op}'"}
except Exception as e:
stats = {"ok": False, "msg": f"{self.op} failed: {e}"}
self.finished.emit(stats)
PythonAdd Menu Items to UI.PY
In ui.py in the MainWindow class we adjust the method _make_menubar(self) to hold following Manage Person items.
# menu to manage application functions
def _make_menubar(self):
mb = self.menuBar()
# Quit
self.menu_settings = mb.addMenu("&Quit")
act_quit = QtGui.QAction("Quit", self)
act_quit.triggered.connect(self.close)
self.menu_settings.addAction(act_quit)
# Settings
self.menu_settings = mb.addMenu("&Settings")
act_prefs = QtGui.QAction("Preferences…", self)
act_prefs.triggered.connect(self._on_prefs)
self.menu_settings.addAction(act_prefs)
# Manage Persons
self.menu_manage_persons = mb.addMenu("&Manage Persons")
act_add = QtGui.QAction("Add Person…", self); act_add.triggered.connect(self.menu_add_person)
act_remove = QtGui.QAction("Remove Person…", self); act_remove.triggered.connect(self.menu_remove_person)
act_rename = QtGui.QAction("Rename Person…", self); act_rename.triggered.connect(self.menu_rename_person)
act_reenc = QtGui.QAction("Re-encode Person…", self); act_reenc.triggered.connect(self.menu_reencode_person)
self.menu_manage_persons.addActions([act_add, act_remove, act_rename, act_reenc])
# remainder stays as it is …..
PythonAlso in the MainWindow class we add the corresponding trigger methods to call the Dialogs.
def menu_add_person(self):
if not self.cfg.dataset_dir:
QtWidgets.QMessageBox.warning(self, "No KB", "Initialize the KB first.")
return
dlg = AddPersonDialog(self)
if dlg.exec() != QtWidgets.QDialog.DialogCode.Accepted: return
name = dlg.name.text().strip(); src = dlg.src.text().strip()
if not name or not src:
QtWidgets.QMessageBox.warning(self, "Missing info", "Provide a person name and source folder.")
return
main_dir, kb_path = self._kb_paths()
from kb import KBOpWorker
w = KBOpWorker(op="add", main_dir=main_dir, kb_path=kb_path,
reid_model=self.cfg.reid_model, name=name, src_folder=Path(src),
valid_exts=tuple(self.cfg.valid_exts))
self._start_kb_op(w, f"Add person '{name}'")
def menu_remove_person(self):
if not self.cfg.dataset_dir:
QtWidgets.QMessageBox.warning(self, "No KB", "Initialize the KB first.")
return
main_dir, kb_path = self._kb_paths()
dlg = PersonSelectDialog(main_dir, "Remove Person", self)
if dlg.exec() != QtWidgets.QDialog.DialogCode.Accepted: return
name = dlg.combo.currentText()
if QtWidgets.QMessageBox.question(self, "Confirm delete",
f"Remove '{name}' and all its encodings? This cannot be undone.") != QtWidgets.QMessageBox.StandardButton.Yes:
return
from kb import KBOpWorker
w = KBOpWorker(op="remove", main_dir=main_dir, kb_path=kb_path, name=name)
self._start_kb_op(w, f"Remove '{name}'")
def menu_rename_person(self):
if not self.cfg.dataset_dir:
QtWidgets.QMessageBox.warning(self, "No KB", "Initialize the KB first.")
return
main_dir, kb_path = self._kb_paths()
dlg = RenameDialog(main_dir, self)
if dlg.exec() != QtWidgets.QDialog.DialogCode.Accepted: return
old, new = dlg.from_cb.currentText(), dlg.to_le.text().strip()
if not new:
QtWidgets.QMessageBox.warning(self, "Missing name", "Enter a new name.")
return
from kb import KBOpWorker
w = KBOpWorker(op="rename", main_dir=main_dir, kb_path=kb_path, old_name=old, new_name=new)
self._start_kb_op(w, f"Rename '{old}' → '{new}'")
def menu_reencode_person(self):
if not self.cfg.dataset_dir:
QtWidgets.QMessageBox.warning(self, "No KB", "Initialize the KB first.")
return
main_dir, kb_path = self._kb_paths()
dlg = PersonSelectDialog(main_dir, "Re-encode Person", self)
if dlg.exec() != QtWidgets.QDialog.DialogCode.Accepted: return
name = dlg.combo.currentText()
from kb import KBOpWorker
w = KBOpWorker(op="reencode", main_dir=main_dir, kb_path=kb_path,
reid_model=self.cfg.reid_model, name=name,
valid_exts=tuple(self.cfg.valid_exts))
self._start_kb_op(w, f"Re-encode '{name}'")
PythonAdd Dialog classes
Finally in ui.py but below the MainWindow class we add the Dialog classes.
# ---- Dialogs for managing persons in the KB ----
class AddPersonDialog(QtWidgets.QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Add Person")
form = QtWidgets.QFormLayout(self)
self.name = QtWidgets.QLineEdit(self)
self.src = QtWidgets.QLineEdit(self); self.src.setReadOnly(True)
btn = QtWidgets.QPushButton("Browse…", self)
row = QtWidgets.QHBoxLayout(); row.addWidget(self.src); row.addWidget(btn)
form.addRow("Person name:", self.name)
form.addRow("Source folder:", row)
btns = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.StandardButton.Ok|
QtWidgets.QDialogButtonBox.StandardButton.Cancel, parent=self)
form.addRow(btns)
btn.clicked.connect(self._pick)
btns.accepted.connect(self.accept); btns.rejected.connect(self.reject)
def _pick(self):
d = QtWidgets.QFileDialog.getExistingDirectory(self, "Select source folder")
if d: self.src.setText(d)
class PersonSelectDialog(QtWidgets.QDialog):
def __init__(self, kb_root: Path, title="Select Person", parent=None):
super().__init__(parent); self.setWindowTitle(title)
v = QtWidgets.QVBoxLayout(self)
self.combo = QtWidgets.QComboBox(self)
names = [p.name for p in sorted(Path(kb_root).iterdir()) if p.is_dir()]
self.combo.addItems(names)
v.addWidget(self.combo)
btns = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.StandardButton.Ok|
QtWidgets.QDialogButtonBox.StandardButton.Cancel, parent=self)
v.addWidget(btns); btns.accepted.connect(self.accept); btns.rejected.connect(self.reject)
class RenameDialog(QtWidgets.QDialog):
def __init__(self, kb_root: Path, parent=None):
super().__init__(parent); self.setWindowTitle("Rename Person")
form = QtWidgets.QFormLayout(self)
self.from_cb = QtWidgets.QComboBox(self)
names = [p.name for p in sorted(Path(kb_root).iterdir()) if p.is_dir()]
self.from_cb.addItems(names)
self.to_le = QtWidgets.QLineEdit(self)
form.addRow("From:", self.from_cb)
form.addRow("To:", self.to_le)
btns = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.StandardButton.Ok|
QtWidgets.QDialogButtonBox.StandardButton.Cancel, parent=self)
form.addRow(btns); btns.accepted.connect(self.accept); btns.rejected.connect(self.reject)
PythonExtra: KB Overview and Enhanced About Dialog
While we’re at it, we might as well enhance the app a bit.
KB Stats Dialog
We can edit the KB now with the new person operations as well as batch extension. So it would be nice the see some info on the state of things. Therefore in ui.py in the MainWindow class we adjust the Manage Knowledgebase section of the Menu in the method _make_menubar(self) by adding three lines at the bottom:
# Manage Knowledgebase
self.menu_kb = mb.addMenu("&Knowledgebase")
act_kb_init = QtGui.QAction("Initialize KB…", self)
act_kb_init.triggered.connect(self._kb_init)
self.menu_kb.addAction(act_kb_init)
act_kb_extend = QtGui.QAction("Extend KB from Folder…", self)
act_kb_extend.triggered.connect(self._kb_extend)
self.menu_kb.addAction(act_kb_extend)
self.menu_kb.addSeparator()
act_kb_stats = QtGui.QAction("Show KB Stats…", self)
act_kb_stats.triggered.connect(self.menu_kb_stats)
self.menu_kb.addAction(act_kb_stats)
PythonThen we drop this little trigger method in ui.py in the MainWindow class.
def menu_kb_stats(self):
if not self.cfg.dataset_dir:
QtWidgets.QMessageBox.warning(self, "No KB", "Initialize the KB first.")
return
main_dir = Path(self.cfg.dataset_dir)
kb_path = main_dir / self.cfg.encodings_filename
from kb import get_kb_stats
stats = get_kb_stats(main_dir, kb_path, valid_exts=tuple(self.cfg.valid_exts))
KBStatsDialog(stats, self).exec()
PythonNotice this method does something more than just calling the dialog. First it calls a KB function that gathers the information the dialog is about to display.
So in kb.py we need to drop in this little method that scans the dataset folders and the encodings.pkl file:
# gather KB statistics
def get_kb_stats(main_dir: Path, kb_path: Path, valid_exts=(".jpg",".jpeg",".png",".webp")) -> dict:
main_dir = Path(main_dir)
kb_path = Path(kb_path)
stats = {
"kb_root": str(main_dir),
"encodings_file": str(kb_path),
"total_persons": 0,
"total_images": 0,
"average_images_per_person": 0.0,
"person_most_images": ("", 0),
"person_least_images": ("", 0),
"total_face_encodings": 0,
"total_body_encodings": 0,
"face_dim": 0,
"body_dim": 0,
"encoding_file_size_kb": 0.0,
"names_only_in_fs": [],
"names_only_in_pkl": [],
"persons": [] # rows: {"name","images","faces","bodies"}
}
# ---- filesystem scan
person_image_counts = {}
if main_dir.is_dir():
for p in sorted(main_dir.iterdir()):
if p.is_dir():
cnt = sum(1 for f in p.iterdir() if f.suffix.lower() in valid_exts)
person_image_counts[p.name] = cnt
stats["total_persons"] = len(person_image_counts)
stats["total_images"] = sum(person_image_counts.values())
if stats["total_persons"]:
stats["average_images_per_person"] = round(stats["total_images"] / stats["total_persons"], 2)
# most/least images
most = max(person_image_counts.items(), key=lambda kv: kv[1], default=("", 0))
least = min(person_image_counts.items(), key=lambda kv: kv[1], default=("", 0))
stats["person_most_images"] = most
stats["person_least_images"] = least
# ---- encodings.pkl
db = {"face_encodings": [], "face_names": [], "body_encodings": [], "body_names": []}
if kb_path.exists():
try:
db = pickle.load(open(kb_path, "rb")) or db
stats["encoding_file_size_kb"] = round(os.path.getsize(kb_path) / 1024.0, 2)
except Exception:
pass
stats["total_face_encodings"] = len(db.get("face_encodings", []))
stats["total_body_encodings"] = len(db.get("body_encodings", []))
if db.get("face_encodings"):
stats["face_dim"] = int(np.asarray(db["face_encodings"][0]).shape[-1])
if db.get("body_encodings"):
stats["body_dim"] = int(np.asarray(db["body_encodings"][0]).shape[-1])
# per-person enc counts
face_c = Counter(db.get("face_names", []))
body_c = Counter(db.get("body_names", []))
# reconcile names
names_fs = set(person_image_counts.keys())
names_pkl = set(face_c.keys()) | set(body_c.keys())
stats["names_only_in_fs"] = sorted(names_fs - names_pkl)
stats["names_only_in_pkl"] = sorted(names_pkl - names_fs)
# build rows
rows = []
for name in sorted(names_fs | names_pkl):
rows.append({
"name": name,
"images": person_image_counts.get(name, 0),
"faces": face_c.get(name, 0),
"bodies": body_c.get(name, 0),
})
stats["persons"] = rows
return stats
PythonThat’s it. Back to ui.py. Here’s the code to display the KB Overview dialog. Drop it at the end of the file.
class KBStatsDialog(QtWidgets.QDialog):
def __init__(self, stats: dict, parent=None):
super().__init__(parent)
self.setWindowTitle("Knowledge Base — Overview")
self.resize(720, 520)
layout = QtWidgets.QVBoxLayout(self)
# --- summary grid
grid = QtWidgets.QGridLayout()
def add_row(r, label, value):
grid.addWidget(QtWidgets.QLabel(label), r, 0)
grid.addWidget(QtWidgets.QLabel(str(value)), r, 1)
add_row(0, "KB root", stats.get("kb_root", ""))
add_row(1, "Encodings file", stats.get("encodings_file", ""))
add_row(2, "Persons (folders)", stats.get("total_persons", 0))
add_row(3, "Images (total)", stats.get("total_images", 0))
add_row(4, "Avg images/person", stats.get("average_images_per_person", 0.0))
add_row(5, "Face encodings", stats.get("total_face_encodings", 0))
add_row(6, "Body encodings", stats.get("total_body_encodings", 0))
add_row(7, "Face dim", stats.get("face_dim", 0))
add_row(8, "Body dim", stats.get("body_dim", 0))
add_row(9, "encodings.pkl (KB)", stats.get("encoding_file_size_kb", 0.0))
layout.addLayout(grid)
# name discrepancies (compact line)
warn = []
nf = stats.get("names_only_in_fs", [])
npkl = stats.get("names_only_in_pkl", [])
if nf: warn.append(f"Only in folders: {', '.join(nf[:6])}{'…' if len(nf)>6 else ''}")
if npkl: warn.append(f"Only in encodings: {', '.join(npkl[:6])}{'…' if len(npkl)>6 else ''}")
if warn:
note = QtWidgets.QLabel("⚠ " + " | ".join(warn))
note.setWordWrap(True)
layout.addWidget(note)
# --- table
table = QtWidgets.QTableWidget(self)
rows = stats.get("persons", [])
table.setRowCount(len(rows)); table.setColumnCount(4)
table.setHorizontalHeaderLabels(["Person", "Images", "Face encs", "Body encs"])
for r, row in enumerate(rows):
table.setItem(r, 0, QtWidgets.QTableWidgetItem(row["name"]))
table.setItem(r, 1, QtWidgets.QTableWidgetItem(str(row["images"])))
table.setItem(r, 2, QtWidgets.QTableWidgetItem(str(row["faces"])))
table.setItem(r, 3, QtWidgets.QTableWidgetItem(str(row["bodies"])))
table.resizeColumnsToContents()
table.horizontalHeader().setStretchLastSection(True)
layout.addWidget(table)
# buttons
btns = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.StandardButton.Close, parent=self)
layout.addWidget(btns)
btns.rejected.connect(self.reject)
btns.accepted.connect(self.accept)
PythonAbout Dialog
Currently the About menu doesn’t do very much. Therefore we’ll replace it with a very simple dialog, showing a short HTML based text explaining the app’s purpose and showing a cheat sheet on the face threshold value. As the About menu is already there, we only have to adjust the trigger method in ui.py in the MainWindow class to call the About dialog.
def _on_about(self):
dialog = AboutDialog(self)
dialog.exec()
PythonAlso in ui.py we add the AboutDialog class at the end of the source file. Next to the other Dialog classes we just added. Feel free to edit the text as you like!
class AboutDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("About This Application")
layout = QVBoxLayout()
html = """
Person Recognition App
This application performs local face and body recognition using a customizable Knowledge base.
Person DB - Persons Descriptions by the Knowledge base
The json based persons database will be added to the images dataset in the next stage of this project .
usage: 'py schema_gui.py person.schema.json persons.json'
⚙️ Recognition Parameters
face_threshold
Defines how strictly a face must match a known encoding:
Threshold Behavior
0.6 Standard — balanced match
0.5 Strict — fewer false positives
0.4 Very strict — high precision
≤ 0.35 Extremely strict — near-identical only
body_threshold
Similar logic, based on cosine similarity of body features.
"""
label = QLabel()
label.setTextFormat(Qt.TextFormat.RichText)
label.setText(html)
label.setWordWrap(True)
label.setOpenExternalLinks(True)
layout.addWidget(label)
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
button_box.accepted.connect(self.accept)
layout.addWidget(button_box)
self.setLayout(layout)
PythonWhat’s next
You may have noticed the About dialog reffering to a ‘JSON based Persons DB’. That’s coming up: we’ll introduce a lightweight schema and GUI to keep person metadata (labels, notes, canonical names) alongside the image dataset.