Compare commits

..

No commits in common. "d1552b3eb5b683fe46a1a3d4239b39f66ed1e645" and "30ee8107fd07da54f4af0a12a2f788b7990d16b7" have entirely different histories.

18 changed files with 128 additions and 1847 deletions

12
.envrc
View file

@ -1,12 +0,0 @@
#!/usr/bin/env bash
export DIRENV_WARN_TIMEOUT=20s
eval "$(devenv direnvrc)"
# `use devenv` supports the same options as the `devenv shell` command.
#
# To silence all output, use `--quiet`.
#
# Example usage: use devenv --quiet --impure --option services.postgres.enable:bool true
use devenv

View file

@ -1,35 +1,2 @@
# Protestswap # protestswap
## Motivation
Fotos auf social media zu verbreiten ist für politische Gruppen ein zweischneidiges Schwert.
Einerseits möchte mensch die Identitäten der aktiven schützen, weswegen es gängig ist, die Gesichter zu verpixeln.
Andererseits kommt das Verpixeln der Gesichter bei der bürgerlich geprägten Öffentlichkeit nicht sehr gut an.
Es macht den Eindruck, als wären nur Verbrechys und Tankies an der Organisation beteiligt.
## Lösungsansatz
Snapchat kann seit der Mitte der 2010er Jahre live Gesichter lokal auf dem Handy swappen.
Mit den heute verfügbaren CV-tools und Machine-learning resourcen, sollte es möglich sein, Gesichter auf Fotos authentisch genug auszutauschen, um gleichzeitig die Identitäten von Aktivistys zu schützen und die Optik des zu teilenden Bildes nicht kaputt zu machen - und zwar ohne auf die turbokapitalistischen Unionbuster aus Silicon Valley, Redmond und Südafrika zurückzugreifen (OpenAI, Grok, Microsoft, usw.).
Im Idealfall werden die Gesichter von echten Personen mit KI-generierten menschlichen Gesichtern ausgetauscht, um keine Persönlichkeitsrechte von celebrities zu verletzen und sich damit rechtlich angreifbar zu machen. Zum Beispiel aus einem [Random-Face-Generator](https://this-person-does-not-exist.com/de).
## Requirements
- Faceswapping von allen Gesichtern auf einem Foto
- Lokal ausführbar auf einem PC
### Nice-To-Have
- *Prio A*: Einfache GUI vom Handy aus
- *Prio B*: Angeben von Bereichen, wo Gesichter nicht geswappt werden (z.B. um nicht Emma Goldman auf Plakaten zu faceswappen)
- *Prio C*: Lokal ausführbar auf einem Handy
## Technik
Disclaimer: Ich bin weder sehr bewandert in Machine-learning, noch darin eine fertige App zu entwickeln. Ich mache hauptsächlich devops und Linux-kram.
### Backend
Als Backend wird [`insightface`](https://github.com/deepinsight/insightface) genutzt. Ich habe online ein älteres Repo gefunden, das die Funktionalität hat und das umgeschrieben.
Das Modell ist von Huggingface zu haben

View file

23
flake.lock generated
View file

@ -34,31 +34,10 @@
"type": "github" "type": "github"
} }
}, },
"pyproject-nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1760402624,
"narHash": "sha256-jF6UKLs2uGc2rtved8Vrt58oTWjTQoAssuYs/0578Z4=",
"owner": "pyproject-nix",
"repo": "pyproject.nix",
"rev": "84c4ea102127c77058ea1ed7be7300261fafc7d2",
"type": "github"
},
"original": {
"owner": "pyproject-nix",
"repo": "pyproject.nix",
"type": "github"
}
},
"root": { "root": {
"inputs": { "inputs": {
"flake-utils": "flake-utils", "flake-utils": "flake-utils",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs"
"pyproject-nix": "pyproject-nix"
} }
}, },
"systems": { "systems": {

View file

@ -49,8 +49,8 @@
ls -l $src ls -l $src
echo $src echo $src
mkdir -p "$out/var/lib/insightface/models/" mkdir -p "$out/var/lib/insightface/buffalo_l/"
unar -D -o $out/var/lib/insightface/models/buffalo_l/ $src unar -D -o $out/var/lib/insightface/buffalo_l/ $src
''; '';
nativeBuildInputs = [ pkgs.unar ]; nativeBuildInputs = [ pkgs.unar ];
@ -74,12 +74,9 @@
arg = project.renderers.mkPythonEditablePackage { inherit python; }; arg = project.renderers.mkPythonEditablePackage { inherit python; };
pythonEnv = python.pkgs.mkPythonEditablePackage ( arg ); pythonEnv = python.pkgs.mkPythonEditablePackage ( arg );
in pkgs.mkShell { in pkgs.mkShell {
packages = with pkgs; [ packages = [ pkgs.python3 pkgs.uv
python3 # buffaloModel
uv protestswap pkgs.gcc14 ];
gcc14
python3Packages.fastapi
] ++ [protestswap];
shellHook = /*shell*/ shellHook = /*shell*/
'' ''
ls -l ${buffaloModel} ls -l ${buffaloModel}
@ -87,10 +84,10 @@
INSIGHTFACE_ROOT_DIR=$(mktemp -d /tmp/insightface.XXXXXXXX) INSIGHTFACE_ROOT_DIR=$(mktemp -d /tmp/insightface.XXXXXXXX)
export INSIGHTFACE_ROOT_DIR export INSIGHTFACE_ROOT_DIR
mkdir -p "$INSIGHTFACE_ROOT_DIR/models/" mkdir -p "$INSIGHTFACE_ROOT_DIR"
ln -s ${inswapperModel}/var/lib/insightface/models/inswapper_128.onnx "$INSIGHTFACE_ROOT_DIR/models/" ln -s ${inswapperModel}/var/lib/insightface/models "$INSIGHTFACE_ROOT_DIR/models"
ln -s ${buffaloModel}/var/lib/insightface/models/buffalo_l "$INSIGHTFACE_ROOT_DIR/models/buffalo_l" ln -s ${buffaloModel}/var/lib/insightface/buffalo_l "$INSIGHTFACE_ROOT_DIR/buffalo_l"
# export PROTESTSWAP_ROOT="/tmp/protestswap_models" # export PROTESTSWAP_ROOT="/tmp/protestswap_models"
# mkdir -p "$PROTESTSWAP_ROOT/buffalo_l" # mkdir -p "$PROTESTSWAP_ROOT/buffalo_l"
@ -105,3 +102,82 @@
packages.${system}.default = protestswap; packages.${system}.default = protestswap;
}; };
} }
# flake-utils.lib.eachDefaultSystem (system: let
# pyproject = builtins.fromTOML (builtins.readFile ./pyproject.toml);
# project = pyproject.project;
#
# package = pkgs.python3Packages.buildPythonPackage {
# pname = project.name;
# inherit (project) version;
# format = "pyproject";
#
# src = ./.;
#
# build-system = with pkgs.python3Packages; [
# uv-build
# ];
#
# # # test dependencies
# nativeCheckInputs = with pkgs; [
# # python3Packages.mypy
# # python3Packages.pytest
# taplo
# ];
#
# # checkPhase = '''';
#
# propagatedBuildInputs = with pkgs; [
# python3Packages.click
# python3Packages.insightface
#
# ] ++ builtins.map (dep: pkgs.python3Packages.${dep}) project.dependencies;
# };
#
# editablePackage = pkgs.python3.pkgs.mkPythonEditablePackage {
# pname = project.name;
# inherit (project) scripts version;
# root = "$PWD";
# };
# in {
# devShells = {
# default = pkgs.mkShell {
# inputsFrom = [
# package
# ];
#
# buildInputs = [
# # our package
# editablePackage
#
# #################
# # VARIOUS TOOLS #
# #################
#
# pkgs.python3Packages.build
# pkgs.python3Packages.ipython
# pkgs.python3Packages.insightface
# inswapperModel
#
# ####################
# # EDITOR/LSP TOOLS #
# ####################
#
# # LSP server:
# pkgs.python3Packages.python-lsp-server
#
# # LSP server plugins of interest:
# pkgs.python3Packages.pylsp-mypy
# pkgs.python3Packages.pylsp-rope
# pkgs.python3Packages.python-lsp-ruff
# ];
# };
# };
#
# packages = {
# "inswapper-model" = inswapperModel;
# "${project.name}" = package;
# default = self.packages.${system}.${project.name};
# };
# });

View file

@ -5,17 +5,14 @@ license-files = ["LICEN[CS]E*"]
description = "A minimal pyproject.toml" description = "A minimal pyproject.toml"
authors = [ ] authors = [ ]
requires-python = ">=3.13" requires-python = ">=3.13"
dependencies = [ dependencies = [
"insightface", "insightface",
"onnxruntime", "onnxruntime",
"opencv-python", "opencv-python", "matplotlib"
"matplotlib",
"fastapi[standard]",
] ]
[project.scripts] [project.scripts]
protestswap-cli = "protestswap.cli:main" protestswap-cli = "protestswap.cli:main"
# protestswap-server = "protestswap.server:"
[build-system] [build-system]
requires = ["uv_build >= 0.8.17, <0.9.0"] requires = ["uv_build >= 0.8.17, <0.9.0"]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 390 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 836 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 579 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 921 KiB

1
result
View file

@ -1 +0,0 @@
/nix/store/zadcgfd4y5c41acjm1cabk5kdaix6xa7-python3.13-protestswap-0.0.1

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 MiB

View file

@ -1,10 +1,6 @@
import os import os
import logging import logging
import argparse
import cv2
from protestswap.protestswap import ProtestFaceSwapper from protestswap.protestswap import ProtestFaceSwapper
logger = logging.getLogger("protestswap") logger = logging.getLogger("protestswap")
@ -14,47 +10,19 @@ INSIGHTFACE_VAR = "INSIGHTFACE_ROOT_DIR"
def main(): def main():
parser = argparse.ArgumentParser(
prog="protestswap", description="Swaps faces of people on protests"
)
parser.add_argument("target", help="The image to swap the faces on")
parser.add_argument(
"-t", "--template", action="append", help="The template faces to paste on"
)
args = parser.parse_args()
print(args.target)
if not args.template:
print("You need at least one template (with '-t')!")
exit(1)
insightface_dir = None insightface_dir = None
if os.environ.get(INSIGHTFACE_VAR): if os.path.exists(DEFAULT_INSIGHTFACE_DIR):
insightface_dir = os.environ.get(INSIGHTFACE_VAR)
elif os.path.exists(DEFAULT_INSIGHTFACE_DIR):
insightface_dir = DEFAULT_INSIGHTFACE_DIR insightface_dir = DEFAULT_INSIGHTFACE_DIR
elif os.environ.get(INSIGHTFACE_VAR):
insightface_dir = os.environ.get(INSIGHTFACE_VAR)
else: else:
logger.warning( logger.warning(
f"No directory at '{DEFAULT_INSIGHTFACE_DIR}' and '{INSIGHTFACE_VAR}' not set yet." f"No directory at '{DEFAULT_INSIGHTFACE_DIR}' and '{INSIGHTFACE_VAR}' not set yet."
) )
insightface_dir = f"{os.environ.get('HOME')}/.cache/insightface" insightface_dir = f"{os.environ.get('HOME')}/.cache/insightface"
swapper = ProtestFaceSwapper(insightface_dir) # swapper = ProtestFaceSwapper(insightface_dir)
swapper.prepare_model() # swapper.prepare_model()
swapper.set_target_path(args.target)
for template in args.template:
print(f"Adding template {template}...")
swapper.add_template_path(template)
print("Detecting faces")
swapper.detect_target_faces()
swapped = swapper.swap()
cv2.imwrite("result.png", swapped)
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -1,10 +1,10 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os
import logging import logging
import os
import cv2 import cv2
import random # TODO: seed! import random # TODO: seed!
import insightface import insightface
from insightface.app import FaceAnalysis from insightface.app import FaceAnalysis
@ -14,65 +14,51 @@ from insightface.app.common import Face
logger = logging.getLogger("protestswap") logger = logging.getLogger("protestswap")
SWAPPING_MODEL = "inswapper_128.onnx" SWAPPING_MODEL = 'inswapper_128.onnx'
class ProtestFaceSwapper: class ProtestFaceSwapper:
def __init__(self, root_dir): def __init__(self, root_dir):
self._root_dir = root_dir self._app : FaceAnalysis = FaceAnalysis(name='buffalo_l', root=insightface_dir)
self._app: FaceAnalysis = FaceAnalysis(name="buffalo_l", root=root_dir) self._model : INSwapper
self._model: INSwapper self._source : cv2.typing.MatLike|None = None
self._target: cv2.typing.MatLike | None = None self._source_faces : list[Face] = []
self._target_faces: list[Face] = [] self._template_faces : list[Face] = []
self._template_faces: list[Face] = []
self._app.prepare(ctx_id=0, det_size=(640, 640)) self._app.prepare(ctx_id=0, det_size=(640, 640))
def prepare_model(self) -> None: def prepare_model(self) -> None:
self._model = insightface.model_zoo.get_model( self._model = insightface.model_zoo.get_model(SWAPPING_MODEL, download=True, download_zip=True) # Revealed type: "INSwapper"
os.path.join(self._root_dir, "models", SWAPPING_MODEL),
root=self._root_dir,
download=False,
download_zip=False,
) # Revealed type: "INSwapper"
print(f"Model: {self._model}")
def set_target_path(self, path: str) -> None: def set_source(self, path: str) -> None:
self.set_target(cv2.imread(path)) self._source = cv2.imread(path)
def set_target(self, image: cv2.UMat) -> None: def detect_source_faces(self):
self._target = image self._source_faces = self._app.get(self._source)
def detect_target_faces(self): def add_template_faces(self, template_path: str):
self._target_faces = self._app.get(self._target) self._template_faces += [self._app.get(cv2.imread(template_path))]
print(f"Found {len(self._target_faces)} faces!")
def add_template_path(self, template_path: str):
self.add_template(cv2.imread(template_path))
def add_template(self, template: cv2.UMat):
self._template_faces += self._app.get(template)
def swap(self) -> cv2.typing.MatLike: def swap(self) -> cv2.typing.MatLike:
work_copy = self._target.copy() work_copy = self._source_faces.copy()
for i, face in enumerate(self._target_faces): for i, face in enumerate(self._source_faces):
print( print(f"Replacing face {i+1}/{len(self._source_faces)}...", end="", flush=True);
f"Replacing face {i + 1}/{len(self._target_faces)}...",
# end="",
# flush=True,
)
imposed_face = random.choice(self._template_faces) imposed_face = random.choice(self._template_faces)
print( work_copy = self._model.get(work_copy, face, imposed_face, paste_back=True)
f"work_copy: {type(work_copy)}, face: {type(face)}, imposed: {
type(imposed_face)
}"
)
work_copy = self._model.get(
work_copy, face, imposed_face, paste_back=True)
print(" done!") print(" done!")
return work_copy return work_copy
@staticmethod @staticmethod
def readFile(path: str) -> cv2.typing.MatLike | None: def readFile(path: str) -> cv2.typing.MatLike|None:
return cv2.imread(path) return cv2.imread(path)
def main():
swapper = ProtestFaceSwapper(insightface_dir)
swapper.prepare_model()
if __name__ == '__main__':
main()

View file

@ -1,32 +0,0 @@
from typing import Annotated
from fastapi import FastAPI, File, Form, UploadFile
from pydantic import BaseModel
class Item(BaseModel):
target: Annotated[UploadFile, File()]
template: list[UploadFile]
description: str | None = None
app = FastAPI()
#
# @app.post("/items/")
# async def create_item(item: Item):
# return item
@app.post("/files/")
async def upload_file(
# item: Item,
file: Annotated[bytes, File()],
fileb: list[Annotated[UploadFile, File()]],
token: Annotated[str, Form()],
):
return {
"file_size": len(file),
"token": token,
"fileb_content_type": fileb[0].content_type,
}

1647
uv.lock generated

File diff suppressed because it is too large Load diff