Pe ultima „sută de metri” a campaniei electorale pentru alegerile prezidențiale din România, un set de fotografii a stârnit un val de reacții publice, controverse aprinse și speculații mediatice. Respectivele fotografii i-au adus în prim-plan pe Nicușor Dan – candidat la președinție și pe Florian Coldea – fost director adjunct al SRI.
Fotografiile au fost publicate în mediul online de Elena Lasconi, la rândul ei candidat pentru fotoliul de la Cotroceni. Lasconi a fost cea care l-a întrebat pe Nicușor Dan, în timpul uneia dintre cele trei dezbateri electorale, dacă a avut și o altă întâlnire cu Florian Coldea în afară de cea despre care vorbise, de la Ambasada Franței.
Dincolo de contextul politic și de momentul apariției acestor fotografii în spațiul public, s-au pus și mai multe întrebări extrem de interesante.
Acest articol răspunde tehnic la aceste întrebări, printr-o analiză completă forensică și steganografică.
Vom parcurge următorii pași pentru fiecare fotografie:
ÿ
→ nimic semnificativ.Fotografiile au trecut cu brio testele pentru:
Din punct de vedere forensic, nu există dovezi că oricare dintre ele ar ascunde informații secrete sau ar fi fost modificate (manual sau cu AI).
pip install pillow # manipulare imagini, EXIF, ELA
pip install opencv-python # procesare imagini, FFT, DCT, ORB
pip install numpy # calcule matriciale
pip install matplotlib # vizualizări (histograme, ELA, FFT)
pip install pandas # tabele (metadate, cuantizare)
pip install piexif # extras opțional de EXIF mai avansat
forensic_analysis.py
)#!/usr/bin/env python3
import sys
from io import BytesIO
from PIL import Image, ImageChops, ImageEnhance, ExifTags, ImageFilter
import piexif
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import cv2
def extract_exif(img: Image.Image) -> pd.DataFrame:
# încearcă cu piexif, fallback la PIL
metadata = []
try:
exif_dict = piexif.load(img.info['exif'])
for ifd in exif_dict:
for tag_id, value in exif_dict[ifd].items():
tag = piexif.TAGS[ifd].get(tag_id, {'name':tag_id})['name']
metadata.append({'Tag': f"{ifd}.{tag}", 'Value': value})
except Exception:
raw = img.info.get('exif')
if raw:
metadata.append({'Tag': 'RawBytesLength', 'Value': len(raw)})
return pd.DataFrame(metadata)
def reveal_lsb(img_path: str) -> str:
arr = np.array(Image.open(img_path))
blue = arr[:, :, 2].flatten() & 1
bits = blue.reshape(-1, 8)
chars = [chr(int("".join(b.astype(str)), 2)) for b in bits]
message = "".join(chars).split('\x00')[0]
return message if message.strip() else None
def quant_tables(img: Image.Image) -> pd.DataFrame:
rows = []
qt = img.quantization if hasattr(img, 'quantization') else {}
for table_id, table in qt.items():
for idx, val in enumerate(table):
rows.append({'Table': table_id, 'Index': idx, 'Value': val})
return pd.DataFrame(rows)
def error_level_analysis(img: Image.Image, quality=90, scale=10):
buf = BytesIO()
img.save(buf, format='JPEG', quality=quality)
buf.seek(0)
recompressed = Image.open(buf)
diff = ImageChops.difference(img, recompressed)
return ImageEnhance.Brightness(diff).enhance(scale)
def noise_std(img: Image.Image, blur_radius=2) -> float:
blur = img.filter(ImageFilter.GaussianBlur(radius=blur_radius))
noise = np.array(img, float) - np.array(blur, float)
return float(np.std(noise))
def fourier_spectrum(gray: np.ndarray):
f = np.fft.fft2(gray)
fshift = np.fft.fftshift(f)
return np.log(np.abs(fshift) + 1)
def dct_coeffs_hist(gray: np.ndarray, pos=(1,2), bins=50):
h, w = gray.shape
coeffs = []
for i in range(0, h, 8):
for j in range(0, w, 8):
block = gray[i:i+8, j:j+8]
if block.shape == (8,8):
d = cv2.dct(block.astype(np.float32))
coeffs.append(d[pos])
plt.hist(coeffs, bins=bins)
plt.title(f"DCT coef dist {pos}")
plt.xlabel("Value")
plt.ylabel("Frequency")
plt.show()
def copy_move_orb(gray: np.ndarray, n_features=2000, match_count=100):
orb = cv2.ORB_create(n_features)
kp, des = orb.detectAndCompute(gray, None)
bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
matches = bf.match(des, des)
# filtrare self-match și apropiere
good = []
for m in matches:
if m.queryIdx != m.trainIdx:
p1 = np.array(kp[m.queryIdx].pt)
p2 = np.array(kp[m.trainIdx].pt)
if np.linalg.norm(p1-p2) > 20:
good.append(m)
good = sorted(good, key=lambda x: x.distance)[:match_count]
vis = cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)
for m in good:
pt1 = tuple(map(int, kp[m.queryIdx].pt))
pt2 = tuple(map(int, kp[m.trainIdx].pt))
cv2.line(vis, pt1, pt2, (255,0,0), 1)
plt.imshow(cv2.cvtColor(vis, cv2.COLOR_BGR2RGB))
plt.title("Copy–Move ORB matches")
plt.axis('off')
plt.show()
def analyze_image(path: str):
print(f"\n=== Analiză {path} ===")
img = Image.open(path).convert('RGB')
gray = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
# 1. Metadate
df_exif = extract_exif(img)
print("\n>> EXIF metadata:")
print(df_exif if not df_exif.empty else " ")
# 2. Stego LSB
msg = reveal_lsb(path)
print("\n>> LSB hidden:", msg or "")
# 3. Quant tables
df_q = quant_tables(img)
print("\n>> JPEG quant tables (first 16 intrări):")
print(df_q.head(16))
# 4. ELA
ela = error_level_analysis(img)
plt.imshow(ela)
plt.title("ELA (×10)")
plt.axis('off')
plt.show()
# 5. Noise
ns = noise_std(img)
print(f"\n>> Noise σ: {ns:.2f}")
# 6. Fourier
spec = fourier_spectrum(gray)
plt.imshow(spec, cmap='viridis')
plt.title("Fourier Spectrum")
plt.axis('off')
plt.show()
# 7. DCT histogram
dct_coeffs_hist(gray)
# 8. Copy–move
copy_move_orb(gray)
if __name__ == "__main__":
if len(sys.argv) [ ...]")
sys.exit(1)
for img_path in sys.argv[1:]:
analyze_image(img_path)
-------------
chmod +x forensic_analysis.py
./forensic_analysis.py coldea.jpg nicusor_2.jpg ponta.jpg
-------------
NOTĂ - Acest script va parcurge toate cele 8 etape pentru fiecare fișier JPEG furnizat și va afișa:
Citește și: