Skip to content

Commit 7e3a970

Browse files
committed
feat: batch implementation preparation
1 parent b121027 commit 7e3a970

File tree

14 files changed

+308
-96
lines changed

14 files changed

+308
-96
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,10 @@
33
This library has been freely inspired from [imgaug](https://github.com/aleju/imgaug)
44

55
It is made to work with [hasard](https://www.npmjs.com/package/hasard) and [opencv4nodejs](https://github.com/justadudewhohacks/opencv4nodejs)
6+
7+
## Todo list
8+
9+
[] Add benchmark test to measure the speed
10+
[] Faster random generator using [tensorflow js truncated normal](https://js.tensorflow.org/api/1.0.0/#truncatedNormal)
11+
[] Run augmentation by batches on tensorflowjs
12+
[x] Get affine and perspective transform to work with tensorflow backend

lib/augmenters/abstract.js

Lines changed: 72 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,29 +12,29 @@ const img = cv.imread('lenna.png')
1212
const augmenter = new ia.PerspectiveTransform(0.2)
1313
* @example
1414
// Run the augmenter once
15-
const {image} = augmenter.runAugmenter(img)
16-
cv.imread('output.png', image);
15+
const {images} = augmenter.runAugmenter([img])
16+
cv.imwrite('output.png', images[0]);
1717
* @example
18-
// Run the augmenter multiple times
19-
augmenter.run({image, times: 10});
20-
// => [{ image : Mat }, { image : Mat }, ...]
18+
// Run the augmenter 4 times
19+
augmenter.run({images: [img, img, img, img]});
20+
// => { images : [Mat, Mat, ...]}
2121
* @example
2222
// follow a point in the augmentation workflow
23-
augmenter.run({image, points: [[25, 90], [12, 32]]})
24-
// => [{ image : Mat, points: [<first point new position>, <second point new position>] }]
23+
augmenter.run({images: [img], points: [[[25, 90], [12, 32]]]})
24+
// => { images : [Mat], points: [[<first point new position>, <second point new position>]] }
2525
**/
2626
class AbstractAugmenter extends Abstract{
2727
constructor(opts) {
2828
super(opts)
2929
this._augmenter = true;
3030
}
3131
/**
32-
* @typedef {AugmenterFormat | Image} OneRunOption
32+
* @typedef {AugmenterFormat | Images} OneRunOption
3333
**/
3434

3535
/**
3636
* @typedef {Object} AugmenterFormat
37-
* @property {Image} image the image to augment
37+
* @property {Images} images the image to augment
3838
* @property {Array.<Point>} points the points to augment
3939
* @property {Array.<Box>} boxes bournding boxes to augment
4040
*/
@@ -46,33 +46,41 @@ class AbstractAugmenter extends Abstract{
4646
*/
4747
runAugmenter(runOpts) {
4848
let params1;
49-
if (runOpts && runOpts.image && this.backend.isImage(runOpts.image)) {
49+
if (runOpts && runOpts.images && this.backend.isImages(runOpts.images)) {
5050
params1 = runOpts;
51-
} else if (this.backend.isImage(runOpts)) {
52-
params1 = {image: runOpts};
51+
} else if (this.backend.isImages(runOpts)) {
52+
params1 = {images: runOpts};
5353
} else {
54-
console.log(runOpts.image, this.backend.key)
55-
throw(new Error('runOnce must have an image in it'));
54+
throw(new Error('runOnce must have images in it'));
5655
}
57-
const metadata = this.backend.getMetadata(params1.image);
58-
59-
const points = (params1.points || []).map(p => {
60-
if (Array.isArray(p)) {
61-
return this.backend.point(p[0], p[1]);
62-
}
63-
return p;
64-
});
56+
const metadata = this.backend.getMetadata(params1.images);
57+
6558
debug(`buildHasard ${this._name}`);
6659

67-
const o2 = Object.assign({}, {boxes: []}, metadata, params1, {points});
60+
const o2 = Object.assign({}, metadata, params1);
61+
const nImages = metadata.nImages;
62+
6863
const params = this.buildHasard(o2);
64+
6965
debug(`runOnce ${this._name}`);
70-
const resolved = hasard.isHasard(params) ? params.runOnce(runOpts) : params;
66+
const resolved = [];
67+
68+
// every image hasard is generated independantly
69+
for(var i = 0; i< nImages; i++){
70+
const resolvedParams = hasard.isHasard(params) ? params.runOnce(runOpts) : params
71+
resolved.push(resolvedParams);
72+
}
7173
//console.log({resolved, params})
7274
debug(`augment ${this._name}`);
7375
return Promise.resolve(this.augment(o2, resolved, runOpts));
7476
}
7577

78+
fromFilenames({filenames}){
79+
return this.backend.readImages(filenames).then(images => {
80+
this.runAugmenter({images: images})
81+
});
82+
}
83+
7684
buildHasard(o){
7785
return this.buildParams(o);
7886
}
@@ -98,19 +106,51 @@ class AbstractAugmenter extends Abstract{
98106
// return this.runAugmenter();
99107
// }
100108
// }
101-
augment(attr, opts) {
102-
this.checkParams(opts);
109+
augment(attrs, opts, runOpts) {
110+
opts.forEach(o => {
111+
this.checkParams(o);
112+
})
113+
114+
const res = this.backend.splitImages(attrs.images).map((image, index) => {
115+
const points = ((attrs.points && attrs.points[index]) || []).map(p => {
116+
if (Array.isArray(p)) {
117+
return this.backend.point(p[0], p[1]);
118+
}
119+
return p;
120+
});
121+
122+
const newAttrs = Object.assign(
123+
{},
124+
attrs,
125+
{image}, {images: null},
126+
{points},
127+
{boxes: (attrs.boxes && attrs.boxes[index]) || []}
128+
)
129+
return this.augmentOne(
130+
newAttrs,
131+
opts[index],
132+
runOpts
133+
)
134+
});
135+
103136
return {
104-
image: this.augmentImage(attr, opts),
105-
boxes: this.augmentBoxes(attr, opts),
106-
points: this.augmentPoints(attr, opts)
137+
images: res.map(r => r.image),
138+
boxes: res.map(r => r.boxes),
139+
points: res.map(r => r.points)
107140
};
108141
}
109-
110-
augmentImage({image}) {
142+
augmentOne(attr, opts, runOpts) {
143+
return {
144+
image: this.augmentImage(attr, opts, runOpts),
145+
boxes: this.augmentBoxes(attr, opts, runOpts),
146+
points: this.augmentPoints(attr, opts, runOpts)
147+
};
148+
}
149+
150+
augmentImage({images}, opts, runOpts) {
151+
// by default do nothing
111152
return image;
112153
}
113-
114154
augmentPoints({points}) {
115155
return points;
116156
}

lib/augmenters/add-weighted.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ const AbstractAugmenter = require('./abstract');
55
/**
66
* Adds noise sampled from a gaussian distribution
77
* @param {Object} opts options
8-
* @param {Array.<Number>} opts.value length-3 RGB pixel to add to the image
8+
* @param {Array.<Number>} opts.value length-3 RGB pixel to add to the images
99
* @param {Number} [opts.alpha=1]
1010
* @example
11-
// Simple usage, add 12 red to the image's pixels
11+
// Simple usage, overlay 10% opacity white layer over the image
1212
new ia.AddWeighted({
1313
value: [255, 255, 255],
1414
alpha: 0.1

lib/augmenters/crop.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ class CropAugmenter extends AbstractAugmenter {
6363

6464

6565
getRect({width, height, params}) {
66-
const size = params.size;
66+
const size = params.size.concat();
6767
return {
6868
x: size[0],
6969
y: size[1],
@@ -80,7 +80,7 @@ class CropAugmenter extends AbstractAugmenter {
8080
return this.backend.crop(image, rect);
8181
}
8282

83-
augmentPoints({points, width, height}, params) {
83+
augmentPointsOnImage({points, width, height}, params) {
8484
const rect = this.getRect({width, height, params});
8585
const origin = this.backend.point(rect.x, rect.y);
8686
return points.map(p => p.sub(origin));

lib/augmenters/sequential.js

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,21 +28,34 @@ class Sequential extends AbstractAugmenter {
2828
return this.steps;
2929
}
3030
augment(o, steps, runOpts) {
31-
let promise = Promise.resolve(o)
32-
steps.forEach(s => {
33-
s.setBackend(this.backend.key)
34-
//console.log({s})
35-
promise = promise.then(current => {
36-
if(!this.backend.isImage(current.image)){
37-
throw(new Error(`Can only augment on object with image property in it`))
38-
}
39-
if(this.backend.isEmptyImage(current.image)){
40-
throw(new Error(`Empty image not allowed`))
41-
}
42-
return s.runAugmenter(Object.assign({},runOpts, current));
43-
})
31+
const promises = this.backend.splitImages(o.images).map((image, imageIndex) => {
32+
let promise = Promise.resolve({images: [image]})
33+
steps[imageIndex].forEach(s => {
34+
s.setBackend(this.backend.key)
35+
//console.log({s})
36+
promise = promise.then(current => {
37+
38+
if(!this.backend.isImages(current.images)){
39+
//console.log(this.backend.isImages.toString(), current)
40+
throw(new Error(`Can only augment on object with image property in it`))
41+
}
42+
if(this.backend.isEmptyImages(current.images)){
43+
throw(new Error(`Empty image not allowed`))
44+
}
45+
const runOpts2 = Object.assign({},runOpts, current);
46+
return s.runAugmenter(runOpts2);
47+
})
48+
});
49+
return promise;
50+
})
51+
52+
return Promise.all(promises).then(results => {
53+
return {
54+
images: results.map(({images}) => images).reduce((a,b) => a.concat(b)),
55+
points: results.map(({points}) => points).reduce((a,b) => a.concat(b)),
56+
boxes: results.map(({boxes}) => boxes).reduce((a,b) => a.concat(b))
57+
}
4458
});
45-
return promise;
4659
}
4760
}
4861
module.exports = Sequential;

lib/backend/index.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@ module.exports = {
1616
get(k){
1717
if(k === 'opencv4nodejs'){
1818
return new OpenCVBackend();
19-
} else if('tfjs'){
19+
} else if(k === 'tfjs'){
2020
return new TensorflowBackend()
21+
} else if(k.version && typeof(k.version.tfjs) === 'string'){
22+
return new TensorflowBackend(k)
23+
} else {
24+
throw(new Error(`invalid backend ${k}`))
2125
}
2226
}
2327
};

lib/backend/opencv-backend.js

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
const debug = require('debug')('image-augment:backend:opencv4nodejs');
22

3+
const pluralAny = function(fn){
4+
return function(imgs){
5+
if(Array.isArray(imgs)){
6+
return imgs.map(fn).filter(a => !a).length === 0
7+
} else {
8+
return false
9+
}
10+
}
11+
}
12+
313
class OpenCVBackend {
414
constructor() {
515
this._cv = require('opencv4nodejs');
@@ -40,17 +50,22 @@ class OpenCVBackend {
4050
}
4151
return false
4252
}
53+
isImages(imgs){
54+
return pluralAny(this.isImage)(imgs)
55+
}
4356
isEmptyImage(img){
4457
if(img.cols === 0 || img.rows === 0){
4558
return true
4659
}
4760
return false
4861
}
62+
isEmptyImages(imgs){
63+
return pluralAny(this.isEmptyImage)(imgs)
64+
}
4965
resize(image, width, height){
5066
return image.resize(height, width);
5167
}
5268
drawBoxes(image, boxes, color, thickness = 1){
53-
console.log({boxes})
5469
boxes.forEach(box => {
5570
image.drawRectangle(
5671
this.point(box[0], box[1]),
@@ -106,6 +121,15 @@ class OpenCVBackend {
106121
return resImg;
107122
}
108123

124+
addWeighteds(imgs, imgs2, alphas) {
125+
if(imgs.length !== imgs2.length || imgs.length !== alpha.length){
126+
throw(new Error('lenght does not match'))
127+
}
128+
return imgs.map((img, i) => {
129+
return this.addWeighted(imgs[i], imgs2[i], alphas[i]);
130+
})
131+
}
132+
109133
addWeighted(img, img2, alpha) {
110134
const typeSigned = img.channels === 4 ? this._cv.CV_16SC4 : this._cv.CV_16SC3;
111135
const typeFinal = img.channels === 4 ? this._cv.CV_8UC4 : this._cv.CV_8UC3;
@@ -116,7 +140,6 @@ class OpenCVBackend {
116140
const resImg = added.convertTo(typeFinal);
117141
return resImg;
118142
}
119-
120143
blur(img, size) {
121144
return img.blur(new this._cv.Size(size[0], size[1]));
122145
}
@@ -209,7 +232,9 @@ class OpenCVBackend {
209232
readImage(filename) {
210233
return this._cv.imreadAsync(filename, this._cv.IMREAD_UNCHANGED);
211234
}
212-
235+
readImages(filenames) {
236+
return Promise.all(filenames.map(filename => this.readImage(filename)));
237+
}
213238
overlay({foreground, background, width, height, channels}) {
214239

215240
if (channels === 3) {
@@ -243,16 +268,29 @@ class OpenCVBackend {
243268
writeImage(filename, img) {
244269
return this._cv.imwriteAsync(filename, img);
245270
}
246-
271+
writeImages(filenames, imgs) {
272+
if(imgs.length !== filenames.length){
273+
throw (new Error('array length and filenames lenght should match'))
274+
}
275+
return Promise.all(imgs.map((img, i) => this.writeImage(filenames[i], img)))
276+
}
247277
imageToBuffer(img) {
248-
return img.getData();
278+
return Promise.resolve(img.getData());
249279
}
250-
251-
getMetadata(img) {
280+
imagesToBuffer(imgs) {
281+
return Promise.all(imgs.map(this.imageToBuffer)).then(buffers => {
282+
return Buffer.concat(buffers);
283+
})
284+
}
285+
splitImages(imgs){
286+
return imgs;
287+
}
288+
getMetadata(imgs) {
252289
return {
253-
width: img.cols,
254-
height: img.rows,
255-
channels: img.channels
290+
nImages: imgs.length,
291+
width: imgs[0].cols,
292+
height: imgs[0].rows,
293+
channels: imgs[0].channels
256294
};
257295
}
258296

0 commit comments

Comments
 (0)