Adding Eye Candy
Note: In this article, real people’s faces and names in illustrations have been intentionally obscured to protect privacy
In this next step we provide some eye candy. Two new visually attractive features are introduced to the application. Finding lookalikes and visually inspecting our knowledge base of known persons.
We make things more visually engaging by adding the ability to search for lookalikes and browse the results side-by-side. This enhancement transforms the Knowledge Base (KB) from a passive storage system into an analytical tool. Recall that the knowledge basis consists of a collection of photos of known persons, with both faces & bodies encoded.
Advanced Knowledge Base Utilization
The lookalike function provides a powerful new way to leverage the existing knowledge base. Instead of merely identifying known people in new photos, users can now query the internal embeddings to find similarities between existing entries.
This is particularly useful for identifying duplicates—for example, cases where the same person might be stored under different names. It can also help uncover relationships, such as family members or individuals with similar physical characteristics.
The same capability supports active data-integrity maintenance: ensuring photos are labeled correctly, optionally re-encoding persons or adjusting thresholds to fine-tune behavior when no matches are found (or when too many are).
Technical Efficiency and Performance
Nobody likes to wait. For that reason, we built the lookalike feature with high-performance logic to keep the application responsive. Once again, we leverage the existing vectorized comparison helpers.
Instead of relying on slow nested loops, the application performs a vector-to-bank comparison. This computes distances or similarities for an entire set of embeddings in a single pass, which is significantly faster.

Because both face and body encodings are available, dual-signal analysis is possible. Face encodings are compared using Euclidean distance (lower is better), while body encodings use cosine similarity (higher is better). Together, these signals provide robust and flexible comparison behavior.
Enhanced User Experience (UX) and Visual Verification
The most prominent UI addition is the Side-by-Side Preview. When a user double-clicks a result in the lookalike table, a second dialog opens showing the target person and the candidate next to each other.
For intuitive navigation, the app supports keyboard shortcuts: arrow keys navigate images for the left person, while A/D keys navigate images for the right person. This makes it easy to browse both folders and visually confirm a match.
At the same time, the data remains front and center. The dialog presents results in a table that includes thumbnails and detailed scores (average and best matches), making the information far more digestible than a simple list of names.
A Digital Lineup
You can think of this feature as a digital lineup. Instead of manually searching through a vast warehouse of filing cabinets, the application instantly brings the most likely candidates into a private viewing room and places them next to your target. Differences and similarities become immediately apparent at a glance.

A Closer Look at the Details
Let’s examine how this was implemented.
The kb.py source file already contained most of the necessary building blocks: helpers for loading and saving the KB, face and body bank builders, scoring and normalization utilities. This made adding a lookalikes_for() function relatively straightforward.
Specifically, the implementation builds on:
- kb_load(), build_face_bank(), and build_body_bank()
- _normalize_rows(), _normalize_vec(), and dot-product cosine similarity
- Default threshold handling via params_from_settings()
The function returns a result set that ui.py can display directly. Importantly, it avoids inefficient “target vs. all others” nested loops and instead performs a single vector-to-bank pass per target embedding, followed by aggregation per person.
#-----------------------------------------------------------------------------
# Find Lookalikes in KB
#------------------------------------------------------------------------------
LookMode = Literal["face", "body"]
def lookalikes_for(target_name: str, kb_path: str, *, mode: LookMode = "face", topk: int = 10, settings: Dict[str, Any] | None = None, include_self: bool = False, oversample: int = 5,
) -> Tuple[List[Tuple[str, float, float]], Dict[str, Any]]:
"""
Find lookalikes for an existing KB person using *their own stored embeddings*.
Returns:
rows: list of tuples (other_name, avg_score, best_score)
- face: score is distance (lower is better)
- body: score is similarity (higher is better)
meta: dict with thresholds used and notes
Notes:
- Uses params_from_settings() for face_tol/body_tol and relax defaults. :contentReference[oaicite:3]{index=3}
- Uses build_face_bank/build_body_bank and normalization helpers. :contentReference[oaicite:4]{index=4}
"""
pr = params_from_settings(settings)
topk = max(1, int(topk))
oversample = max(1, int(oversample))
want_n = topk * oversample
db = kb_load(kb_path)
if mode == "face":
F, F_names = build_face_bank(db)
if F.size == 0:
return [], {"mode": mode, "error": "Empty face bank."}
is_t = (F_names == target_name)
if not np.any(is_t):
return [], {"mode": mode, "error": f"No face encodings for '{target_name}'."}
# Query set: all embeddings of the target in the KB
Q = F[is_t] # (M, dim)
# Candidate bank: all embeddings (including target unless include_self=False)
# We'll filter self later by name.
# Thresholding behavior consistent with your defaults:
# face_tol is a distance cutoff; allow a small relax for "no results" situations.
thr = float(pr["face_tol"])
relax = float(pr.get("face_relax", DEFAULTS["face_relax"]))
thr_relaxed = thr + relax
# Aggregate: for each other person, we compute:
# - per query vector q: best distance to that person (min over their images)
# - avg_best_distance = mean(best_per_q)
# - min_distance = min(best_per_q)
uniq_names = np.unique(F_names)
# map name -> list of indices in F
idx_by_name = {n: np.where(F_names == n)[0] for n in uniq_names}
per_name_best_over_q: Dict[str, List[float]] = {}
for q in Q:
d = np.linalg.norm(F - q[None, :], axis=1).astype(np.float32) # (N,)
for n, idxs in idx_by_name.items():
if (not include_self) and (n == target_name):
continue
# best match for this person given this q
per_name_best_over_q.setdefault(n, []).append(float(np.min(d[idxs])))
rows: List[Tuple[str, float, float]] = []
for n, bests in per_name_best_over_q.items():
avg_best = float(np.mean(bests))
best = float(np.min(bests))
rows.append((n, avg_best, best))
# Sort by best (min distance), then avg
rows.sort(key=lambda r: (r[2], r[1]))
# Apply threshold filter AFTER sorting (cheap)
filtered = [r for r in rows if r[2] <= thr_relaxed]
return filtered[:topk], {
"mode": mode,
"target": target_name,
"threshold": thr,
"threshold_relaxed": thr_relaxed,
"unit": "distance",
"note": "lower is better",
}
# ---------------- BODY ----------------
B, B_names, body_dim = build_body_bank(db)
if B.size == 0 or body_dim == 0:
return [], {"mode": mode, "error": "Empty body bank."}
is_t = (B_names == target_name)
if not np.any(is_t):
return [], {"mode": mode, "error": f"No body encodings for '{target_name}'."}
# Normalize once and use dot-product cosine (fast)
B_unit = _normalize_rows(B)
Q = B_unit[is_t] # target vectors already normalized
thr = float(pr["body_tol"])
relax = float(pr.get("body_relax", DEFAULTS["body_relax"]))
thr_relaxed = thr - relax
uniq_names = np.unique(B_names)
idx_by_name = {n: np.where(B_names == n)[0] for n in uniq_names}
per_name_best_over_q: Dict[str, List[float]] = {}
for q in Q:
# q already unit, B_unit rows unit -> cosine similarity
sims = (B_unit @ q).astype(np.float32) # (N,)
for n, idxs in idx_by_name.items():
if (not include_self) and (n == target_name):
continue
per_name_best_over_q.setdefault(n, []).append(float(np.max(sims[idxs])))
rows: List[Tuple[str, float, float]] = []
for n, bests in per_name_best_over_q.items():
avg_best = float(np.mean(bests))
best = float(np.max(bests))
rows.append((n, avg_best, best))
# Sort by best (max similarity), then avg
rows.sort(key=lambda r: (-r[2], -r[1]))
filtered = [r for r in rows if r[2] >= thr_relaxed]
return filtered[:topk], {
"mode": mode,
"target": target_name,
"threshold": thr,
"threshold_relaxed": thr_relaxed,
"unit": "cosine_similarity",
"note": "higher is better",
}
PythonInside the GUI
To the existing application menu, we add a new action under the Knowledge Base section: Find Lookalikes…. This integrates seamlessly with the existing menu structure and shortcuts.
Menu Action Item
# Knowledge Base
m = mb.addMenu("Knowledge Base")
act(m, "Initialize KB", self._kb_init, shortcut="Ctrl+I", tip="Create a new knowledge base from the dataset folder", status="Build encodings.pkl from the persons dataset", key="act_kb_init")
act(m, "Extend KB from Folder…", self._kb_extend, shortcut="Ctrl+E", tip="Bulk-add new persons from a folder", status="Adds new person folders and encodes them", key="act_kb_extend")
m.addSeparator()
act(m, "Find Lookalikes…", self.find_lookalikes, shortcut="Ctrl+F", tip="Search for lookalikes in the knowledge base", status="Searches and Show similar persons", key="act_fnd_lookalikes")
m.addSeparator()
act(m, "Show KB Stats…", self.menu_kb_stats, shortcut="Ctrl+S", tip="Show knowledge base statistics", status="Displays counts and file size", key="act_kb_stats")
PythonMenu Handler Method
The menu handler first verifies that a knowledge base is present. It then lets the user select a target person and choose whether to compare faces or bodies. The worker function executes with this information.
If results are found, they are passed to a dialog for presentation.
Here is the menu handler method that orchestrates things.
# Search the KB for lookalikes of a selected person comparing Face and Body embeddings
def find_lookalikes(self):
# --- Preconditions
if not self.cfg.dataset_dir:
QMessageBox.warning(self, "No KB", "Initialize the Knowledge Base first.")
return
main_dir, kb_path = self._kb_paths()
if not kb_path.exists():
QMessageBox.warning(self, "KB Missing", f"Encodings file not found:\n{kb_path}")
return
# --- Select target person
dlg = PersonSelectDialog(main_dir, "Find Lookalikes", self)
if dlg.exec() != QDialog.DialogCode.Accepted:
return
target = dlg.combo.currentText()
# --- Ask mode (face/body)
mode, ok = QtWidgets.QInputDialog.getItem(self, "Lookalike Mode", "Compare using:",
["face", "body"], editable=False)
if not ok:
return
# --- Ask Top-K
topk, ok = QtWidgets.QInputDialog.getInt(self, "Top-K Results", "Number of lookalikes to show:",
value=10, min=1, max=50 )
if not ok:
return
# --- Busy UI hint
QtWidgets.QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
self.statusBar().showMessage("Finding lookalikes…")
self.log(f"[Lookalikes] target='{target}' mode={mode} topk={topk}")
# --- call KB
try:
rows, meta = lookalikes_for(target_name=target, kb_path=str(kb_path), mode=mode, topk=topk, settings=self.cfg.__dict__,)
except Exception as e:
QMessageBox.warning(self, "Lookalikes Failed", str(e))
return
finally:
QtWidgets.QApplication.restoreOverrideCursor()
self.statusBar().showMessage("Ready")
# --- No results
if not rows:
QMessageBox.information(
self,
"No Lookalikes",
f"No {mode} lookalikes found for '{target}'.\n\n"
f"Try:\n• Adding more images\n• Relaxing thresholds\n• Re-encoding this person"
)
return
# --- Display results Side-by-side preview (simple, but great UX)
thr = meta.get("threshold_relaxed", meta.get("threshold", None))
LookalikeResultsDialog(title=f"Lookalikes for {target} ({mode})", rows=rows, target_name=target, mode=mode, threshold=thr, sample_getter=self._sample_image,
folder_getter=lambda n: str(self._person_folder(n)),parent=self).exec()
def _person_folder(self, name: str) -> Path:
kb_root, _kb_path = self._kb_paths()
return (kb_root / name).expanduser()
def _sample_image(self, name: str) -> str | None:
folder = self._person_folder(name)
if not folder.exists() or not folder.is_dir():
return None
exts = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".gif"}
for p in sorted(folder.iterdir()):
if p.suffix.lower() in exts:
return str(p)
return None
PythonPerson Seclection Dialog
To select the target person, we reuse the existing PersonSelectDialog. A nice detail here is the use of pathlib: in a single statement, we collect all person names by listing the subfolders of the KB root directory.
names = [p.name for p in sorted(Path(kb_root).iterdir()) if p.is_dir()]PythonHere’s the complete fragment.
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)
PythonGetting Thumbnails
To present the results we created the LookalikeResultsDialog. We also add two tiny helpers to aid the dialog in finding images to use as thumbnails.
def _person_folder(self, name: str) -> Path:
kb_root, _kb_path = self._kb_paths()
return (kb_root / name).expanduser()
def _sample_image(self, name: str) -> str | None:
folder = self._person_folder(name)
if not folder.exists() or not folder.is_dir():
return None
exts = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".gif"}
for p in sorted(folder.iterdir()):
if p.suffix.lower() in exts:
return str(p)
return None
PythonLookalike Results Dialog
The results dialog itself is straightforward, but it includes a useful enhancement: each row displays not only the person’s name and scores, but also a thumbnail image retrieved via small helper functions.
Double-clicking a row opens the side-by-side preview dialog.
# --- Lookalike Results Dialog ---
class LookalikeResultsDialog(QtWidgets.QDialog):
"""
rows: list of tuples (name, avg_score, best_score)
- face: score = distance (lower is better)
- body: score = similarity (higher is better)
"""
def __init__(self, *, title: str, rows, target_name: str, mode: str,
threshold: float | None,
sample_getter, folder_getter, parent=None):
super().__init__(parent)
self.setWindowTitle(title)
self.resize(760, 520)
self.rows = rows
self.target_name = target_name
self.mode = mode
self.threshold = threshold
self.sample_getter = sample_getter
self.folder_getter = folder_getter
# --- MAIN LAYOUT
v = QtWidgets.QVBoxLayout(self)
# Header hint
hint = QtWidgets.QLabel(self)
if mode == "face":
msg = f"<b>Mode:</b> face (distance — lower is better)"
else:
msg = f"<b>Mode:</b> body (similarity — higher is better)"
if threshold is not None:
msg += f" <b>Threshold:</b> {float(threshold):.3f}"
hint.setText(msg)
hint.setTextFormat(QtCore.Qt.TextFormat.RichText)
v.addWidget(hint)
# --- UX hint (keyboard navigation)
nav_hint = QtWidgets.QLabel("Tip: ←/→ browse LEFT · A/D browse RIGHT", self)
nav_hint.setStyleSheet("color: #666;")
v.addWidget(nav_hint)
# --- Results table
self.table = QtWidgets.QTableWidget(self)
self.table.setColumnCount(4)
self.table.setHorizontalHeaderLabels(["Person", "Avg", "Best", "Preview"])
self.table.setRowCount(len(rows))
self.table.setSortingEnabled(True)
self.table.horizontalHeader().setStretchLastSection(True)
for r, (name, avg_s, best_s) in enumerate(rows):
self.table.setItem(r, 0, QtWidgets.QTableWidgetItem(str(name)))
self.table.setItem(r, 1, QtWidgets.QTableWidgetItem(f"{float(avg_s):.4f}"))
best_item = QtWidgets.QTableWidgetItem(f"{float(best_s):.4f}")
# highlight "passes threshold" (same intent as old dialog) :contentReference[oaicite:2]{index=2}
try:
if threshold is not None:
if self.mode == "face" and float(best_s) <= float(threshold):
best_item.setForeground(QtGui.QBrush(QtCore.Qt.GlobalColor.red))
elif self.mode == "body" and float(best_s) >= float(threshold):
best_item.setForeground(QtGui.QBrush(QtCore.Qt.GlobalColor.red))
except Exception:
pass
self.table.setItem(r, 2, best_item)
thumb = QtWidgets.QLabel(self)
thumb.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
p = self.sample_getter(name) if self.sample_getter else None
if p and os.path.exists(p):
pix = QtGui.QPixmap(p).scaledToHeight(
64, QtCore.Qt.TransformationMode.SmoothTransformation
)
thumb.setPixmap(pix)
self.table.setCellWidget(r, 3, thumb)
# default sort by Best
self.table.sortItems(2, QtCore.Qt.SortOrder.AscendingOrder if mode == "face" else QtCore.Qt.SortOrder.DescendingOrder)
v.addWidget(self.table, 1)
btns = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.StandardButton.Close, parent=self)
btns.rejected.connect(self.reject)
btns.accepted.connect(self.reject)
v.addWidget(btns)
# interactions
self.table.cellDoubleClicked.connect(self._on_double_click)
self.table.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu)
self.table.customContextMenuRequested.connect(self._on_context_menu)
def _on_double_click(self, row: int, _col: int):
cand_item = self.table.item(row, 0)
if not cand_item:
return
cand = cand_item.text().strip()
left = self.sample_getter(self.target_name)
right = self.sample_getter(cand)
SideBySidePreviewDialog(left, right, self.target_name, cand, self).exec()
def _on_context_menu(self, pos):
idx = self.table.indexAt(pos)
row = idx.row()
if row < 0:
return
cand_item = self.table.item(row, 0)
if not cand_item:
return
cand = cand_item.text().strip()
cand_folder = self.folder_getter(cand) if self.folder_getter else None
target_folder = self.folder_getter(self.target_name) if self.folder_getter else None
menu = QtWidgets.QMenu(self)
act_c = menu.addAction("Open candidate folder")
act_t = menu.addAction(f"Open {self.target_name} folder") if target_folder else None
chosen = menu.exec(self.table.viewport().mapToGlobal(pos))
if chosen == act_c:
self._open_folder(cand_folder)
elif act_t and chosen == act_t:
self._open_folder(target_folder)
def _open_folder(self, folder: str | Path | None):
if not folder:
QtWidgets.QMessageBox.information(self, "Folder not found", str(folder))
return
p = Path(str(folder)).expanduser()
if not p.exists() or not p.is_dir():
QtWidgets.QMessageBox.information(self, "Folder not found", str(p))
return
# Use Qt, consistent with our IdentifyResultDialog implementation
PythonSide-By-Side Preview
This is where the feature really shines.
When a user double-clicks a candidate, a new dialog opens showing an image of the target person next to an image of the candidate. Both sides can be browsed independently using buttons or keyboard shortcuts, enabling fast visual verification.
# -- Side-by-Side Preview Dialog (for lookalike results) browse target + candidate image folders ---
class SideBySidePreviewDialog(QtWidgets.QDialog):
def __init__(self, left_path, right_path, left_title, right_title, parent=None):
super().__init__(parent)
self.setWindowTitle(f"{left_title} ⇄ {right_title}")
self.resize(1100, 650)
v = QtWidgets.QVBoxLayout(self)
# --- images row
row = QtWidgets.QHBoxLayout()
self.left = QtWidgets.QLabel(left_title); self.left.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.right = QtWidgets.QLabel(right_title); self.right.setAlignment(Qt.AlignmentFlag.AlignCenter)
row.addWidget(self.left, 1)
row.addWidget(self.right, 1)
# --- nav buttons row (like old dialog)
nav = QtWidgets.QHBoxLayout()
self.btn_prev_l = QtWidgets.QPushButton("◀ Left")
self.btn_next_l = QtWidgets.QPushButton("Right ▶")
self.btn_prev_r = QtWidgets.QPushButton("◀ Left")
self.btn_next_r = QtWidgets.QPushButton("Right ▶")
nav.addWidget(self.btn_prev_l); nav.addWidget(self.btn_next_l)
nav.addStretch()
nav.addWidget(self.btn_prev_r); nav.addWidget(self.btn_next_r)
v.addLayout(row)
v.addLayout(nav)
# --- footer
btns = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.StandardButton.Close, parent=self)
btns.rejected.connect(self.reject)
btns.accepted.connect(self.reject)
v.addWidget(btns)
# initial pixmaps
self._set_pixmap(self.left, left_path)
self._set_pixmap(self.right, right_path)
# collect all images in the two folders (same logic as old) :contentReference[oaicite:1]{index=1}
self.left_title, self.right_title = left_title, right_title
self.left_files = self._list_images(Path(left_path).parent if left_path else None)
self.right_files = self._list_images(Path(right_path).parent if right_path else None)
self.li = self.left_files.index(left_path) if left_path in self.left_files else 0
self.ri = self.right_files.index(right_path) if right_path in self.right_files else 0
self.btn_prev_l.clicked.connect(lambda: self._step(-1, side="L"))
self.btn_next_l.clicked.connect(lambda: self._step(+1, side="L"))
self.btn_prev_r.clicked.connect(lambda: self._step(-1, side="R"))
self.btn_next_r.clicked.connect(lambda: self._step(+1, side="R"))
# keyboard arrows (same mapping as old dialog) :contentReference[oaicite:2]{index=2}
# Left side: ← / →
# Right side: A / D
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
def _list_images(self, folder: Path | None):
if not folder or not folder.exists():
return []
exts = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".gif"}
return sorted(str(p) for p in folder.iterdir() if p.suffix.lower() in exts)
def keyPressEvent(self, e: QtGui.QKeyEvent):
if e.key() == Qt.Key.Key_Left:
self._step(-1, side="L")
elif e.key() == Qt.Key.Key_Right:
self._step(+1, side="L")
elif e.key() == Qt.Key.Key_A:
self._step(-1, side="R")
elif e.key() == Qt.Key.Key_D:
self._step(+1, side="R")
else:
super().keyPressEvent(e)
def _step(self, delta: int, side: str):
if side == "L" and self.left_files:
self.li = (self.li + delta) % len(self.left_files)
self._set_pixmap(self.left, self.left_files[self.li])
elif side == "R" and self.right_files:
self.ri = (self.ri + delta) % len(self.right_files)
self._set_pixmap(self.right, self.right_files[self.ri])
def _set_pixmap(self, label: QtWidgets.QLabel, path: str | None):
if not path or not os.path.exists(path):
label.setText(f"{label.text()}\n(no image)")
return
pix = QtGui.QPixmap(path)
if pix.isNull():
label.setText(f"{label.text()}\n(invalid image)")
return
label.setPixmap(pix.scaled(520, 620, QtCore.Qt.AspectRatioMode.KeepAspectRatio, QtCore.Qt.TransformationMode.SmoothTransformation) )
PythonConsistent Architecture
Although the side-by-side preview is conceptually simple, it adds significant UX value. Importantly, it fits cleanly into the existing architecture: computation remains in kb.py, while ui.py handles presentation and interaction. Separation of concerns is preserved, and no GUI logic leaks into the knowledge-base layer.
One more Thing
While completing this feature, we identified a small bug in the previously provided source code. The Knowledge Base Overview dialog was displaying an empty persons table.
This issue has been fixed, and as a bonus, a new visual feature was added: double-clicking a person now opens their image folder directly.
Enhanced KB Stats Dialog: Browsing a Persons Folder
The correct folder opens in the system’s file explorer. We connect a double-click event to QDesktopServices.openUrl . This small addition greatly improves usability when inspecting the knowledge base.
The Double-click Trick
This is achieved with QDesktopServices.openUrl, we already used before. Just adding this small piece of code to the init() method of the KBStatsDialog, right after populating the persons table, does the trick.
This works because we first tell it the location of the persons folders so that when adding the person’s name it will pick the right image folder. Then we connect a double-click event to a small helper method that will open the correct directory in Windows Explorer.
# --- Double-click row -> open person folder
kb_root = Path(stats.get("kb_root", ""))
table.cellDoubleClicked.connect(_open_person_folder)
def _open_person_folder(row: int, _col: int):
try:
name_item = table.item(row, 0)
if not name_item:
return
person_name = name_item.text().strip()
folder = kb_root / person_name
if folder.exists() and folder.is_dir():
QtGui.QDesktopServices.openUrl(QtCore.QUrl.fromLocalFile(str(folder)))
else:
QtWidgets.QMessageBox.information(self, "Folder not found", f"Folder does not exist:\n{folder}")
except Exception as e:
QtWidgets.QMessageBox.warning(self, "Open folder failed", str(e))
PythonThe Folder opened in Explorer
The result after double clicking on a certain person will look something like this.

The Architecture
Here’s a sketch of the four architectural layers.

Together they offer an end-to-end data pipeline:
Images → Encodings → Vector Banks → Lookalike Scoring → Ranked Persons → Visual Confirmation
New Download
A new package is now available containing the complete application source code, including all newly added visual features.
The package also includes an updated settings.json with sensible default thresholds and configuration values. Be sure to update the dataset_dir entry to point to your own knowledge base location for the application to function correctly.

A short version of this article has been posted on LinkedIn.
Several subject of this article, for instance finding lookalikes, have been discussed on Reddit.