Skip to content

Commit d685690

Browse files
committed
feat: poisson and gaussion noise
1 parent 189d764 commit d685690

File tree

7 files changed

+270
-47
lines changed

7 files changed

+270
-47
lines changed

lib/augmenters/abstract.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class AbstractAugmenter {
1616
}
1717
})
1818
const o2 = Object.assign({}, {boxes: []}, metadata, o, {points});
19-
const params = this.buildParams(o);
19+
const params = this.buildParams(o2);
2020
const resolved = params.runOnce();
2121
return this.augment(o2, resolved);
2222
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
const h = require('hasard');
2+
const AbstractAugmenter = require('./abstract');
3+
4+
/**
5+
* Adds noise sampled from a gaussian distribution
6+
* @param {Number} [opts.mean=0] `mean` of the gaussian distribution
7+
* @param {Number} opts.sigma `sigma` of the gaussian distribution
8+
* @param {Number} [opts.scale=1] if `scale` is defined (0 < scale < 1), then the noise can apply at a less granular scale
9+
* @param {Number} [opts.perChannel=false] If perChannel is true, then the sampled values may be different per channel (and pixel).
10+
*/
11+
12+
class AdditiveGaussianNoiseAugmenter extends AbstractAugmenter {
13+
constructor(opts) {
14+
super(opts);
15+
const {mean=0, sigma, scale=1, perChannel=false} = opts;
16+
this.mean = mean;
17+
this.sigma = sigma;
18+
this.scale = scale;
19+
this.perChannel = perChannel;
20+
}
21+
22+
buildParams({width, height, channels}) {
23+
const scaleRef = h.reference(this.scale);
24+
25+
const hasardPixelValue = h.round(h.number({
26+
type: 'normal',
27+
mean: this.mean,
28+
sigma: this.sigma
29+
}));
30+
31+
const hasardPixelValueRef = h.reference({
32+
source: hasardPixelValue,
33+
context: 'colorPixel'
34+
});
35+
36+
const perChannelRef = h.reference({
37+
source: this.perChannel,
38+
context: 'image'
39+
});
40+
return h.object({
41+
noise: h.matrix({
42+
shape: h.array([
43+
h.round(h.multiply(scaleRef, width)),
44+
h.round(h.multiply(scaleRef, height))
45+
]),
46+
contextName: 'image',
47+
value: h.if(this.perChannel,
48+
h.array([
49+
hasardPixelValue,
50+
hasardPixelValue,
51+
hasardPixelValue
52+
]),
53+
h.array({
54+
values: [
55+
hasardPixelValueRef,
56+
hasardPixelValueRef,
57+
hasardPixelValueRef
58+
],
59+
contextName: 'colorPixel'
60+
})
61+
)
62+
}),
63+
scale: scaleRef
64+
});
65+
}
66+
67+
/**
68+
* @param {PipedImageAttribute} imgAttributes
69+
* @param {AugmenterRandomProperties} augmenterRandomProperties
70+
* @returns {PipedImageAttribute}
71+
*/
72+
augmentImage({img}, {noise, scale}) {
73+
return this.backend.addNoise(img, {noise, scale});
74+
}
75+
}
76+
77+
module.exports = AdditiveGaussianNoiseAugmenter;

lib/augmenters/additive-poisson-noise.js

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const AbstractAugmenter = require('./abstract');
1111
class AdditivePoissonNoiseAugmenter extends AbstractAugmenter {
1212
constructor(opts) {
1313
super(opts);
14-
const {lambda, scale, perChannel} = opts;
14+
const {lambda, scale=1, perChannel=false} = opts;
1515
this.lambda = lambda;
1616
this.scale = scale;
1717
this.perChannel = perChannel;
@@ -20,38 +20,40 @@ class AdditivePoissonNoiseAugmenter extends AbstractAugmenter {
2020
buildParams({width, height, channels}) {
2121
const scaleRef = h.reference(this.scale);
2222

23-
console.log([width, height, channels, this.lambda]);
24-
25-
const hasardPixelValue = h.multiply(
26-
h.if(h.boolean(), -1, 1),
27-
h.integer({
28-
type: 'poisson',
29-
lambda: this.lambda
30-
})
31-
);
23+
const hasardPixelValue = h.integer({
24+
type: 'poisson',
25+
lambda: this.lambda
26+
});
3227

3328
const hasardPixelValueRef = h.reference({
3429
source: hasardPixelValue,
35-
inScope: 'colorPixel'
30+
context: 'colorPixel'
31+
});
32+
33+
const perChannelRef = h.reference({
34+
source: this.perChannel,
35+
context: 'image'
3636
});
3737
return h.object({
3838
noise: h.matrix({
39-
shape: [height, width, 4],
39+
shape: h.array([
40+
h.round(h.multiply(scaleRef, width)),
41+
h.round(h.multiply(scaleRef, height))
42+
]),
43+
contextName: 'image',
4044
value: h.if(this.perChannel,
4145
h.array([
4246
hasardPixelValue,
4347
hasardPixelValue,
44-
hasardPixelValue,
45-
0
48+
hasardPixelValue
4649
]),
4750
h.array({
4851
values: [
4952
hasardPixelValueRef,
5053
hasardPixelValueRef,
51-
hasardPixelValueRef,
52-
0
54+
hasardPixelValueRef
5355
],
54-
scope: 'colorPixel'
56+
contextName: 'colorPixel'
5557
})
5658
)
5759
}),

lib/backend/opencv-backend.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,15 @@ class OpenCVBackend {
4545
}
4646

4747
addNoise(img, {noise, scale}) {
48-
let noiseImg = new this._cv.Mat(noise, this._cv.CV_16SC4);
48+
let noiseImg = new this._cv.Mat(noise, this._cv.CV_16SC3);
4949
if (scale !== 1) {
5050
noiseImg = noiseImg.resize(img.rows, img.cols);
5151
}
5252

5353
const img16 = img.convertTo(this._cv.CV_16SC4);
54+
5455
const added = noiseImg.add(img16);
56+
5557
const resImg = added.convertTo(this._cv.CV_8UC4);
5658
return resImg;
5759
}
@@ -145,6 +147,26 @@ class OpenCVBackend {
145147
channels: img.channels
146148
};
147149
}
150+
151+
absdiff(m1, m2){
152+
return m1.absdiff(m2);
153+
}
154+
diff(m1, m2){
155+
return m1.convertTo(this._cv.CV_16SC3).sub(m2.convertTo(this._cv.CV_16SC3));
156+
}
157+
norm(m){
158+
return m.norm()
159+
}
160+
normL1(m){
161+
return m.norm(this._cv.NORM_L1)
162+
}
163+
forEachPixel(m, fn){
164+
return m.getDataAsArray().forEach((row, rIndex) => {
165+
row.forEach((v, cIndex) => {
166+
fn(v, rIndex, cIndex)
167+
})
168+
})
169+
}
148170
}
149171

150172
module.exports = OpenCVBackend;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
const path = require('path');
2+
const test = require('ava');
3+
const AdditiveGaussianNoise = require('../../lib/augmenters/additive-gaussian-noise');
4+
const macroAugmenter = require('../macros/augmenter');
5+
6+
const mean = 2;
7+
8+
test('additiveGaussianNoise not perChannel', macroAugmenter, AdditiveGaussianNoise, {
9+
input: path.join(__dirname, '..', 'data/lenna.png'),
10+
expectImg: function(t, mat1, mat2, backend){
11+
const metadata = backend.getMetadata(mat1);
12+
const diff = backend.diff(mat1, mat2);
13+
const norm = backend.normL1(diff)/(metadata.width* metadata.height* metadata.channels);
14+
15+
t.true(Math.abs(norm - mean) < 1e-1);
16+
17+
//console.log(diff.getDataAsArray().slice(0,30).map(v => v.slice(440, 450)))
18+
19+
let count = 0;
20+
const m2 = mat2.getDataAsArray();
21+
backend.forEachPixel(diff, function([b, g, r], rowIndex, colIndex){
22+
if(m2[rowIndex][colIndex].indexOf(255) === -1 && m2[rowIndex][colIndex].indexOf(0) === -1 && (r !== g || g!==b)){
23+
count++;
24+
}
25+
});
26+
t.is(count, 0)
27+
},
28+
options: {
29+
mean,
30+
sigma: 2,
31+
perChannel: false
32+
}
33+
});
34+
35+
test('additiveGaussianNoise per Channel', macroAugmenter, AdditiveGaussianNoise, {
36+
input: path.join(__dirname, '..', 'data/lenna.png'),
37+
expectImg: function(t, mat1, mat2, backend){
38+
const diff = backend.diff(mat1, mat2);
39+
let count = 0;
40+
const m2 = mat2.getDataAsArray()
41+
backend.forEachPixel(diff, function([b, g, r], rowIndex, colIndex){
42+
if(m2[rowIndex][colIndex].indexOf(255) === -1 && m2[rowIndex][colIndex].indexOf(0) === -1 && (r !== g || g!==b)){
43+
count++;
44+
}
45+
});
46+
t.not(count, 0)
47+
},
48+
options: {
49+
mean,
50+
sigma: 2,
51+
perChannel: true
52+
}
53+
});
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
const path = require('path');
2+
const test = require('ava');
3+
const AdditivePoissonNoise = require('../../lib/augmenters/additive-poisson-noise');
4+
const macroAugmenter = require('../macros/augmenter');
5+
6+
const lambda = 4;
7+
8+
test('additivePoissonNoise not perChannel', macroAugmenter, AdditivePoissonNoise, {
9+
input: path.join(__dirname, '..', 'data/lenna.png'),
10+
expectImg: function(t, mat1, mat2, backend){
11+
const absdiff = backend.absdiff(mat1, mat2);
12+
const metadata = backend.getMetadata(mat1);
13+
const norm = backend.normL1(absdiff)/(metadata.width* metadata.height* metadata.channels);
14+
15+
t.true(norm > lambda * 2 / 3);
16+
t.true(norm < lambda * 4 / 3);
17+
18+
const norm2 = backend.normL1(backend.diff(mat1, mat2))/(metadata.width* metadata.height* metadata.channels);
19+
20+
t.true(norm2 > lambda * 1e-3);
21+
22+
let count=0;
23+
//console.log(diff.getDataAsArray().slice(0,30).map(v => v.slice(440, 450)))
24+
const m2 = mat2.getDataAsArray()
25+
backend.forEachPixel(diff, function([b, g, r], rowIndex, colIndex){
26+
if(m2[rowIndex][colIndex].indexOf(255) === -1 && m2[rowIndex][colIndex].indexOf(0) === -1 && (r !== g || g!==b)){
27+
count++;
28+
}
29+
});
30+
t.is(count, 0)
31+
},
32+
options: {
33+
lambda,
34+
perChannel: false
35+
}
36+
});
37+
38+
test('additivePoissonNoiseperChannel', macroAugmenter, AdditivePoissonNoise, {
39+
input: path.join(__dirname, '..', 'data/lenna.png'),
40+
expectImg: function(t, mat1, mat2, backend){
41+
const diff = backend.absdiff(mat1, mat2);
42+
const metadata = backend.getMetadata(mat1);
43+
const norm = backend.normL1(diff)/(metadata.width* metadata.height* metadata.channels);
44+
t.true(norm > lambda * 2 / 3);
45+
t.true(norm < lambda * 4 / 3);
46+
let count=0;
47+
const m2 = mat2.getDataAsArray()
48+
backend.forEachPixel(diff, function([b, g, r], rowIndex, colIndex){
49+
if(m2[rowIndex][colIndex].indexOf(255) === -1 && m2[rowIndex][colIndex].indexOf(0) === -1 && (r !== g || g!==b)){
50+
count++;
51+
}
52+
});
53+
t.not(count, 0)
54+
},
55+
options: {
56+
lambda,
57+
perChannel: true
58+
}
59+
});

test/macros/augmenter.js

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,41 +6,51 @@ module.exports = function (t, Cstr, {
66
options,
77
inputPoints,
88
outputPoints,
9+
expectImg,
910
backend = backends.getDefault()
1011
}) {
1112
const inst = new Cstr(options);
1213
const img = backend.readImage(input);
1314
const res = inst.runOnce({img});
1415

15-
return Promise.resolve().then(() => {
16-
if(!output){
17-
t.pass()
16+
return Promise.resolve()
17+
.then(() => {
18+
if(!output){
19+
t.pass()
20+
return Promise.resolve()
21+
}
22+
const expected = backend.readImage(output);
23+
24+
const data2 = backend.imageToBuffer(expected);
25+
t.true(backend.imageToBuffer(res.img).equals(data2));
1826
return Promise.resolve()
19-
}
20-
const expected = backend.readImage(output);
21-
22-
const data2 = backend.imageToBuffer(expected);
23-
t.true(backend.imageToBuffer(res.img).equals(data2));
24-
return Promise.resolve()
25-
}).then(() =>{
26-
if(!inputPoints && !outputPoints){
27-
t.pass()
28-
return Promise.resolve()
29-
}
30-
const width = img.cols;
31-
const height = img.rows;
32-
const toSize = ([x,y]) => ([x*width, y*height])
33-
const res = inst.runOnce({img, points: inputPoints.map(toSize)});
34-
35-
const expected = outputPoints.map(toSize).map(a => backend.point(...a));
36-
const tolerance = 1e-6*(width+height)/2;
37-
res.points.forEach((p, index) => {
38-
//console.log({actual: p.x, expected: expected[index].x, res: Math.abs(p.x - expected[index].x) < tolerance})
39-
t.true(Math.abs(p.x - expected[index].x) < tolerance)
40-
t.true(Math.abs(p.y - expected[index].y) < tolerance)
41-
});
42-
43-
})
27+
})
28+
.then(() => {
29+
if(!expectImg){
30+
t.pass()
31+
return Promise.resolve()
32+
}
33+
expectImg(t, img, res.img, backend)
34+
})
35+
.then(() =>{
36+
if(!inputPoints && !outputPoints){
37+
t.pass()
38+
return Promise.resolve()
39+
}
40+
const width = img.cols;
41+
const height = img.rows;
42+
const toSize = ([x,y]) => ([x*width, y*height])
43+
const res = inst.runOnce({img, points: inputPoints.map(toSize)});
44+
45+
const expected = outputPoints.map(toSize).map(a => backend.point(...a));
46+
const tolerance = 1e-6*(width+height)/2;
47+
res.points.forEach((p, index) => {
48+
//console.log({actual: p.x, expected: expected[index].x, res: Math.abs(p.x - expected[index].x) < tolerance})
49+
t.true(Math.abs(p.x - expected[index].x) < tolerance)
50+
t.true(Math.abs(p.y - expected[index].y) < tolerance)
51+
});
52+
53+
})
4454

4555

4656
};

0 commit comments

Comments
 (0)