stegmark 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- stegmark/__init__.py +27 -0
- stegmark/_detectors.py +44 -0
- stegmark/cli.py +13 -0
- stegmark/core.py +187 -0
- stegmark/report.py +70 -0
- stegmark/robustness.py +162 -0
- stegmark-1.0.0.dist-info/METADATA +22 -0
- stegmark-1.0.0.dist-info/RECORD +11 -0
- stegmark-1.0.0.dist-info/WHEEL +5 -0
- stegmark-1.0.0.dist-info/entry_points.txt +2 -0
- stegmark-1.0.0.dist-info/top_level.txt +1 -0
stegmark/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Stegmark — Universal Steganographic Analysis
|
|
3
|
+
=============================================
|
|
4
|
+
pip install stegmark
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from stegmark import analyze, robustness_test
|
|
8
|
+
|
|
9
|
+
# Analyze a single image
|
|
10
|
+
result = analyze("image.jpg")
|
|
11
|
+
print(result.verdict)
|
|
12
|
+
print(result.confidence)
|
|
13
|
+
result.save_report("audit.pdf")
|
|
14
|
+
|
|
15
|
+
# Test watermark robustness
|
|
16
|
+
report = robustness_test("cover.jpg", secret="my_watermark")
|
|
17
|
+
print(f"Survived {report.survival_rate:.0%} of attacks")
|
|
18
|
+
report.save_report("robustness_audit.pdf")
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from stegmark.core import analyze, AnalysisResult
|
|
22
|
+
from stegmark.robustness import robustness_test, RobustnessResult
|
|
23
|
+
from stegmark.report import generate_report
|
|
24
|
+
|
|
25
|
+
__version__ = "1.0.0"
|
|
26
|
+
__all__ = ["analyze", "robustness_test", "generate_report",
|
|
27
|
+
"AnalysisResult", "RobustnessResult"]
|
stegmark/_detectors.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
stegmark._detectors — internal detector wrappers
|
|
3
|
+
Bridges the package API to the underlying detector modules.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os, sys, dataclasses
|
|
7
|
+
_PKG_DIR = os.path.dirname(__file__)
|
|
8
|
+
sys.path.insert(0, _PKG_DIR)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run_statistical(image_path: str, cover_path=None) -> dict:
|
|
12
|
+
from steganalysis import run_report
|
|
13
|
+
report = run_report(image_path, cover_path)
|
|
14
|
+
return dataclasses.asdict(report)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def run_neural(image_path: str) -> dict:
|
|
18
|
+
try:
|
|
19
|
+
from neural_watermark import run_neural_detection
|
|
20
|
+
nd = run_neural_detection(image_path)
|
|
21
|
+
return {
|
|
22
|
+
"attempted": True,
|
|
23
|
+
"any_detected": nd.any_detected,
|
|
24
|
+
"detected_schemes": nd.detected_schemes,
|
|
25
|
+
"detail": nd.detail,
|
|
26
|
+
"schemes": [dataclasses.asdict(s) for s in nd.schemes],
|
|
27
|
+
}
|
|
28
|
+
except ImportError:
|
|
29
|
+
return {
|
|
30
|
+
"attempted": False,
|
|
31
|
+
"error": "pip install trustmark torch torchvision",
|
|
32
|
+
"schemes": [],
|
|
33
|
+
}
|
|
34
|
+
except Exception as e:
|
|
35
|
+
return {"attempted": True, "error": str(e), "schemes": []}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def run_srnet_detection(image_path: str) -> dict:
|
|
39
|
+
try:
|
|
40
|
+
from srnet_detector import detect_srnet
|
|
41
|
+
sr = detect_srnet(image_path)
|
|
42
|
+
return dataclasses.asdict(sr)
|
|
43
|
+
except Exception as e:
|
|
44
|
+
return {"attempted": False, "error": str(e)}
|
stegmark/cli.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
stegmark CLI — installed as `stegmark` command after pip install
|
|
3
|
+
"""
|
|
4
|
+
import sys, os
|
|
5
|
+
sys.path.insert(0, os.path.dirname(__file__))
|
|
6
|
+
|
|
7
|
+
def main():
|
|
8
|
+
# Delegate to run.py's main
|
|
9
|
+
from run import main as run_main
|
|
10
|
+
run_main()
|
|
11
|
+
|
|
12
|
+
if __name__ == "__main__":
|
|
13
|
+
main()
|
stegmark/core.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""
|
|
2
|
+
stegmark.core — main analyze() function
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import dataclasses
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
|
|
11
|
+
# Add package internals to path
|
|
12
|
+
_PKG_DIR = os.path.dirname(__file__)
|
|
13
|
+
sys.path.insert(0, _PKG_DIR)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class AnalysisResult:
|
|
18
|
+
"""
|
|
19
|
+
Full steganalysis result for a single image.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
verdict Plain-language verdict string
|
|
23
|
+
confidence 0.0–1.0 aggregate confidence stego is present
|
|
24
|
+
detected_by List of detector names that fired
|
|
25
|
+
image_path Path to analyzed image
|
|
26
|
+
cover_path Path to cover image (if provided)
|
|
27
|
+
|
|
28
|
+
# Detector results
|
|
29
|
+
chi_square JPEG DCT equalization score result
|
|
30
|
+
rs_analysis RS analysis result
|
|
31
|
+
spa Sample pair analysis result
|
|
32
|
+
eof EOF hidden data result
|
|
33
|
+
exif EXIF anomaly result
|
|
34
|
+
lsb_histogram LSB uniformity result
|
|
35
|
+
multi_channel Per-channel R/G/B result
|
|
36
|
+
neural Neural watermark detection result
|
|
37
|
+
srnet SRNet adaptive steg result
|
|
38
|
+
quality PSNR/SSIM (if cover provided)
|
|
39
|
+
|
|
40
|
+
Methods:
|
|
41
|
+
save_report(path) Generate and save PDF report
|
|
42
|
+
to_dict() Return as JSON-serializable dict
|
|
43
|
+
is_clean True if no stego detected
|
|
44
|
+
is_watermarked True if neural watermark detected
|
|
45
|
+
"""
|
|
46
|
+
verdict: str
|
|
47
|
+
confidence: float
|
|
48
|
+
detected_by: list
|
|
49
|
+
image_path: str
|
|
50
|
+
cover_path: Optional[str]
|
|
51
|
+
|
|
52
|
+
# Raw detector results
|
|
53
|
+
chi_square: dict = field(default_factory=dict)
|
|
54
|
+
rs_analysis: dict = field(default_factory=dict)
|
|
55
|
+
spa: dict = field(default_factory=dict)
|
|
56
|
+
eof: dict = field(default_factory=dict)
|
|
57
|
+
exif: dict = field(default_factory=dict)
|
|
58
|
+
lsb_histogram: dict = field(default_factory=dict)
|
|
59
|
+
multi_channel: dict = field(default_factory=dict)
|
|
60
|
+
neural: dict = field(default_factory=dict)
|
|
61
|
+
srnet: dict = field(default_factory=dict)
|
|
62
|
+
quality: Optional[dict] = None
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def is_clean(self) -> bool:
|
|
66
|
+
return self.confidence < 0.05 and not self.detected_by
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def is_watermarked(self) -> bool:
|
|
70
|
+
return any("neural" in d for d in self.detected_by)
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def psnr(self) -> Optional[float]:
|
|
74
|
+
return self.quality.get("psnr_db") if self.quality else None
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def ssim(self) -> Optional[float]:
|
|
78
|
+
return self.quality.get("ssim") if self.quality else None
|
|
79
|
+
|
|
80
|
+
def save_report(self, output_path: str = None) -> str:
|
|
81
|
+
"""Generate PDF report and save to output_path."""
|
|
82
|
+
from stegmark.report import generate_report
|
|
83
|
+
if output_path is None:
|
|
84
|
+
base = os.path.splitext(self.image_path)[0]
|
|
85
|
+
output_path = f"{base}_stegmark_report.pdf"
|
|
86
|
+
generate_report(self.to_dict(), output_path,
|
|
87
|
+
self.image_path, self.cover_path or "")
|
|
88
|
+
return output_path
|
|
89
|
+
|
|
90
|
+
def to_dict(self) -> dict:
|
|
91
|
+
return dataclasses.asdict(self)
|
|
92
|
+
|
|
93
|
+
def __repr__(self):
|
|
94
|
+
return (f"AnalysisResult(verdict={self.verdict!r}, "
|
|
95
|
+
f"confidence={self.confidence:.1%}, "
|
|
96
|
+
f"detected_by={self.detected_by})")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def analyze(
|
|
100
|
+
image_path: str,
|
|
101
|
+
cover_path: Optional[str] = None,
|
|
102
|
+
neural: bool = True,
|
|
103
|
+
srnet: bool = True,
|
|
104
|
+
verbose: bool = False,
|
|
105
|
+
) -> AnalysisResult:
|
|
106
|
+
"""
|
|
107
|
+
Run complete steganalysis on an image. Runs entirely locally.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
image_path Path to the image to analyze (JPEG or PNG)
|
|
111
|
+
cover_path Path to the original cover image (optional).
|
|
112
|
+
Enables PSNR/SSIM quality metrics.
|
|
113
|
+
neural Run neural watermark detectors (TrustMark, Stable Signature).
|
|
114
|
+
Requires: pip install trustmark torch torchvision
|
|
115
|
+
srnet Run SRNet adaptive steg detector if checkpoint available.
|
|
116
|
+
Train with: stegmark train (runs overnight on Modal)
|
|
117
|
+
verbose Print progress to stdout
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
AnalysisResult with .verdict, .confidence, .detected_by,
|
|
121
|
+
and per-detector results. Call .save_report() to get a PDF.
|
|
122
|
+
|
|
123
|
+
Example:
|
|
124
|
+
from stegmark import analyze
|
|
125
|
+
|
|
126
|
+
result = analyze("watermarked_image.jpg")
|
|
127
|
+
print(result.verdict)
|
|
128
|
+
# "WATERMARK DETECTED — neural (trustmark)"
|
|
129
|
+
|
|
130
|
+
result.save_report("audit_report.pdf")
|
|
131
|
+
|
|
132
|
+
# With cover for quality metrics:
|
|
133
|
+
result = analyze("stego.jpg", cover_path="original.jpg")
|
|
134
|
+
print(f"PSNR: {result.psnr:.1f} dB")
|
|
135
|
+
"""
|
|
136
|
+
from stegmark._detectors import (
|
|
137
|
+
run_statistical, run_neural, run_srnet_detection
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if verbose:
|
|
141
|
+
print(f"[stegmark] Analyzing {image_path}...")
|
|
142
|
+
|
|
143
|
+
# Phase 0: statistical + forensic
|
|
144
|
+
if verbose: print("[stegmark] Running statistical detectors...")
|
|
145
|
+
stat = run_statistical(image_path, cover_path)
|
|
146
|
+
|
|
147
|
+
# Phase 1: neural watermark detection
|
|
148
|
+
neural_result = {"attempted": False, "schemes": []}
|
|
149
|
+
if neural:
|
|
150
|
+
if verbose: print("[stegmark] Running neural detectors...")
|
|
151
|
+
neural_result = run_neural(image_path)
|
|
152
|
+
if neural_result.get("any_detected"):
|
|
153
|
+
for s in neural_result.get("schemes", []):
|
|
154
|
+
if s.get("detected"):
|
|
155
|
+
label = f"{s['scheme']}-neural"
|
|
156
|
+
if label not in stat["detected_by"]:
|
|
157
|
+
stat["detected_by"].append(label)
|
|
158
|
+
if stat["confidence"] < 0.5:
|
|
159
|
+
stat["confidence"] = 1.0
|
|
160
|
+
schemes = ", ".join(neural_result.get("detected_schemes", []))
|
|
161
|
+
stat["overall_verdict"] = f"WATERMARK DETECTED — neural ({schemes})"
|
|
162
|
+
|
|
163
|
+
# Phase 2: SRNet
|
|
164
|
+
srnet_result = {"attempted": False}
|
|
165
|
+
if srnet:
|
|
166
|
+
srnet_result = run_srnet_detection(image_path)
|
|
167
|
+
if srnet_result.get("detected"):
|
|
168
|
+
if "srnet-adaptive" not in stat["detected_by"]:
|
|
169
|
+
stat["detected_by"].append("srnet-adaptive")
|
|
170
|
+
|
|
171
|
+
return AnalysisResult(
|
|
172
|
+
verdict=stat["overall_verdict"],
|
|
173
|
+
confidence=stat["confidence"],
|
|
174
|
+
detected_by=stat["detected_by"],
|
|
175
|
+
image_path=image_path,
|
|
176
|
+
cover_path=cover_path,
|
|
177
|
+
chi_square=stat.get("chi_square", {}),
|
|
178
|
+
rs_analysis=stat.get("rs_analysis", {}),
|
|
179
|
+
spa=stat.get("spa", {}),
|
|
180
|
+
eof=stat.get("eof", {}),
|
|
181
|
+
exif=stat.get("exif", {}),
|
|
182
|
+
lsb_histogram=stat.get("lsb_histogram", {}),
|
|
183
|
+
multi_channel=stat.get("multi_channel", {}),
|
|
184
|
+
neural=neural_result,
|
|
185
|
+
srnet=srnet_result,
|
|
186
|
+
quality=stat.get("quality"),
|
|
187
|
+
)
|
stegmark/report.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""
|
|
2
|
+
stegmark.report — generate_report() function
|
|
3
|
+
Thin wrapper around generate_report.py
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os, sys
|
|
7
|
+
_PKG_DIR = os.path.dirname(__file__)
|
|
8
|
+
sys.path.insert(0, _PKG_DIR)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def generate_report(result: dict, output_path: str,
|
|
12
|
+
image_filename: str = "",
|
|
13
|
+
cover_filename: str = "") -> str:
|
|
14
|
+
"""Generate a PDF report from an analysis result dict."""
|
|
15
|
+
from generate_report import generate_pdf
|
|
16
|
+
generate_pdf(result, output_path, image_filename, cover_filename)
|
|
17
|
+
return output_path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def generate_robustness_report(result, output_path: str) -> str:
|
|
21
|
+
"""Generate a PDF robustness report from a RobustnessResult."""
|
|
22
|
+
import dataclasses
|
|
23
|
+
from generate_report import generate_pdf
|
|
24
|
+
|
|
25
|
+
# Build a result dict that generate_pdf can consume
|
|
26
|
+
attacks = result.attacks
|
|
27
|
+
survived = sum(1 for a in attacks if a.survived)
|
|
28
|
+
|
|
29
|
+
# Minimal stat result for the report
|
|
30
|
+
fake_result = {
|
|
31
|
+
"chi_square": {"equalization_score": 0, "estimated_payload_rate": 0,
|
|
32
|
+
"detected": False, "threshold": 0.65, "n_pairs": 0,
|
|
33
|
+
"detail": "Robustness test — not applicable"},
|
|
34
|
+
"rs_analysis": {"payload_rate": 0, "detected": False, "threshold": 0.05,
|
|
35
|
+
"detail": "Robustness test — not applicable"},
|
|
36
|
+
"spa": {"payload_rate": 0, "detected": False, "threshold": 0.07,
|
|
37
|
+
"detail": "Robustness test — not applicable"},
|
|
38
|
+
"eof": {"has_hidden_data": False, "hidden_bytes": 0, "detail": "N/A"},
|
|
39
|
+
"exif": {"has_exif": False, "anomalies": [], "software": None,
|
|
40
|
+
"suspicious": False, "detail": "N/A"},
|
|
41
|
+
"lsb_histogram": {"lsb_uniformity": 0, "noise_floor": 0,
|
|
42
|
+
"detected": False, "detail": "N/A"},
|
|
43
|
+
"multi_channel": {"channels": [], "most_suspicious": None, "detail": "N/A"},
|
|
44
|
+
"quality": {"psnr_db": result.baseline_psnr, "ssim": 0.99,
|
|
45
|
+
"imperceptible": result.baseline_psnr > 40,
|
|
46
|
+
"detail": f"Baseline PSNR={result.baseline_psnr:.2f}dB"},
|
|
47
|
+
"overall_verdict": (
|
|
48
|
+
f"ROBUSTNESS TEST — {result.secret!r} — "
|
|
49
|
+
f"Survived {survived}/{len(attacks)} attacks ({result.survival_rate:.0%})"
|
|
50
|
+
),
|
|
51
|
+
"confidence": result.survival_rate,
|
|
52
|
+
"detected_by": ["trustmark-neural"],
|
|
53
|
+
"neural": {
|
|
54
|
+
"attempted": True, "any_detected": True,
|
|
55
|
+
"detected_schemes": ["trustmark"],
|
|
56
|
+
"detail": f"TrustMark watermark — survival rate {result.survival_rate:.0%}",
|
|
57
|
+
"schemes": [{
|
|
58
|
+
"scheme": "trustmark", "detected": True,
|
|
59
|
+
"confidence": 1.0, "payload": result.secret,
|
|
60
|
+
"bit_accuracy": 1.0, "error": None,
|
|
61
|
+
"description": "Adobe TrustMark — C2PA content provenance",
|
|
62
|
+
}]
|
|
63
|
+
},
|
|
64
|
+
"srnet": {"attempted": False},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
generate_pdf(fake_result, output_path,
|
|
68
|
+
image_filename=result.image_path,
|
|
69
|
+
cover_filename="")
|
|
70
|
+
return output_path
|
stegmark/robustness.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""
|
|
2
|
+
stegmark.robustness — robustness_test() function
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os, sys, json
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Optional, List
|
|
8
|
+
|
|
9
|
+
_PKG_DIR = os.path.dirname(__file__)
|
|
10
|
+
sys.path.insert(0, _PKG_DIR)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class AttackResult:
|
|
15
|
+
attack: str
|
|
16
|
+
psnr: float
|
|
17
|
+
bit_accuracy: float
|
|
18
|
+
survived: bool
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class RobustnessResult:
|
|
23
|
+
"""
|
|
24
|
+
Robustness test result for a watermarked image.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
image_path Original cover image
|
|
28
|
+
secret Embedded secret string
|
|
29
|
+
baseline_psnr PSNR of watermarked vs cover
|
|
30
|
+
survival_rate Fraction of attacks watermark survived (0.0–1.0)
|
|
31
|
+
attacks Per-attack results
|
|
32
|
+
stego_path Path to saved watermarked image
|
|
33
|
+
|
|
34
|
+
Methods:
|
|
35
|
+
save_report(path) Generate and save PDF report
|
|
36
|
+
to_dict() Return as JSON-serializable dict
|
|
37
|
+
"""
|
|
38
|
+
image_path: str
|
|
39
|
+
secret: str
|
|
40
|
+
baseline_psnr: float
|
|
41
|
+
survival_rate: float
|
|
42
|
+
attacks: List[AttackResult] = field(default_factory=list)
|
|
43
|
+
stego_path: str = ""
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def survived_attacks(self) -> List[str]:
|
|
47
|
+
return [a.attack for a in self.attacks if a.survived]
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def failed_attacks(self) -> List[str]:
|
|
51
|
+
return [a.attack for a in self.attacks if not a.survived]
|
|
52
|
+
|
|
53
|
+
def save_report(self, output_path: str = None) -> str:
|
|
54
|
+
from stegmark.report import generate_robustness_report
|
|
55
|
+
if output_path is None:
|
|
56
|
+
base = os.path.splitext(self.image_path)[0]
|
|
57
|
+
output_path = f"{base}_robustness_report.pdf"
|
|
58
|
+
generate_robustness_report(self, output_path)
|
|
59
|
+
return output_path
|
|
60
|
+
|
|
61
|
+
def to_dict(self) -> dict:
|
|
62
|
+
import dataclasses
|
|
63
|
+
return dataclasses.asdict(self)
|
|
64
|
+
|
|
65
|
+
def __repr__(self):
|
|
66
|
+
return (f"RobustnessResult(survival_rate={self.survival_rate:.0%}, "
|
|
67
|
+
f"survived={len(self.survived_attacks)}/{len(self.attacks)}, "
|
|
68
|
+
f"baseline_psnr={self.baseline_psnr:.1f}dB)")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def robustness_test(
|
|
72
|
+
image_path: str,
|
|
73
|
+
secret: str = "stegmark",
|
|
74
|
+
tier2: bool = False,
|
|
75
|
+
verbose: bool = True,
|
|
76
|
+
) -> RobustnessResult:
|
|
77
|
+
"""
|
|
78
|
+
Embed a TrustMark watermark and test survival across attack suite.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
image_path Cover image path (JPEG or PNG)
|
|
82
|
+
secret String payload to embed (default: "stegmark")
|
|
83
|
+
tier2 Also run tier-2 attacks (brightness, contrast, noise)
|
|
84
|
+
verbose Print progress table
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
RobustnessResult with .survival_rate, .survived_attacks,
|
|
88
|
+
.failed_attacks, .baseline_psnr. Call .save_report() for PDF.
|
|
89
|
+
|
|
90
|
+
Example:
|
|
91
|
+
from stegmark import robustness_test
|
|
92
|
+
|
|
93
|
+
report = robustness_test("cover.jpg", secret="my_company_id")
|
|
94
|
+
print(f"Survival rate: {report.survival_rate:.0%}")
|
|
95
|
+
print(f"Failed: {report.failed_attacks}")
|
|
96
|
+
report.save_report("robustness_audit.pdf")
|
|
97
|
+
"""
|
|
98
|
+
import io
|
|
99
|
+
import numpy as np
|
|
100
|
+
from PIL import Image
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
from trustmark import TrustMark
|
|
104
|
+
except ImportError:
|
|
105
|
+
raise ImportError("pip install trustmark torch torchvision")
|
|
106
|
+
|
|
107
|
+
from robust_watermark_test import (
|
|
108
|
+
TIER1_ATTACKS, TIER2_ATTACKS, psnr, bit_accuracy
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if verbose:
|
|
112
|
+
print(f"[stegmark] Loading TrustMark model...")
|
|
113
|
+
tm = TrustMark(verbose=False, model_type="Q")
|
|
114
|
+
|
|
115
|
+
cover = Image.open(image_path).convert("RGB")
|
|
116
|
+
stego = tm.encode(cover, secret)
|
|
117
|
+
|
|
118
|
+
stego_path = os.path.splitext(image_path)[0] + "_stegmark_stego.png"
|
|
119
|
+
stego.save(stego_path)
|
|
120
|
+
|
|
121
|
+
baseline = psnr(cover, stego)
|
|
122
|
+
decoded_wm, detected, _ = tm.decode(stego)
|
|
123
|
+
|
|
124
|
+
if verbose:
|
|
125
|
+
print(f"[stegmark] Baseline: PSNR={baseline:.1f}dB detected={detected}")
|
|
126
|
+
print(f"\n {'Attack':<25} {'PSNR':>7} {'bit_acc':>8} {'survive':>8}")
|
|
127
|
+
print(f" {'-'*54}")
|
|
128
|
+
|
|
129
|
+
attacks = TIER1_ATTACKS + (TIER2_ATTACKS if tier2 else [])
|
|
130
|
+
results = []
|
|
131
|
+
|
|
132
|
+
for name, fn in attacks:
|
|
133
|
+
try:
|
|
134
|
+
attacked = fn(stego)
|
|
135
|
+
p = psnr(cover, attacked)
|
|
136
|
+
try:
|
|
137
|
+
dec, det, _ = tm.decode(attacked)
|
|
138
|
+
acc = bit_accuracy(dec, decoded_wm) if dec and decoded_wm else 0.0
|
|
139
|
+
survived = det and acc > 0.8
|
|
140
|
+
except Exception:
|
|
141
|
+
acc = 0.0; survived = False
|
|
142
|
+
|
|
143
|
+
if verbose:
|
|
144
|
+
print(f" {name:<25} {p:>6.1f} {acc:>7.1%} {'✓' if survived else '✗'}")
|
|
145
|
+
results.append(AttackResult(name, round(p,2), round(acc,3), survived))
|
|
146
|
+
except Exception as e:
|
|
147
|
+
results.append(AttackResult(name, 0.0, 0.0, False))
|
|
148
|
+
|
|
149
|
+
survived_count = sum(1 for r in results if r.survived)
|
|
150
|
+
rate = survived_count / len(results) if results else 0.0
|
|
151
|
+
|
|
152
|
+
if verbose:
|
|
153
|
+
print(f"\n Survived {survived_count}/{len(results)} ({rate:.0%})")
|
|
154
|
+
|
|
155
|
+
return RobustnessResult(
|
|
156
|
+
image_path=image_path,
|
|
157
|
+
secret=secret,
|
|
158
|
+
baseline_psnr=round(baseline, 2),
|
|
159
|
+
survival_rate=round(rate, 3),
|
|
160
|
+
attacks=results,
|
|
161
|
+
stego_path=stego_path,
|
|
162
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: stegmark
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Universal steganographic analysis — statistical, forensic, and neural watermark detection
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Requires-Dist: numpy<2.0,>=1.24
|
|
7
|
+
Requires-Dist: scipy>=1.10
|
|
8
|
+
Requires-Dist: scikit-image>=0.21
|
|
9
|
+
Requires-Dist: Pillow>=10.0
|
|
10
|
+
Requires-Dist: reportlab>=4.0
|
|
11
|
+
Requires-Dist: jpegio>=0.2.8
|
|
12
|
+
Requires-Dist: fastapi>=0.100
|
|
13
|
+
Requires-Dist: uvicorn>=0.23
|
|
14
|
+
Requires-Dist: python-multipart>=0.0.6
|
|
15
|
+
Provides-Extra: neural
|
|
16
|
+
Requires-Dist: trustmark>=0.1; extra == "neural"
|
|
17
|
+
Requires-Dist: torch>=2.0; extra == "neural"
|
|
18
|
+
Requires-Dist: torchvision>=0.15; extra == "neural"
|
|
19
|
+
Provides-Extra: all
|
|
20
|
+
Requires-Dist: trustmark>=0.1; extra == "all"
|
|
21
|
+
Requires-Dist: torch>=2.0; extra == "all"
|
|
22
|
+
Requires-Dist: torchvision>=0.15; extra == "all"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
stegmark/__init__.py,sha256=H216oaX10Y_Bug_DI6fMuMe7zv7SuG_MMs4O5Xm4bhc,833
|
|
2
|
+
stegmark/_detectors.py,sha256=mL7cNW0gpNaSN6mBg3ui9tiqDzVCIucgJl_aUOOVhBA,1351
|
|
3
|
+
stegmark/cli.py,sha256=yS_jcQIs3Ydwr-Y4zGoWoaQED9XLjQIUQzGRGsMsUEk,272
|
|
4
|
+
stegmark/core.py,sha256=5UyK6viBePrQzM4w-cZhwPI2Gf4n18BRsyG7v4wvKAw,6595
|
|
5
|
+
stegmark/report.py,sha256=8zh4wWv8Btd34BosExrTegDwZwMl8FiFfvs6ybE_RV4,3062
|
|
6
|
+
stegmark/robustness.py,sha256=2g-IE7bqehcGaXl1obWMvOBbO4Lhxy0wz-Tuo4-SIKM,5049
|
|
7
|
+
stegmark-1.0.0.dist-info/METADATA,sha256=IoFvcfnMYuYm4tL6qRDzwRJyyTYZU915LBC2pp7STWg,780
|
|
8
|
+
stegmark-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
9
|
+
stegmark-1.0.0.dist-info/entry_points.txt,sha256=rTsDDAbnrdXDYrT0eZeDqMa1adHD1tOk5Of-zGwOc5g,47
|
|
10
|
+
stegmark-1.0.0.dist-info/top_level.txt,sha256=LSUI8MIJoPOw_Eu380gTUMdjHL7P-9tTRldD5PcXx8c,9
|
|
11
|
+
stegmark-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
stegmark
|