Classification : Documentation Technique — Niveau Développeur
Version : 4.0 FORENSIC & HERITAGE UNIFIED
Langage : Python 3.x (PIL/Pillow + NumPy)
- Architecture du Système d'Effets
- Structure d'un Effet
- API Interne Disponible
- Catégories d'Effets
- Guide de Développement Pas à Pas
- Techniques Avancées
- Optimisation des Performances
- Débogage et Tests
- Référence des Effets Intégrés
- Bonnes Pratiques
Le système d'effets repose sur une architecture déclarative + impérative :
┌─────────────────────────────────────────────────────────────┐
│ effects.json (Déclaratif) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Métadonnées │ │ Paramètres │ │ Code Python (str) │ │
│ │ (id, name) │ │ (dict) │ │ (exécuté via exec) │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ ImageAnalyzerApp._apply_effect_and_adjustments() │
│ (Moteur d'exécution) │
│ 1. Charge l'effet depuis effects.json │
│ 2. Exécute le code via `apply_effect()` │
│ 3. Applique les ajustements (brightness, contrast...) │
│ 4. Rafraîchit l'affichage │
└─────────────────────────────────────────────────────────────┘
# 1. Sélection de l'effet dans la ComboBox
self.current_effect_id = "mon_effet"
# 2. Déclenchement de _apply_effect_and_adjustments()
# → Charge l'image originale (RGB)
# → Exécute apply_effect(img, effect)
# → Applique les ajustements utilisateur
# → Met à jour l'affichage
# 3. Fonction apply_effect() (dans image_analyzer.py)
def apply_effect(img: Image.Image, effect: dict) -> Image.Image:
params = effect.get("params", {})
code = effect.get("code", "return img.copy()")
local_ns = {"img": img, "params": params}
# Le code est wrappé dans une fonction
wrapped = "def _effect(img, params):\n"
for line in code.splitlines():
wrapped += " " + line + "\n"
exec(wrapped, local_ns)
return local_ns["_effect"](img, params)
⚠️ Important : Le code des effets est exécuté viaexec(). Cela offre une grande flexibilité mais implique que :
- Tout module Python disponible dans l'environnement peut être importé
- Les erreurs de syntaxe/runtime sont capturées et affichées dans une boîte de dialogue
- Le code s'exécute dans le namespace local de
apply_effect()
{
"id": "mon_effet_unique",
"name": "Mon Effet Pro",
"category": "custom",
"description": "Description courte et claire de ce que fait l'effet",
"params": {
"param1": 42,
"param2": "#ff0000",
"param3": true
},
"code": "import numpy as np\nfrom PIL import Image\n# Votre code ici\nreturn img.copy()"
}| Champ | Type | Description |
|---|---|---|
id |
str |
Identifiant unique, snake_case, sans espaces |
name |
str |
Nom affiché dans l'interface (préfixer [CUSTOM] pour les effets personnalisés) |
category |
str |
Catégorie fonctionnelle (voir §4) |
description |
str |
Description affichée sous le sélecteur d'effets |
params |
dict |
Paramètres par défaut (peut être vide {}) |
code |
str |
Code Python exécutable (doit retourner un PIL.Image.Image) |
# IDs des effets intégrés
base_*, color_*, forensic_*, enhancement_*, artistic_*,
distortion_*, blur_*, stylize_*, lighting_*,
paleo_*, epi_*, stego_*, custom_*
# IDs des effets custom (créés par l'utilisateur)
# → Préfixer le name avec [CUSTOM] automatiquement
# → L'ID est fourni par l'utilisateur dans l'éditeur
Lors de l'exécution du code d'un effet, deux variables sont automatiquement injectées :
| Variable | Type | Description |
|---|---|---|
img |
PIL.Image.Image |
Image d'entrée (toujours en mode compatible, souvent RGB) |
params |
dict |
Dictionnaire des paramètres avec leurs valeurs actuelles |
Le code d'effet peut importer tout module installé dans l'environnement Python :
# Modules garantis (dépendances de l'application)
from PIL import Image, ImageFilter, ImageEnhance, ImageOps, ImageDraw, ImageChops
import numpy as np
import math
import colorsys
import io
import json
# Modules optionnels (si installés)
import cv2 # OpenCV — pour les opérations avancées
from scipy import ndimage, signal # SciPy — pour le traitement signal
from scipy.interpolate import interp1dBien que non directement accessibles depuis le code d'effet, les fonctions suivantes du fichier image_analyzer.py peuvent être reproduites :
# Conversion HEX → RGB
def hex_to_rgb(hex_str):
h = hex_str.lstrip('#')
return tuple(int(h[i:i+2], 16) for i in (0, 2, 4))
# Conversion RGB → HSV (vecteurisé)
# → Utiliser colorsys.rgb_to_hsv() ou numpy| Catégorie | Préfixe ID | Usage |
|---|---|---|
base |
base_, auto_, flip_ |
Opérations de base (rotation, recadrage...) |
color |
color_, sepia, duotone |
Manipulation des canaux couleur |
forensic |
forensic_, ela, noise_ |
Analyse forensique et détection de falsification |
enhancement |
enhance_, sharpen_, hdr_ |
Amélioration de la qualité image |
artistic |
emboss, pixelate, glitch |
Effets artistiques et rétro |
distortion |
fisheye, swirl, ripple |
Distorsions géométriques |
blur |
motion_blur, bokeh, zoom_blur |
Effets de flou |
stylize |
comic_book, neon_glow, cyberpunk |
Styles visuels prédéfinis |
lighting |
lens_flare, god_rays, bloom |
Effets d'éclairage |
paleography |
paleo_ |
Traitement de manuscrits, parchemins, palimpsestes |
epigraphy |
epi_ |
Traitement d'inscriptions sur pierre/métal |
steganography |
stego_ |
Détection et extraction de données cachées |
custom |
custom_ |
Effets créés par l'utilisateur |
Les effets sont affichés dans l'ordre du fichier effects.json. L'ordre est :
none(toujours en premier)- Catégories dans l'ordre d'apparition
- Effets custom à la fin
Objectif : Créer un effet "Teinte Rouge" qui accentue le canal rouge.
{
"id": "custom_red_boost",
"name": "[CUSTOM] Boost Rouge",
"category": "custom",
"description": "Accentue le canal rouge de l'image",
"params": {
"intensity": 1.5
},
"code": "..."
}import numpy as np
from PIL import Image
# Récupérer le paramètre
intensity = params.get('intensity', 1.5)
# Convertir en array NumPy
arr = np.array(img.convert('RGB'), dtype=np.float32)
# Appliquer l'effet : multiplier le canal rouge
arr[:,:,0] = np.clip(arr[:,:,0] * intensity, 0, 255)
# Retourner un PIL.Image
return Image.fromarray(arr.astype(np.uint8)){
"id": "custom_red_boost",
"name": "[CUSTOM] Boost Rouge",
"category": "custom",
"description": "Accentue le canal rouge de l'image",
"params": {
"intensity": 1.5
},
"code": "import numpy as np\nfrom PIL import Image\nintensity = params.get('intensity', 1.5)\narr = np.array(img.convert('RGB'), dtype=np.float32)\narr[:,:,0] = np.clip(arr[:,:,0] * intensity, 0, 255)\nreturn Image.fromarray(arr.astype(np.uint8))"
}💡 Astuce : Utilisez l'Éditeur d'Effet Custom intégré à l'application (bouton "✎ AJOUTER EFFET CUSTOM") pour éviter d'écrire le JSON manuellement.
Objectif : Effet "Vignette Radiale" avec couleur personnalisable.
import numpy as np
from PIL import Image
# Paramètres
color_str = params.get('color', '#000000')
strength = params.get('strength', 2.5)
# Convertir HEX → RGB
hex_val = color_str.lstrip('#')
r, g, b = tuple(int(hex_val[i:i+2], 16) for i in (0, 2, 4))
# Image en array
arr = np.array(img.convert('RGB'), dtype=np.float32)
h, w = arr.shape[:2]
# Calcul de la distance au centre
Y, X = np.ogrid[:h, :w]
cx, cy = w/2, h/2
dist = np.sqrt(((X-cx)/cx)**2 + ((Y-cy)/cy)**2)
# Masque de vignette (1 au centre, 0 aux bords)
mask = 1 - np.clip(dist * strength * 0.5, 0, 1)
# Application : interpolation entre l'image et la couleur
for c, val in enumerate([r, g, b]):
arr[:,:,c] = arr[:,:,c] * mask + val * (1 - mask)
return Image.fromarray(arr.clip(0,255).astype(np.uint8))Objectif : Détection de zones retouchées par analyse du bruit.
import numpy as np
from PIL import Image, ImageFilter
# Paramètres
sigma = params.get('sigma', 1.5)
# Image originale en float
arr = np.array(img.convert('RGB'), dtype=np.float32)
# Version lissée (supprime le bruit naturel)
smooth = np.array(
img.filter(ImageFilter.GaussianBlur(radius=sigma)).convert('RGB'),
dtype=np.float32
)
# Bruit résiduel = différence amplifiée
noise = np.abs(arr - smooth) * 4
noise = np.clip(noise, 0, 255)
# Le bruit non uniforme indique une retouche
return Image.fromarray(noise.astype(np.uint8))❌ À ÉVITER (lent) :
# Boucles Python sur les pixels — TRÈS LENT
for i in range(arr.shape[0]):
for j in range(arr.shape[1]):
r, g, b = arr[i,j]
arr[i,j] = [r*2, g, b]✅ RECOMMANDÉ (rapide) :
# Vectorisation NumPy — RAPIDE
arr[:,:,0] = np.clip(arr[:,:,0] * 2, 0, 255)# Créer un masque conditionnel
bright_mask = arr.mean(axis=2) > 200
# Appliquer selectivement
result = arr.copy()
result[bright_mask] = result[bright_mask] * 1.2from PIL import ImageFilter
# Kernel personnalisé (ex: sharpen)
kernel = ImageFilter.Kernel(
size=(3, 3),
kernel=[0, -1, 0, -1, 5, -1, 0, -1, 0],
scale=1,
offset=0
)
return img.filter(kernel).convert('RGB')# Séparer les canaux
r, g, b = img.split()
# Traiter individuellement
r = r.point(lambda x: min(255, int(x * 1.2)))
# Recombiner
return Image.merge('RGB', (r, g, b))from scipy import ndimage
# Filtre morphologique
binary = gray > 128
dilated = ndimage.binary_dilation(binary, iterations=2)
# Mesures de régions
labels, num = ndimage.label(binary)
sizes = ndimage.sum(binary, labels, range(1, num+1))from PIL import ImageFilter
import numpy as np
# Gradient Sobel approximé
def sobel_edge(gray_img):
arr = np.array(gray_img, dtype=np.float32)
# Kernels Sobel
kx = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]])
ky = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]])
gx = ndimage.convolve(arr, kx)
gy = ndimage.convolve(arr, ky)
magnitude = np.sqrt(gx**2 + gy**2)
return Image.fromarray(np.clip(magnitude, 0, 255).astype(np.uint8))| Règle | Impact | Exemple |
|---|---|---|
| Vectoriser avec NumPy | ×10 à ×100 | Remplacer les boucles par des opérations array |
| Éviter les conversions inutiles | ×2 | Minimiser Image ↔ array |
| Utiliser des tailles réduites pour les tests | Instantané | Tester sur 64×64 avant l'image complète |
| Préférer PIL pour les opérations simples | ×2-5 | ImageEnhance vs NumPy pour brightness |
| Éviter les imports conditionnels dans la boucle | Mineur | Importer en début de code |
# ❌ Mauvais : import dans le code
if some_condition:
import cv2 # Lent si répété
# ✅ Bon : import en haut du code
try:
import cv2
except ImportError:
cv2 = None
# ❌ Mauvais : création d'image intermédiaire inutile
arr = np.array(img.convert('RGB'))
result = Image.fromarray(arr)
arr2 = np.array(result.convert('RGB')) # Inutile !
# ✅ Bon : garder l'array
arr = np.array(img.convert('RGB'), dtype=np.float32)
# ... traitements ...
return Image.fromarray(arr.astype(np.uint8))# Libérer explicitement les grands arrays intermédiaires
import gc
large_temp = np.zeros((4000, 6000, 3), dtype=np.float32)
# ... traitements ...
del large_temp
gc.collect()L'application dispose d'un testeur d'effet intégré :
- Cliquez sur "✎ AJOUTER EFFET CUSTOM"
- Remplissez les champs
- Cliquez sur "[ TESTER ]"
- Le test s'exécute sur une image 64×64 de test
| Erreur | Cause | Solution |
|---|---|---|
NameError: name 'Image' is not defined |
Import manquant | Ajouter from PIL import Image |
ValueError: cannot convert float NaN to integer |
Valeurs NaN/Inf dans l'array | Utiliser np.nan_to_num() ou np.clip() |
TypeError: 'NoneType' object is not subscriptable |
params.get() retourne None |
Fournir une valeur par défaut : params.get('key', default) |
MemoryError |
Image trop grande | Réduire la résolution ou optimiser |
ModuleNotFoundError: No module named 'cv2' |
OpenCV non installé | Utiliser try/except avec fallback |
# Ajouter des prints (visibles dans la console Python)
print(f"DEBUG: img.size = {img.size}")
print(f"DEBUG: params = {params}")
# Vérifier les valeurs min/max
arr = np.array(img.convert('RGB'), dtype=np.float32)
print(f"DEBUG: min={arr.min()}, max={arr.max()}, mean={arr.mean()}")
# Sauvegarder une image intermédiaire (pour inspection)
# temp = Image.fromarray(arr.astype(np.uint8))
# temp.save('/tmp/debug_step1.png')# Script de test indépendant
from PIL import Image
import json
# Charger l'effet
with open('effects.json') as f:
data = json.load(f)
effect = next(e for e in data['effects'] if e['id'] == 'mon_effet')
# Créer une image de test
test_img = Image.new('RGB', (256, 256), (128, 64, 200))
# Exécuter (copier la logique de apply_effect)
params = effect.get("params", {})
code = effect.get("code", "return img.copy()")
local_ns = {"img": test_img, "params": params}
wrapped = "def _effect(img, params):\n"
for line in code.splitlines():
wrapped += " " + line + "\n"
exec(wrapped, local_ns)
result = local_ns["_effect"](test_img, params)
# Vérifier
assert isinstance(result, Image.Image)
assert result.size == (256, 256)
print("✓ Test passed")| ID | Nom | Description |
|---|---|---|
none |
Aucun effet | Passe-through |
auto_crop |
Recadrage Auto | Supprime les bords vides |
auto_rotate |
Rotation Auto | Applique l'orientation EXIF |
flip_mirror |
Miroir Horizontal | Retournement horizontal |
| ID | Paramètres Clés | Technique |
|---|---|---|
grayscale |
— | ImageOps.grayscale() |
invert |
— | ImageOps.invert() |
sepia |
— | Matrice de transformation RGB |
duotone |
color1, color2 |
Interpolation linéaire entre deux couleurs |
color_temperature |
temp |
Ajustement chaud/froid |
vibrance |
amount |
Saturation intelligente |
colorize |
hue, saturation |
Colorisation monochrome |
cross_process |
— | Courbes de tonalité croisées |
black_white_split |
black_point, white_point |
Expansion des tons |
split_toning |
shadow_hue, highlight_hue |
Tons partagés ombres/lumières |
| ID | Paramètres | Usage |
|---|---|---|
thermal |
— | Pseudo-colorisation thermique FLIR |
night_vision |
— | NVG militaire (vert phosphore) |
edge_detection |
— | Filtre FIND_EDGES + boost |
histogram_equalize |
— | Égalisation d'histogramme |
clahe |
— | CLAHE (requiert OpenCV) |
forensic_enhance |
— | Pipeline NCIS (sharpen + contraste + égalisation) |
ela |
quality, amplify |
Error Level Analysis |
noise_analysis |
sigma |
Visualisation du bruit résiduel |
shadow_recovery |
lift |
Extraction des détails dans les ombres |
highlight_recovery |
compress |
Récupération des hautes lumières |
frequency_separation |
mode |
Séparation haute/basse fréquence |
difference_blend |
— | Mode différence |
grain_analysis |
— | Analyse du grain de compression |
channel_isolation |
channel |
Isolation RGB individuelle |
local_contrast |
amount, radius |
Unsharp Mask avancé |
| ID | Paramètres | Principe |
|---|---|---|
stego_lsb_extract |
bit_mask, channel |
Extraction des bits de poids faible |
stego_entropy_map |
radius |
Cartographie d'entropie locale |
forensic_bit_plane_slicing |
plane |
Décomposition en plans de bits (0-7) |
forensic_laplacian_noise |
gain |
Extraction du bruit de capteur |
| ID | Paramètres | Application |
|---|---|---|
paleo_ink_enhance |
ink_channel, sensitivity |
Extraction encre délavée |
paleo_fade_recovery |
recovery_boost |
Récupération encre effacée |
paleo_ghost_text |
layer_depth |
Texte palimpseste |
paleo_raking_light |
angle, elevation |
Lumière rasante RTI |
paleo_multispectral |
band |
Fausses couleurs multispectral |
paleo_stain_remove |
stain_threshold |
Suppression taches |
paleo_line_thinning |
thin_factor |
Amincissement traits |
paleo_letter_separation |
separation_strength |
Séparation ligatures |
paleo_ink_corrosion |
corrosion_threshold |
Inversion corrosion |
paleo_parchment_flatten |
smoothness |
Aplatissement parchemin |
paleo_ductus_reveal |
sensitivity |
Révélation ductus |
paleo_illumination_enhance |
saturation_threshold |
Extraction enluminures |
paleo_ir_simulation |
penetration_depth |
Réflectographie IR |
paleo_watermark_reveal |
sensitivity |
Filigranes papier |
paleo_text_stabilize |
line_smooth |
Stabilisation texte |
paleo_ink_dating |
analysis_mode |
Datation spectrale indicative |
| ID | Paramètres | Application |
|---|---|---|
epi_erosion_compensate |
erosion_type |
Reconstruction érosion |
epi_lichen_remove |
green_suppress |
Suppression lichens |
epi_raking_enhance |
light_angle |
Lumière rasante mur |
epi_depth_map |
scale |
Carte de profondeur |
epi_moss_remove |
moisture_threshold |
Suppression mousse |
epi_patina_remove |
patina_type |
Suppression patine |
epi_crack_repair |
crack_width |
Réparation fissures |
epi_3d_relief |
relief_height |
Reconstruction 3D |
epi_sandblast_restore |
wind_direction |
Restauration sablage |
epi_multiview_stereo |
n_lights |
Stéréo-photométrie |
epi_weathering_reverse |
weathering_type |
Inversion altération |
epi_ink_trace |
red_threshold |
Traces encre rouge |
epi_surface_clean |
clean_strength |
Nettoyage surface |
epi_pigment_analysis |
pigment |
Analyse pigments |
epi_rti_simple |
n_angles |
RTI simplifié |
epi_inscription_extract |
method |
Extraction inscription |
epi_stroke_enhance |
line_width |
Renforcement traits |
Avant de sauvegarder un effet, vérifiez :
- Le code retourne toujours un objet
PIL.Image.Image - Les paramètres ont des valeurs par défaut sensibles
- Les valeurs sont clippées entre 0 et 255 avant conversion
uint8 - Les imports optionnels (cv2, scipy) sont dans un try/except
- L'ID est unique et en snake_case
- Le nom est descriptif et préfixé par
[CUSTOM]si applicable - Le code fonctionne sur une image 64×64 (test intégré)
- Le code fonctionne sur une image grande résolution (pas de MemoryError)
# ✅ Style recommandé
import numpy as np
from PIL import Image
# 1. Extraire les paramètres avec valeurs par défaut
strength = params.get('strength', 1.0)
color = params.get('color', '#ffffff')
# 2. Convertir l'image
arr = np.array(img.convert('RGB'), dtype=np.float32)
# 3. Traitement (vectorisé)
result = arr * strength
# 4. Clip et conversion
result = np.clip(result, 0, 255)
# 5. Retour
return Image.fromarray(result.astype(np.uint8))# Fallback gracieux
try:
import cv2
# Code utilisant OpenCV
result = cv2.some_operation(...)
except ImportError:
# Fallback avec PIL
from PIL import ImageFilter
result = img.filter(ImageFilter.GaussianBlur(radius=2))
# Toujours retourner un Image
return result.convert('RGB')# Description de l'algorithme
# Algorithme : Unsharp Mask avec masque de luminance
# Référence : Pratt, W.K. "Digital Image Processing"
# Paramètres : amount (1.0-3.0), radius (1-10)
import numpy as np
from PIL import Image, ImageFilter
amount = params.get('amount', 1.5)
rad = params.get('radius', 5)
arr = np.array(img.convert('RGB'), dtype=np.float32)
blur = np.array(img.filter(ImageFilter.GaussianBlur(radius=rad)).convert('RGB'), dtype=np.float32)
# Masque = image originale - image floue
mask = arr - blur
# Application du masque amplifié
result = arr + mask * amount
return Image.fromarray(result.clip(0,255).astype(np.uint8))| Source | Conversion | Destination |
|---|---|---|
PIL.Image |
np.array(img) |
ndarray uint8 |
ndarray uint8 |
arr.astype(np.float32) |
ndarray float32 |
ndarray float32 |
np.clip(arr, 0, 255) |
ndarray float32 (safe) |
ndarray float32 |
arr.astype(np.uint8) |
ndarray uint8 |
ndarray uint8 |
Image.fromarray(arr) |
PIL.Image |
ndarray uint8 (H,W) |
Image.fromarray(arr).convert('RGB') |
PIL.Image RGB |
# Palette phosphore INGEN Systems (pour référence)
C_BG = "#050a05" # Fond principal
C_BG2 = "#0a0f0a" # Fond secondaire
C_BG3 = "#0f150f" # Fond tertiaire
C_PANEL = "#0d1a0d" # Panneaux
C_BORDER = "#1a3a1a" # Bordures
C_GREEN = "#00ff41" # Vert phosphore (principal)
C_GREEN2 = "#00cc33" # Vert secondaire
C_GREEN3 = "#008822" # Vert foncé
C_AMBER = "#ffb000" # Ambre (alertes)
C_RED = "#ff2222" # Rouge (erreurs)
C_CYAN = "#00e5ff" # Cyan (liens)
C_WHITE = "#e8ffe8" # Blanc cassé
C_DIM = "#3a5a3a" # Vert tamisé{
"id": "custom_hidden_text",
"name": "[CUSTOM] Texte Caché — Détection",
"category": "custom",
"description": "Détecte le texte caché sous une couche de peinture ou d'encre par analyse de la réponse spectrale différentielle",
"params": {
"layer_depth": 0.6,
"contrast_boost": 2.5,
"blue_sensitivity": 1.8
},
"code": "import numpy as np\nfrom PIL import Image, ImageFilter\n\n# Paramètres\ndepth = params.get('layer_depth', 0.6)\nboost = params.get('contrast_boost', 2.5)\nblue_sens = params.get('blue_sensitivity', 1.8)\n\n# Chargement\narr = np.array(img.convert('RGB'), dtype=np.float32)\n\n# Séparation surface / profondeur\nsurface = np.array(img.filter(ImageFilter.GaussianBlur(radius=20)).convert('RGB'), dtype=np.float32)\ndeep = arr - surface * depth\n\n# Analyse spectrale : l'encre profonde réagit différemment au bleu\n# (principe de la réflectographie multispectral simplifié)\nblue_response = deep[:,:,2] * blue_sens - deep[:,:,0] * 0.3\n\n# Normalisation\nblue_response = (blue_response - blue_response.min()) / (blue_response.max() - blue_response.min() + 1e-6) * 255\n\n# Boost de contraste\nblue_response = np.clip((blue_response - 128) * boost + 128, 0, 255)\n\n# Visualisation en fausses couleurs\nresult = np.zeros((arr.shape[0], arr.shape[1], 3), dtype=np.float32)\nresult[:,:,0] = blue_response * 0.2 # Rouge faible\nresult[:,:,1] = blue_response * 0.5 # Vert moyen\nresult[:,:,2] = blue_response # Bleu fort\n\n# Superposition avec l'original pour le contexte\nresult = result * 0.7 + arr * 0.3\n\nreturn Image.fromarray(result.clip(0,255).astype(np.uint8))"
}INGEN SYSTEMS — Classified Analysis Tool — Authorized Personnel Only
Documentation générée pour la version 4.0 FORENSIC & HERITAGE UNIFIED