Compare commits
4 commits
30ee8107fd
...
d1552b3eb5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1552b3eb5 | ||
|
|
4df206981f | ||
|
|
11012b7db5 | ||
|
|
afe6dde499 |
12
.envrc
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#!/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
|
||||
35
README.md
|
|
@ -1,2 +1,35 @@
|
|||
# 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
|
||||
|
|
|
|||
0
devenv.lock
Normal file
23
flake.lock
generated
|
|
@ -34,10 +34,31 @@
|
|||
"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": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
"nixpkgs": "nixpkgs",
|
||||
"pyproject-nix": "pyproject-nix"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
|
|
|
|||
98
flake.nix
|
|
@ -49,8 +49,8 @@
|
|||
ls -l $src
|
||||
echo $src
|
||||
|
||||
mkdir -p "$out/var/lib/insightface/buffalo_l/"
|
||||
unar -D -o $out/var/lib/insightface/buffalo_l/ $src
|
||||
mkdir -p "$out/var/lib/insightface/models/"
|
||||
unar -D -o $out/var/lib/insightface/models/buffalo_l/ $src
|
||||
'';
|
||||
|
||||
nativeBuildInputs = [ pkgs.unar ];
|
||||
|
|
@ -74,9 +74,12 @@
|
|||
arg = project.renderers.mkPythonEditablePackage { inherit python; };
|
||||
pythonEnv = python.pkgs.mkPythonEditablePackage ( arg );
|
||||
in pkgs.mkShell {
|
||||
packages = [ pkgs.python3 pkgs.uv
|
||||
# buffaloModel
|
||||
protestswap pkgs.gcc14 ];
|
||||
packages = with pkgs; [
|
||||
python3
|
||||
uv
|
||||
gcc14
|
||||
python3Packages.fastapi
|
||||
] ++ [protestswap];
|
||||
shellHook = /*shell*/
|
||||
''
|
||||
ls -l ${buffaloModel}
|
||||
|
|
@ -84,10 +87,10 @@
|
|||
INSIGHTFACE_ROOT_DIR=$(mktemp -d /tmp/insightface.XXXXXXXX)
|
||||
export INSIGHTFACE_ROOT_DIR
|
||||
|
||||
mkdir -p "$INSIGHTFACE_ROOT_DIR"
|
||||
mkdir -p "$INSIGHTFACE_ROOT_DIR/models/"
|
||||
|
||||
ln -s ${inswapperModel}/var/lib/insightface/models "$INSIGHTFACE_ROOT_DIR/models"
|
||||
ln -s ${buffaloModel}/var/lib/insightface/buffalo_l "$INSIGHTFACE_ROOT_DIR/buffalo_l"
|
||||
ln -s ${inswapperModel}/var/lib/insightface/models/inswapper_128.onnx "$INSIGHTFACE_ROOT_DIR/models/"
|
||||
ln -s ${buffaloModel}/var/lib/insightface/models/buffalo_l "$INSIGHTFACE_ROOT_DIR/models/buffalo_l"
|
||||
|
||||
# export PROTESTSWAP_ROOT="/tmp/protestswap_models"
|
||||
# mkdir -p "$PROTESTSWAP_ROOT/buffalo_l"
|
||||
|
|
@ -102,82 +105,3 @@
|
|||
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};
|
||||
# };
|
||||
# });
|
||||
|
|
|
|||
|
|
@ -6,13 +6,16 @@ description = "A minimal pyproject.toml"
|
|||
authors = [ ]
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"insightface",
|
||||
"onnxruntime",
|
||||
"opencv-python", "matplotlib"
|
||||
"insightface",
|
||||
"onnxruntime",
|
||||
"opencv-python",
|
||||
"matplotlib",
|
||||
"fastapi[standard]",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
protestswap-cli = "protestswap.cli:main"
|
||||
# protestswap-server = "protestswap.server:"
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build >= 0.8.17, <0.9.0"]
|
||||
|
|
|
|||
BIN
resources/five_faces.jpeg
Normal file
|
After Width: | Height: | Size: 390 KiB |
BIN
resources/greta.png
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
resources/henry_cavill.png
Normal file
|
After Width: | Height: | Size: 836 KiB |
BIN
resources/many_faces_target
Normal file
|
After Width: | Height: | Size: 328 KiB |
BIN
resources/roger_moore.png
Normal file
|
After Width: | Height: | Size: 579 KiB |
BIN
resources/three_faces_template.png
Normal file
|
After Width: | Height: | Size: 921 KiB |
1
result
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
/nix/store/zadcgfd4y5c41acjm1cabk5kdaix6xa7-python3.13-protestswap-0.0.1
|
||||
BIN
result.png
Normal file
|
After Width: | Height: | Size: 3.9 MiB |
|
|
@ -1,6 +1,10 @@
|
|||
import os
|
||||
import logging
|
||||
|
||||
import argparse
|
||||
|
||||
import cv2
|
||||
|
||||
from protestswap.protestswap import ProtestFaceSwapper
|
||||
|
||||
logger = logging.getLogger("protestswap")
|
||||
|
|
@ -10,19 +14,47 @@ INSIGHTFACE_VAR = "INSIGHTFACE_ROOT_DIR"
|
|||
|
||||
|
||||
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
|
||||
if os.path.exists(DEFAULT_INSIGHTFACE_DIR):
|
||||
insightface_dir = DEFAULT_INSIGHTFACE_DIR
|
||||
elif os.environ.get(INSIGHTFACE_VAR):
|
||||
if os.environ.get(INSIGHTFACE_VAR):
|
||||
insightface_dir = os.environ.get(INSIGHTFACE_VAR)
|
||||
elif os.path.exists(DEFAULT_INSIGHTFACE_DIR):
|
||||
insightface_dir = DEFAULT_INSIGHTFACE_DIR
|
||||
else:
|
||||
logger.warning(
|
||||
f"No directory at '{DEFAULT_INSIGHTFACE_DIR}' and '{INSIGHTFACE_VAR}' not set yet."
|
||||
)
|
||||
insightface_dir = f"{os.environ.get('HOME')}/.cache/insightface"
|
||||
|
||||
# swapper = ProtestFaceSwapper(insightface_dir)
|
||||
# swapper.prepare_model()
|
||||
swapper = ProtestFaceSwapper(insightface_dir)
|
||||
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__":
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import logging
|
||||
import os
|
||||
import logging
|
||||
|
||||
import cv2
|
||||
import random # TODO: seed!
|
||||
import random # TODO: seed!
|
||||
|
||||
import insightface
|
||||
from insightface.app import FaceAnalysis
|
||||
|
|
@ -14,51 +14,65 @@ from insightface.app.common import Face
|
|||
|
||||
logger = logging.getLogger("protestswap")
|
||||
|
||||
SWAPPING_MODEL = 'inswapper_128.onnx'
|
||||
SWAPPING_MODEL = "inswapper_128.onnx"
|
||||
|
||||
|
||||
class ProtestFaceSwapper:
|
||||
def __init__(self, root_dir):
|
||||
self._app : FaceAnalysis = FaceAnalysis(name='buffalo_l', root=insightface_dir)
|
||||
self._model : INSwapper
|
||||
self._source : cv2.typing.MatLike|None = None
|
||||
self._source_faces : list[Face] = []
|
||||
self._template_faces : list[Face] = []
|
||||
self._root_dir = root_dir
|
||||
self._app: FaceAnalysis = FaceAnalysis(name="buffalo_l", root=root_dir)
|
||||
self._model: INSwapper
|
||||
self._target: cv2.typing.MatLike | None = None
|
||||
self._target_faces: list[Face] = []
|
||||
self._template_faces: list[Face] = []
|
||||
|
||||
self._app.prepare(ctx_id=0, det_size=(640, 640))
|
||||
|
||||
def prepare_model(self) -> None:
|
||||
self._model = insightface.model_zoo.get_model(SWAPPING_MODEL, download=True, download_zip=True) # Revealed type: "INSwapper"
|
||||
self._model = insightface.model_zoo.get_model(
|
||||
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_source(self, path: str) -> None:
|
||||
self._source = cv2.imread(path)
|
||||
def set_target_path(self, path: str) -> None:
|
||||
self.set_target(cv2.imread(path))
|
||||
|
||||
def detect_source_faces(self):
|
||||
self._source_faces = self._app.get(self._source)
|
||||
def set_target(self, image: cv2.UMat) -> None:
|
||||
self._target = image
|
||||
|
||||
def add_template_faces(self, template_path: str):
|
||||
self._template_faces += [self._app.get(cv2.imread(template_path))]
|
||||
def detect_target_faces(self):
|
||||
self._target_faces = self._app.get(self._target)
|
||||
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:
|
||||
work_copy = self._source_faces.copy()
|
||||
for i, face in enumerate(self._source_faces):
|
||||
print(f"Replacing face {i+1}/{len(self._source_faces)}...", end="", flush=True);
|
||||
work_copy = self._target.copy()
|
||||
for i, face in enumerate(self._target_faces):
|
||||
print(
|
||||
f"Replacing face {i + 1}/{len(self._target_faces)}...",
|
||||
# end="",
|
||||
# flush=True,
|
||||
)
|
||||
imposed_face = random.choice(self._template_faces)
|
||||
work_copy = self._model.get(work_copy, face, imposed_face, paste_back=True)
|
||||
print(
|
||||
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!")
|
||||
|
||||
return work_copy
|
||||
|
||||
@staticmethod
|
||||
def readFile(path: str) -> cv2.typing.MatLike|None:
|
||||
def readFile(path: str) -> cv2.typing.MatLike | None:
|
||||
return cv2.imread(path)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
swapper = ProtestFaceSwapper(insightface_dir)
|
||||
swapper.prepare_model()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
|
|||
32
src/protestswap/server.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
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,
|
||||
}
|
||||