Skip to content

Commit e476d20

Browse files
committed
feat(ops): Add operation for salt/pepper noise
- Added `AddSaltPepperNoise` operation - Renamed `tamper:gaussianSeed` property to `tamper:noiseSeed` - Updated tests, docs, ontology Signed-off-by: Joshua Locash <locashjosh@gmail.com>
1 parent 3f49e25 commit e476d20

11 files changed

Lines changed: 275 additions & 79 deletions

docs/operations.md

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -114,25 +114,47 @@ Applies a Gaussian blur over a square kernel.
114114

115115
### AddGaussianNoise — `tamper:AddGaussianNoise`
116116

117-
Adds per-pixel Gaussian noise (values are clipped to `0``255`).
117+
Adds Gaussian noise (values are clipped to `0``255`). A subtype of
118+
`tamper:AddNoise`.
118119

119120
| Parameter | Property | Type | Constraint | Required |
120121
| ------------------ | --------------------- | ----- | ---------- | -------- |
121122
| mean | `tamper:gaussianMean` | float || yes |
122123
| standard deviation | `tamper:gaussianStd` | float | `>= 0` | yes |
123-
| seed | `tamper:gaussianSeed` | int | `>= 0` | no |
124+
| seed | `tamper:noiseSeed` | int | `>= 0` | yes |
124125

125-
The noise is drawn from a seeded random number generator, so a given seed
126-
always produces identical output. If `tamper:gaussianSeed` is omitted, a seed
127-
is generated automatically and recorded into the result graph, keeping the run
128-
reproducible and self-documenting.
126+
`tamper:noiseSeed` is used to seed the random noise generator, so that the operation is deterministic/reproducable.
129127

130128
```turtle
131129
@prefix tamper: <https://example.org/tamper/core#> .
132130
133131
[] a tamper:AddGaussianNoise ;
134132
tamper:gaussianMean 0.0 ;
135-
tamper:gaussianStd 12.0 .
133+
tamper:gaussianStd 12.0 ;
134+
tamper:noiseSeed 42 .
135+
```
136+
137+
### AddSaltPepperNoise — `tamper:AddSaltPepperNoise`
138+
139+
Replaces a fraction of pixels with pure white ("salt") or pure black ("pepper").
140+
A subtype of `tamper:AddNoise`.
141+
142+
| Parameter | Property | Type | Constraint | Required |
143+
| ---------- | ------------------------- | ----- | ----------- | -------- |
144+
| amount | `tamper:saltPepperAmount` | float | `0.0``1.0` | yes |
145+
| salt ratio | `tamper:saltPepperRatio` | float | `0.0``1.0` | yes |
146+
| seed | `tamper:noiseSeed` | int | `>= 0` | yes |
147+
148+
`amount` is the fraction of pixels corrupted; `salt ratio` is the fraction of
149+
those set to white (salt) rather than black (pepper). The seed makes the operation deterministic/reproducable.
150+
151+
```turtle
152+
@prefix tamper: <https://example.org/tamper/core#> .
153+
154+
[] a tamper:AddSaltPepperNoise ;
155+
tamper:saltPepperAmount 0.05 ;
156+
tamper:saltPepperRatio 0.5 ;
157+
tamper:noiseSeed 42 .
136158
```
137159

138160
### Crop — `tamper:Crop`

tamper/ops/add_noise.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from os import PathLike
2+
from pathlib import Path
3+
4+
import cv2
5+
import numpy as np
6+
from rdflib import XSD
7+
8+
from tamper.vocabularies import TAMPER
9+
10+
from tamper.core import ImageAsset, Operation, MappedProperty
11+
12+
13+
class AddGaussianNoise(Operation):
14+
__rdf_type__ = TAMPER.AddGaussianNoise
15+
16+
mean: MappedProperty[float] = MappedProperty(TAMPER.gaussianMean, XSD.double)
17+
std: MappedProperty[float] = MappedProperty(TAMPER.gaussianStd, XSD.double)
18+
seed: MappedProperty[int] = MappedProperty(TAMPER.noiseSeed, XSD.integer)
19+
20+
def mutate(self, out_dir: PathLike[str] | None = None):
21+
used = self.get_used()
22+
if len(used) != 1:
23+
raise ValueError("Operation requires exactly one image asset")
24+
25+
img_asset = ImageAsset(self.graph, used[0])
26+
27+
img = cv2.imread(img_asset.file_path)
28+
rng = np.random.default_rng(self.seed)
29+
noise = rng.normal(self.mean, self.std, img.shape)
30+
noisy_img = np.clip(img + noise, 0, 255).astype(np.uint8)
31+
ext = Path(img_asset.file_path).suffix or ".png"
32+
ok, buf = cv2.imencode(ext, noisy_img)
33+
if not ok:
34+
raise RuntimeError(f"Encoding to {ext} failed")
35+
36+
with self._generates_file(dir=out_dir, suffix=ext) as f:
37+
Path(f).write_bytes(buf.tobytes())
38+
39+
40+
class AddSaltPepperNoise(Operation):
41+
__rdf_type__ = TAMPER.AddSaltPepperNoise
42+
43+
amount: MappedProperty[float] = MappedProperty(TAMPER.saltPepperAmount, XSD.double)
44+
salt_ratio: MappedProperty[float] = MappedProperty(
45+
TAMPER.saltPepperRatio, XSD.double
46+
)
47+
seed: MappedProperty[int] = MappedProperty(TAMPER.noiseSeed, XSD.integer)
48+
49+
def mutate(self, out_dir: PathLike[str] | None = None):
50+
used = self.get_used()
51+
if len(used) != 1:
52+
raise ValueError("Operation requires exactly one image asset")
53+
54+
img_asset = ImageAsset(self.graph, used[0])
55+
56+
img = cv2.imread(img_asset.file_path)
57+
if img is None:
58+
raise RuntimeError(f"Could not read image: {img_asset.file_path}")
59+
60+
rng = np.random.default_rng(self.seed)
61+
out = img.copy()
62+
h, w = img.shape[:2]
63+
n = int(self.amount * h * w)
64+
n_salt = int(n * self.salt_ratio)
65+
66+
flat = rng.choice(h * w, size=n, replace=False)
67+
ys, xs = np.unravel_index(flat, (h, w))
68+
out[ys[:n_salt], xs[:n_salt]] = 255
69+
out[ys[n_salt:], xs[n_salt:]] = 0
70+
71+
ext = Path(img_asset.file_path).suffix or ".png"
72+
ok, buf = cv2.imencode(ext, out)
73+
if not ok:
74+
raise RuntimeError(f"Encoding to {ext} failed")
75+
76+
with self._generates_file(dir=out_dir, suffix=ext) as f:
77+
Path(f).write_bytes(buf.tobytes())

tamper/ops/image.py

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from pathlib import Path
33

44
import cv2
5-
import numpy as np
65
from rdflib import XSD
76

87
from tamper.vocabularies import TAMPER
@@ -97,30 +96,3 @@ def mutate(self, out_dir: PathLike[str] | None = None):
9796

9897
with self._generates_file(dir=out_dir, suffix=ext) as f:
9998
Path(f).write_bytes(buf.tobytes())
100-
101-
102-
class AddGaussianNoise(Operation):
103-
__rdf_type__ = TAMPER.AddGaussianNoise
104-
105-
mean: MappedProperty[float] = MappedProperty(TAMPER.gaussianMean, XSD.double)
106-
std: MappedProperty[float] = MappedProperty(TAMPER.gaussianStd, XSD.double)
107-
seed: MappedProperty[int] = MappedProperty(TAMPER.gaussianSeed, XSD.integer)
108-
109-
def mutate(self, out_dir: PathLike[str] | None = None):
110-
used = self.get_used()
111-
if len(used) != 1:
112-
raise ValueError("Operation requires exactly one image asset")
113-
114-
img_asset = ImageAsset(self.graph, used[0])
115-
116-
img = cv2.imread(img_asset.file_path)
117-
rng = np.random.default_rng(self.seed)
118-
noise = rng.normal(self.mean, self.std, img.shape)
119-
noisy_img = np.clip(img + noise, 0, 255).astype(np.uint8)
120-
ext = Path(img_asset.file_path).suffix or ".png"
121-
ok, buf = cv2.imencode(ext, noisy_img)
122-
if not ok:
123-
raise RuntimeError(f"Encoding to {ext} failed")
124-
125-
with self._generates_file(dir=out_dir, suffix=ext) as f:
126-
Path(f).write_bytes(buf.tobytes())

tamper/ops/operation-shapes.ttl

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,31 @@ tamper:GaussianStdShape a sh:PropertyShape ;
111111
sh:maxCount 1 ;
112112
sh:message "Gaussian std must be a double" .
113113

114-
tamper:GaussianSeedShape a sh:PropertyShape ;
115-
sh:path tamper:gaussianSeed ;
114+
tamper:NoiseSeedShape a sh:PropertyShape ;
115+
sh:path tamper:noiseSeed ;
116116
sh:datatype xsd:integer ;
117117
sh:minCount 1 ;
118118
sh:maxCount 1 ;
119119
sh:minInclusive 0 ;
120-
sh:message "Gaussian seed must be a non-negative integer" .
120+
sh:message "Noise seed must be a non-negative integer" .
121+
122+
tamper:SaltPepperAmountShape a sh:PropertyShape ;
123+
sh:path tamper:saltPepperAmount ;
124+
sh:datatype xsd:double ;
125+
sh:minCount 1 ;
126+
sh:maxCount 1 ;
127+
sh:minInclusive 0 ;
128+
sh:maxInclusive 1 ;
129+
sh:message "Salt-and-pepper amount must be a fraction between 0 and 1" .
130+
131+
tamper:SaltPepperRatioShape a sh:PropertyShape ;
132+
sh:path tamper:saltPepperRatio ;
133+
sh:datatype xsd:double ;
134+
sh:minCount 1 ;
135+
sh:maxCount 1 ;
136+
sh:minInclusive 0 ;
137+
sh:maxInclusive 1 ;
138+
sh:message "Salt-vs-pepper ratio must be a fraction between 0 and 1" .
121139

122140
# Audio-related Property Shapes
123141

@@ -188,7 +206,13 @@ tamper:AddGaussianNoiseShape a sh:NodeShape ;
188206
sh:targetClass tamper:AddGaussianNoise ;
189207
sh:property tamper:GaussianMeanShape ;
190208
sh:property tamper:GaussianStdShape ;
191-
sh:property tamper:GaussianSeedShape .
209+
sh:property tamper:NoiseSeedShape .
210+
211+
tamper:AddSaltPepperNoiseShape a sh:NodeShape ;
212+
sh:targetClass tamper:AddSaltPepperNoise ;
213+
sh:property tamper:SaltPepperAmountShape ;
214+
sh:property tamper:SaltPepperRatioShape ;
215+
sh:property tamper:NoiseSeedShape .
192216

193217
tamper:ResampleShape a sh:NodeShape ;
194218
sh:targetClass tamper:Resample ;

tamper/plans/thread_executor.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
from tamper.ops.resample import Resample
1313
from tamper.ops.compress import Compress
1414
from tamper.ops.crop import Crop
15+
from tamper.ops.add_noise import AddGaussianNoise, AddSaltPepperNoise
1516
from tamper.ops.image import (
16-
AddGaussianNoise,
1717
Resize,
1818
MedianFilter,
1919
GaussianBlur,
@@ -26,6 +26,7 @@
2626
TAMPER.Compress: Compress,
2727
TAMPER.Transcode: Transcode,
2828
TAMPER.AddGaussianNoise: AddGaussianNoise,
29+
TAMPER.AddSaltPepperNoise: AddSaltPepperNoise,
2930
TAMPER.Resize: Resize,
3031
TAMPER.MedianFilter: MedianFilter,
3132
TAMPER.GaussianBlur: GaussianBlur,

tamper/vocabularies/_TAMPER.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ class TAMPER(DefinedNamespace):
77
DESCRIPTION_EDIT_ME_!
88
99
Generated from: SOURCE_RDF_FILE_EDIT_ME_!
10-
Date: 2026-06-14 02:07:58.350639
10+
Date: 2026-06-14 03:27:32.161201
1111
"""
1212

1313
_NS = Namespace("https://example.org/tamper/core#")
1414

1515
AddGaussianNoise: URIRef # Adds gaussian noise to an image.
16+
AddNoise: URIRef # Adds noise to an image. An abstract grouping of the noise-type-specific operations.
17+
AddSaltPepperNoise: URIRef # Adds salt and pepper noise to an image
1618
AudioAsset: URIRef # A digital audio file containing one or more audio streams.
1719
AudioStream: URIRef # A stream containing audio sample data.
1820
Compress: URIRef # Applies compression to an image using a format and quality level
@@ -57,7 +59,6 @@ class TAMPER(DefinedNamespace):
5759
format: URIRef # compression format (e.g 'webp', 'jpeg')
5860
frameRate: URIRef # The frame rate in frames per second.
5961
gaussianMean: URIRef #
60-
gaussianSeed: URIRef # The seed for the random number generator used to draw the noise, recorded so the operation is exactly reproducible.
6162
gaussianStd: URIRef #
6263
hasStream: URIRef # Relates a stream container to its constituent streams.
6364
height: URIRef # The height in pixels.
@@ -67,8 +68,13 @@ class TAMPER(DefinedNamespace):
6768
mediaType: (
6869
URIRef # The MIME type of the media asset (e.g., 'image/jpeg', 'video/mp4').
6970
)
71+
noiseSeed: URIRef # The seed for the random number generator used to draw the noise, recorded so the operation is exactly reproducible.
7072
pixelFormat: URIRef # The pixel format (e.g., 'yuv420p', 'rgb24').
7173
qualityFactor: URIRef # Image compression quality factor (0 - 100)
74+
saltPepperAmount: (
75+
URIRef # The fraction of pixels (0.0 - 1.0) replaced with salt or pepper noise.
76+
)
77+
saltPepperRatio: URIRef # Of the corrupted pixels, the fraction (0.0 - 1.0) set to salt (white) rather than pepper (black).
7278
sampleRate: URIRef # The audio sample rate in Hertz.
7379
streamIndex: URIRef # The zero-based index of the stream within its container.
7480
targetBitRate: URIRef # The target bit rate in bits per second for an audio transcode operation.

tamper/vocabularies/tamper-core.ttl

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,20 @@ tamper:Compress a owl:Class ;
7070
rdfs:comment "Applies compression to an image using a format and quality level"@en ;
7171
rdfs:subClassOf tamper:Operation .
7272

73+
tamper:AddNoise a owl:Class ;
74+
rdfs:label "Add Noise"@en ;
75+
rdfs:comment "Adds noise to an image. An abstract grouping of the noise-type-specific operations."@en ;
76+
rdfs:subClassOf tamper:Operation .
77+
7378
tamper:AddGaussianNoise a owl:Class ;
7479
rdfs:label "Add Gaussian Noise"@en ;
7580
rdfs:comment "Adds gaussian noise to an image."@en ;
76-
rdfs:subClassOf tamper:Operation .
81+
rdfs:subClassOf tamper:AddNoise .
82+
83+
tamper:AddSaltPepperNoise a owl:Class ;
84+
rdfs:label "Add Salt Pepper Noise"@en ;
85+
rdfs:comment "Adds salt and pepper noise to an image"@en ;
86+
rdfs:subClassOf tamper:AddNoise .
7787

7888
tamper:Resize a owl:Class ;
7989
rdfs:label "Resize"@en ;
@@ -316,12 +326,24 @@ tamper:gaussianStd a owl:DatatypeProperty, owl:FunctionalProperty ;
316326
rdfs:domain tamper:AddGaussianNoise ;
317327
rdfs:range xsd:double .
318328

319-
tamper:gaussianSeed a owl:DatatypeProperty, owl:FunctionalProperty ;
320-
rdfs:label "gaussian seed"@en ;
329+
tamper:noiseSeed a owl:DatatypeProperty, owl:FunctionalProperty ;
330+
rdfs:label "noise seed"@en ;
321331
rdfs:comment "The seed for the random number generator used to draw the noise, recorded so the operation is exactly reproducible."@en ;
322-
rdfs:domain tamper:AddGaussianNoise ;
332+
rdfs:domain tamper:AddNoise ;
323333
rdfs:range xsd:integer .
324334

335+
tamper:saltPepperAmount a owl:DatatypeProperty, owl:FunctionalProperty ;
336+
rdfs:label "salt and pepper amount"@en ;
337+
rdfs:comment "The fraction of pixels (0.0 - 1.0) replaced with salt or pepper noise."@en ;
338+
rdfs:domain tamper:AddSaltPepperNoise ;
339+
rdfs:range xsd:double .
340+
341+
tamper:saltPepperRatio a owl:DatatypeProperty, owl:FunctionalProperty ;
342+
rdfs:label "salt and pepper ratio"@en ;
343+
rdfs:comment "Of the corrupted pixels, the fraction (0.0 - 1.0) set to salt (white) rather than pepper (black)."@en ;
344+
rdfs:domain tamper:AddSaltPepperNoise ;
345+
rdfs:range xsd:double .
346+
325347
tamper:targetWidth a owl:DatatypeProperty, owl:FunctionalProperty ;
326348
rdfs:label "target width"@en ;
327349
rdfs:comment "The target width in pixels for a resize operation."@en ;

0 commit comments

Comments
 (0)