Managing Persons in Photo Collections – Adding, Removing, Renaming and Reencoding People

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
}
Python

Add 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"))
Python

Add 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)
Python

Add 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 …..
Python

Also 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}'")        
Python

Add 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)

Python

Extra: 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)
Python

Then 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()        
Python

Notice 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    
Python

That’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)
Python

About 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()
Python

Also 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:

ThresholdBehavior
0.6Standard — balanced match
0.5Strict — fewer false positives
0.4Very strict — high precision
≤ 0.35Extremely 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)
Python

What’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.

Related Stories