Add pipeline orchestrator
Browse files- run_pipeline.py +212 -0
run_pipeline.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Red Team Pipeline Orchestrator - See README.md for full documentation.
|
| 4 |
+
Workflow: Input -> Protection (Paper X) -> Deepfake Generation -> Evaluation -> Report
|
| 5 |
+
"""
|
| 6 |
+
import os, sys, json, argparse, time, logging, numpy as np
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import List, Dict
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
from dataclasses import dataclass, asdict
|
| 11 |
+
import torch
|
| 12 |
+
from PIL import Image
|
| 13 |
+
|
| 14 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
|
| 15 |
+
logger = logging.getLogger("RedTeamPipeline")
|
| 16 |
+
|
| 17 |
+
@dataclass
|
| 18 |
+
class PipelineConfig:
|
| 19 |
+
input_dir: str = "./data/test_images"
|
| 20 |
+
output_dir: str = "./results"
|
| 21 |
+
methods: List[str] = None
|
| 22 |
+
attacks: List[str] = None
|
| 23 |
+
fs_total_iter: int = 30
|
| 24 |
+
fs_noise_clamp: int = 12
|
| 25 |
+
fs_step_size: float = 1.0
|
| 26 |
+
fs_resize: int = 512
|
| 27 |
+
dg_iters: int = 800
|
| 28 |
+
dg_eps: float = 16.0/255.0
|
| 29 |
+
dg_step_size: float = 1.0/255.0
|
| 30 |
+
dg_batch_size: int = 1
|
| 31 |
+
dg_inference_steps: int = 4
|
| 32 |
+
vgm_steps: int = 1000
|
| 33 |
+
vgm_eps: float = 4.0/255.0
|
| 34 |
+
vgm_directed: bool = False
|
| 35 |
+
preprocessing_tests: List[str] = None
|
| 36 |
+
device: str = "cuda"
|
| 37 |
+
def __post_init__(self):
|
| 38 |
+
if self.methods is None: self.methods = ["faceshield", "diffusionguard", "vgmshield"]
|
| 39 |
+
if self.attacks is None: self.attacks = ["inpainting", "ip_adapter"]
|
| 40 |
+
if self.preprocessing_tests is None:
|
| 41 |
+
self.preprocessing_tests = ["jpeg_q75","jpeg_q50","jpeg_q25","blur_s1","blur_s2","resize_50","resize_75","noise_001","noise_005"]
|
| 42 |
+
|
| 43 |
+
def run_faceshield(image_path, output_path, config):
|
| 44 |
+
logger.info(f" [FaceShield] Protecting {os.path.basename(image_path)}")
|
| 45 |
+
try:
|
| 46 |
+
sys.path.insert(0, "/app/faceshield_src")
|
| 47 |
+
from modules.faceshield.protect import protect_image
|
| 48 |
+
return protect_image(image_path=image_path, output_path=output_path, device=config.device,
|
| 49 |
+
total_iter=config.fs_total_iter, noise_clamp=config.fs_noise_clamp, step_size=config.fs_step_size, resize_shape=config.fs_resize)
|
| 50 |
+
except Exception as e:
|
| 51 |
+
logger.error(f" [FaceShield] Failed: {e}")
|
| 52 |
+
return {"method": "faceshield", "status": "error", "error": str(e)}
|
| 53 |
+
|
| 54 |
+
def run_diffusionguard(image_path, mask_path, output_path, config):
|
| 55 |
+
logger.info(f" [DiffusionGuard] Protecting {os.path.basename(image_path)}")
|
| 56 |
+
try:
|
| 57 |
+
sys.path.insert(0, "/app/diffusionguard_src")
|
| 58 |
+
from modules.diffusionguard.protect import protect_image
|
| 59 |
+
return protect_image(image_path=image_path, mask_path=mask_path, output_path=output_path, device=config.device,
|
| 60 |
+
iters=config.dg_iters, eps=config.dg_eps, step_size=config.dg_step_size, batch_size=config.dg_batch_size, num_inference_steps=config.dg_inference_steps)
|
| 61 |
+
except Exception as e:
|
| 62 |
+
logger.error(f" [DiffusionGuard] Failed: {e}")
|
| 63 |
+
return {"method": "diffusionguard", "status": "error", "error": str(e)}
|
| 64 |
+
|
| 65 |
+
def run_vgmshield(image_path, output_path, config):
|
| 66 |
+
logger.info(f" [VGMShield] Protecting {os.path.basename(image_path)}")
|
| 67 |
+
try:
|
| 68 |
+
sys.path.insert(0, "/app/vgmshield_src")
|
| 69 |
+
from modules.vgmshield.protect import protect_image
|
| 70 |
+
return protect_image(input_path=image_path, output_path=output_path, directed=config.vgm_directed, eps=config.vgm_eps, steps=config.vgm_steps, device=config.device)
|
| 71 |
+
except Exception as e:
|
| 72 |
+
logger.error(f" [VGMShield] Failed: {e}")
|
| 73 |
+
return {"method": "vgmshield", "status": "error", "error": str(e)}
|
| 74 |
+
|
| 75 |
+
def run_inpainting_attack(image_path, mask_path, output_path, prompt="a photograph of a person", device="cuda"):
|
| 76 |
+
logger.info(f" [Attack:Inpainting] On {os.path.basename(image_path)}")
|
| 77 |
+
try:
|
| 78 |
+
from diffusers import StableDiffusionInpaintPipeline
|
| 79 |
+
pipe = StableDiffusionInpaintPipeline.from_pretrained("runwayml/stable-diffusion-inpainting", torch_dtype=torch.float16).to(device)
|
| 80 |
+
src = Image.open(image_path).convert("RGB").resize((512,512))
|
| 81 |
+
mask = Image.open(mask_path).convert("RGB").resize((512,512))
|
| 82 |
+
result = pipe(prompt=prompt, image=src, mask_image=mask, num_inference_steps=50).images[0]
|
| 83 |
+
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
| 84 |
+
result.save(output_path)
|
| 85 |
+
del pipe; torch.cuda.empty_cache()
|
| 86 |
+
return {"attack": "inpainting", "status": "success", "output": output_path}
|
| 87 |
+
except Exception as e:
|
| 88 |
+
logger.error(f" [Attack:Inpainting] Failed: {e}")
|
| 89 |
+
return {"attack": "inpainting", "status": "error", "error": str(e)}
|
| 90 |
+
|
| 91 |
+
def run_ip_adapter_attack(image_path, output_path, prompt="a photo of a person", device="cuda"):
|
| 92 |
+
logger.info(f" [Attack:IP-Adapter] On {os.path.basename(image_path)}")
|
| 93 |
+
try:
|
| 94 |
+
from diffusers import StableDiffusionPipeline
|
| 95 |
+
pipe = StableDiffusionPipeline.from_pretrained("runwayml/stable-diffusion-v1-5", torch_dtype=torch.float16).to(device)
|
| 96 |
+
pipe.load_ip_adapter("h94/IP-Adapter", subfolder="models", weight_name="ip-adapter_sd15.bin")
|
| 97 |
+
pipe.set_ip_adapter_scale(0.8)
|
| 98 |
+
src = Image.open(image_path).convert("RGB").resize((512,512))
|
| 99 |
+
result = pipe(prompt=prompt, ip_adapter_image=src, num_inference_steps=50).images[0]
|
| 100 |
+
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
| 101 |
+
result.save(output_path)
|
| 102 |
+
del pipe; torch.cuda.empty_cache()
|
| 103 |
+
return {"attack": "ip_adapter", "status": "success", "output": output_path}
|
| 104 |
+
except Exception as e:
|
| 105 |
+
logger.error(f" [Attack:IP-Adapter] Failed: {e}")
|
| 106 |
+
return {"attack": "ip_adapter", "status": "error", "error": str(e)}
|
| 107 |
+
|
| 108 |
+
def apply_preprocessing(image_path, attack_name, output_path):
|
| 109 |
+
from modules.evaluation.evaluate import PURIFICATION_ATTACKS
|
| 110 |
+
img = Image.open(image_path).convert("RGB")
|
| 111 |
+
processed = PURIFICATION_ATTACKS[attack_name](img)
|
| 112 |
+
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
| 113 |
+
processed.save(output_path)
|
| 114 |
+
return output_path
|
| 115 |
+
|
| 116 |
+
def generate_face_mask(image_path, output_path):
|
| 117 |
+
try:
|
| 118 |
+
import cv2
|
| 119 |
+
img = cv2.imread(image_path)
|
| 120 |
+
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
| 121 |
+
face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
|
| 122 |
+
faces = face_cascade.detectMultiScale(gray, 1.3, 5)
|
| 123 |
+
mask = np.zeros_like(img)
|
| 124 |
+
if len(faces) > 0:
|
| 125 |
+
for (x, y, w, h) in faces:
|
| 126 |
+
p = int(max(w, h) * 0.3)
|
| 127 |
+
mask[max(0,y-p):min(img.shape[0],y+h+p), max(0,x-p):min(img.shape[1],x+w+p)] = 255
|
| 128 |
+
else:
|
| 129 |
+
h, w = img.shape[:2]; mask[h//4:3*h//4, w//4:3*w//4] = 255
|
| 130 |
+
cv2.imwrite(output_path, mask)
|
| 131 |
+
except Exception:
|
| 132 |
+
img = Image.open(image_path); w, h = img.size
|
| 133 |
+
mask = Image.new("RGB", (w, h), "black")
|
| 134 |
+
from PIL import ImageDraw; draw = ImageDraw.Draw(mask)
|
| 135 |
+
m = min(w, h) // 4; draw.rectangle([m, m, w-m, h-m], fill="white")
|
| 136 |
+
mask.save(output_path)
|
| 137 |
+
return output_path
|
| 138 |
+
|
| 139 |
+
def run_pipeline(config):
|
| 140 |
+
start_time = time.time()
|
| 141 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 142 |
+
run_dir = os.path.join(config.output_dir, f"run_{timestamp}")
|
| 143 |
+
os.makedirs(run_dir, exist_ok=True)
|
| 144 |
+
with open(os.path.join(run_dir, "config.json"), 'w') as f: json.dump(asdict(config), f, indent=2, default=str)
|
| 145 |
+
input_dir = Path(config.input_dir)
|
| 146 |
+
test_images = sorted([str(p) for p in input_dir.iterdir() if p.suffix.lower() in {'.png','.jpg','.jpeg','.bmp','.webp'}])
|
| 147 |
+
if not test_images: logger.error(f"No images in {config.input_dir}"); return
|
| 148 |
+
logger.info(f"Found {len(test_images)} images | Methods: {config.methods} | Attacks: {config.attacks}")
|
| 149 |
+
all_results = []
|
| 150 |
+
for img_idx, image_path in enumerate(test_images):
|
| 151 |
+
img_name = Path(image_path).stem
|
| 152 |
+
logger.info(f"\n{'='*60}\nImage {img_idx+1}/{len(test_images)}: {img_name}\n{'='*60}")
|
| 153 |
+
mask_dir = os.path.join(run_dir, "masks"); os.makedirs(mask_dir, exist_ok=True)
|
| 154 |
+
mask_path = os.path.join(mask_dir, f"{img_name}_mask.png")
|
| 155 |
+
generate_face_mask(image_path, mask_path)
|
| 156 |
+
for method in config.methods:
|
| 157 |
+
method_dir = os.path.join(run_dir, method, img_name); os.makedirs(method_dir, exist_ok=True)
|
| 158 |
+
original_copy = os.path.join(method_dir, "original.png")
|
| 159 |
+
Image.open(image_path).convert("RGB").save(original_copy)
|
| 160 |
+
protected_path = os.path.join(method_dir, "protected.png")
|
| 161 |
+
logger.info(f"\n--- Method: {method} ---")
|
| 162 |
+
if method == "faceshield": protection_result = run_faceshield(image_path, protected_path, config)
|
| 163 |
+
elif method == "diffusionguard": protection_result = run_diffusionguard(image_path, mask_path, protected_path, config)
|
| 164 |
+
elif method == "vgmshield": protection_result = run_vgmshield(image_path, protected_path, config)
|
| 165 |
+
else: continue
|
| 166 |
+
with open(os.path.join(method_dir, "protection_result.json"), 'w') as f: json.dump(protection_result, f, indent=2, default=str)
|
| 167 |
+
if protection_result.get("status") != "success": logger.error(f"Protection failed for {method}"); continue
|
| 168 |
+
if config.preprocessing_tests:
|
| 169 |
+
pp_dir = os.path.join(method_dir, "preprocessed"); os.makedirs(pp_dir, exist_ok=True)
|
| 170 |
+
for pp in config.preprocessing_tests:
|
| 171 |
+
try: apply_preprocessing(protected_path, pp, os.path.join(pp_dir, f"{pp}.png")); logger.info(f" [Preprocess] {pp}")
|
| 172 |
+
except Exception as e: logger.error(f" [Preprocess] {pp} failed: {e}")
|
| 173 |
+
for attack in config.attacks:
|
| 174 |
+
attack_dir = os.path.join(method_dir, f"attack_{attack}"); os.makedirs(attack_dir, exist_ok=True)
|
| 175 |
+
df_clean = os.path.join(attack_dir, "deepfake_clean.png"); df_protected = os.path.join(attack_dir, "deepfake_protected.png")
|
| 176 |
+
if attack == "inpainting":
|
| 177 |
+
run_inpainting_attack(original_copy, mask_path, df_clean, device=config.device)
|
| 178 |
+
run_inpainting_attack(protected_path, mask_path, df_protected, device=config.device)
|
| 179 |
+
elif attack == "ip_adapter":
|
| 180 |
+
run_ip_adapter_attack(original_copy, df_clean, device=config.device)
|
| 181 |
+
run_ip_adapter_attack(protected_path, df_protected, device=config.device)
|
| 182 |
+
try:
|
| 183 |
+
from modules.evaluation.evaluate import evaluate_protection
|
| 184 |
+
for attack in config.attacks:
|
| 185 |
+
attack_dir = os.path.join(method_dir, f"attack_{attack}")
|
| 186 |
+
df_c = os.path.join(attack_dir, "deepfake_clean.png"); df_p = os.path.join(attack_dir, "deepfake_protected.png")
|
| 187 |
+
eval_result = evaluate_protection(original_copy, protected_path,
|
| 188 |
+
df_c if os.path.exists(df_c) else None, df_p if os.path.exists(df_p) else None, method_name=method, device=config.device)
|
| 189 |
+
ed = asdict(eval_result); ed["attack"] = attack; ed["image"] = img_name; all_results.append(ed)
|
| 190 |
+
with open(os.path.join(attack_dir, "evaluation.json"), 'w') as f: json.dump(ed, f, indent=2)
|
| 191 |
+
except Exception as e: logger.error(f" [Evaluation] Failed: {e}")
|
| 192 |
+
with open(os.path.join(run_dir, "report.json"), 'w') as f:
|
| 193 |
+
json.dump({"timestamp": timestamp, "config": asdict(config), "total_images": len(test_images), "methods": config.methods, "attacks": config.attacks, "time_s": time.time()-start_time, "results": all_results}, f, indent=2, default=str)
|
| 194 |
+
logger.info(f"\nPipeline complete! Results: {run_dir} | Time: {time.time()-start_time:.1f}s")
|
| 195 |
+
return all_results
|
| 196 |
+
|
| 197 |
+
def main():
|
| 198 |
+
p = argparse.ArgumentParser(description="Red Team Pipeline")
|
| 199 |
+
p.add_argument("--input_dir", default="./data/test_images"); p.add_argument("--output_dir", default="./results")
|
| 200 |
+
p.add_argument("--methods", nargs="+", default=["faceshield","diffusionguard","vgmshield"])
|
| 201 |
+
p.add_argument("--attacks", nargs="+", default=["inpainting","ip_adapter"]); p.add_argument("--device", default="cuda")
|
| 202 |
+
p.add_argument("--fs_iter", type=int, default=30); p.add_argument("--fs_noise", type=int, default=12)
|
| 203 |
+
p.add_argument("--dg_iter", type=int, default=800); p.add_argument("--dg_eps", type=float, default=16/255)
|
| 204 |
+
p.add_argument("--vgm_steps", type=int, default=1000); p.add_argument("--vgm_eps", type=float, default=4/255)
|
| 205 |
+
p.add_argument("--skip_robustness", action="store_true")
|
| 206 |
+
a = p.parse_args()
|
| 207 |
+
config = PipelineConfig(input_dir=a.input_dir, output_dir=a.output_dir, methods=a.methods, attacks=a.attacks, device=a.device,
|
| 208 |
+
fs_total_iter=a.fs_iter, fs_noise_clamp=a.fs_noise, dg_iters=a.dg_iter, dg_eps=a.dg_eps, vgm_steps=a.vgm_steps, vgm_eps=a.vgm_eps,
|
| 209 |
+
preprocessing_tests=[] if a.skip_robustness else None)
|
| 210 |
+
run_pipeline(config)
|
| 211 |
+
|
| 212 |
+
if __name__ == "__main__": main()
|