Compare commits
No commits in common. "d1552b3eb5b683fe46a1a3d4239b39f66ed1e645" and "30ee8107fd07da54f4af0a12a2f788b7990d16b7" have entirely different histories.
d1552b3eb5
...
30ee8107fd
12
.envrc
|
|
@ -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
|
||||
35
README.md
|
|
@ -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
|
||||
|
|
|
|||
23
flake.lock
generated
|
|
@ -34,31 +34,10 @@
|
|||
"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",
|
||||
"pyproject-nix": "pyproject-nix"
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
|
|
|
|||
98
flake.nix
|
|
@ -49,8 +49,8 @@
|
|||
ls -l $src
|
||||
echo $src
|
||||
|
||||
mkdir -p "$out/var/lib/insightface/models/"
|
||||
unar -D -o $out/var/lib/insightface/models/buffalo_l/ $src
|
||||
mkdir -p "$out/var/lib/insightface/buffalo_l/"
|
||||
unar -D -o $out/var/lib/insightface/buffalo_l/ $src
|
||||
'';
|
||||
|
||||
nativeBuildInputs = [ pkgs.unar ];
|
||||
|
|
@ -74,12 +74,9 @@
|
|||
arg = project.renderers.mkPythonEditablePackage { inherit python; };
|
||||
pythonEnv = python.pkgs.mkPythonEditablePackage ( arg );
|
||||
in pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
python3
|
||||
uv
|
||||
gcc14
|
||||
python3Packages.fastapi
|
||||
] ++ [protestswap];
|
||||
packages = [ pkgs.python3 pkgs.uv
|
||||
# buffaloModel
|
||||
protestswap pkgs.gcc14 ];
|
||||
shellHook = /*shell*/
|
||||
''
|
||||
ls -l ${buffaloModel}
|
||||
|
|
@ -87,10 +84,10 @@
|
|||
INSIGHTFACE_ROOT_DIR=$(mktemp -d /tmp/insightface.XXXXXXXX)
|
||||
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 ${buffaloModel}/var/lib/insightface/models/buffalo_l "$INSIGHTFACE_ROOT_DIR/models/buffalo_l"
|
||||
ln -s ${inswapperModel}/var/lib/insightface/models "$INSIGHTFACE_ROOT_DIR/models"
|
||||
ln -s ${buffaloModel}/var/lib/insightface/buffalo_l "$INSIGHTFACE_ROOT_DIR/buffalo_l"
|
||||
|
||||
# export PROTESTSWAP_ROOT="/tmp/protestswap_models"
|
||||
# mkdir -p "$PROTESTSWAP_ROOT/buffalo_l"
|
||||
|
|
@ -105,3 +102,82 @@
|
|||
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};
|
||||
# };
|
||||
# });
|
||||
|
|
|
|||
|
|
@ -5,17 +5,14 @@ license-files = ["LICEN[CS]E*"]
|
|||
description = "A minimal pyproject.toml"
|
||||
authors = [ ]
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"insightface",
|
||||
"onnxruntime",
|
||||
"opencv-python",
|
||||
"matplotlib",
|
||||
"fastapi[standard]",
|
||||
dependencies = [
|
||||
"insightface",
|
||||
"onnxruntime",
|
||||
"opencv-python", "matplotlib"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
protestswap-cli = "protestswap.cli:main"
|
||||
# protestswap-server = "protestswap.server:"
|
||||
|
||||
[build-system]
|
||||
requires = ["uv_build >= 0.8.17, <0.9.0"]
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 390 KiB |
|
Before Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 836 KiB |
|
Before Width: | Height: | Size: 328 KiB |
|
Before Width: | Height: | Size: 579 KiB |
|
Before Width: | Height: | Size: 921 KiB |
1
result
|
|
@ -1 +0,0 @@
|
|||
/nix/store/zadcgfd4y5c41acjm1cabk5kdaix6xa7-python3.13-protestswap-0.0.1
|
||||
BIN
result.png
|
Before Width: | Height: | Size: 3.9 MiB |
|
|
@ -1,10 +1,6 @@
|
|||
import os
|
||||
import logging
|
||||
|
||||
import argparse
|
||||
|
||||
import cv2
|
||||
|
||||
from protestswap.protestswap import ProtestFaceSwapper
|
||||
|
||||
logger = logging.getLogger("protestswap")
|
||||
|
|
@ -14,47 +10,19 @@ 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.environ.get(INSIGHTFACE_VAR):
|
||||
insightface_dir = os.environ.get(INSIGHTFACE_VAR)
|
||||
elif os.path.exists(DEFAULT_INSIGHTFACE_DIR):
|
||||
if os.path.exists(DEFAULT_INSIGHTFACE_DIR):
|
||||
insightface_dir = DEFAULT_INSIGHTFACE_DIR
|
||||
elif os.environ.get(INSIGHTFACE_VAR):
|
||||
insightface_dir = os.environ.get(INSIGHTFACE_VAR)
|
||||
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.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)
|
||||
# swapper = ProtestFaceSwapper(insightface_dir)
|
||||
# swapper.prepare_model()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import logging
|
||||
import os
|
||||
|
||||
import cv2
|
||||
import random # TODO: seed!
|
||||
import random # TODO: seed!
|
||||
|
||||
import insightface
|
||||
from insightface.app import FaceAnalysis
|
||||
|
|
@ -14,65 +14,51 @@ 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._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 : 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._app.prepare(ctx_id=0, det_size=(640, 640))
|
||||
|
||||
def prepare_model(self) -> None:
|
||||
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}")
|
||||
self._model = insightface.model_zoo.get_model(SWAPPING_MODEL, download=True, download_zip=True) # Revealed type: "INSwapper"
|
||||
|
||||
def set_target_path(self, path: str) -> None:
|
||||
self.set_target(cv2.imread(path))
|
||||
def set_source(self, path: str) -> None:
|
||||
self._source = cv2.imread(path)
|
||||
|
||||
def set_target(self, image: cv2.UMat) -> None:
|
||||
self._target = image
|
||||
def detect_source_faces(self):
|
||||
self._source_faces = self._app.get(self._source)
|
||||
|
||||
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 add_template_faces(self, template_path: str):
|
||||
self._template_faces += [self._app.get(cv2.imread(template_path))]
|
||||
|
||||
def swap(self) -> cv2.typing.MatLike:
|
||||
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,
|
||||
)
|
||||
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);
|
||||
imposed_face = random.choice(self._template_faces)
|
||||
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)
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||