retinanetjs
Version:
Wrapper for models built using keras-retinanet.
183 lines • 16.8 kB
JavaScript
import * as tf from '@tensorflow/tfjs';
import { Zeros } from '@tensorflow/tfjs-layers/dist/initializers'; // tslint:disable-line
import { anchorsForShape, defaultAnchorParameters } from './anchors';
class UpsampleLike extends tf.layers.Layer {
constructor() {
super({});
}
computeOutputShape(inputShape) {
return [
inputShape[0][0],
inputShape[1][1],
inputShape[1][2],
inputShape[0][3]
];
}
call(inputs, _) {
const [source, target] = inputs;
const targetShape = target.shape;
return tf.image.resizeNearestNeighbor(source, [
targetShape[1],
targetShape[2]
]);
}
static get className() {
return 'UpsampleLike';
}
}
class PriorProbability extends Zeros {
}
// tslint:disable-line
/** @nocollapse */
PriorProbability.className = 'PriorProbability';
tf.serialization.registerClass(UpsampleLike); // Needed for serialization.
tf.serialization.registerClass(PriorProbability);
/**
* Represents a RetinaNet model. Rather than creating directly,
* it is intended to be created using `load()`.
*/
export class RetinaNet {
constructor(model, classes, preprocessingMode, anchorParams = defaultAnchorParameters) {
const [height, width] = model.inputs[0].shape.slice(1, 3);
// tslint:disable-next-line
if (height === -1 || width === -1) {
throw new Error('RetinaNetJS only supports fixed input sizes.');
}
// tslint:disable-next-line
if (preprocessingMode !== 'tf' && preprocessingMode !== 'caffe') {
throw new Error('preprocessingMode must be either `tf` or `caffe`.');
}
this.width = width;
this.height = height;
this.model = model;
this.classes = classes;
this.preprocessingMode = preprocessingMode;
this.anchorParams = anchorParams;
}
/**
* Computes predictions. We currently do not support class-specific filtering.
* When non-max suppression is applied, it will be across all boxes, regardless of class.
*
* @param img The image object on which to run object detection
* @param threshold The prediction threshold
* @param nmsThreshold The non-max suppresion IoU threshold
*/
async detect(img, threshold = 0.5, nmsThreshold = 0.5) {
// Build model input from image
const [X, padX, padY] = tf.tidy(() => {
const imageTensor = !(img instanceof tf.Tensor)
? tf.browser.fromPixels(img)
: img;
return this.handleImageTensor(imageTensor);
});
// Run inference
const y = this.model.predict(X);
const [coords, classification] = tf.tidy(() => {
const [boxDeltas, classScores] = y.map(t => t.squeeze([0]));
const anchorBoxes = anchorsForShape(this.model.inputs[0].shape.slice(1, 3), this.anchorParams);
const mean = [0, 0, 0, 0];
const std = [0.2, 0.2, 0.2, 0.2];
const width = anchorBoxes
.slice([0, 2], [-1, 1])
.sub(anchorBoxes.slice([0, 0], [-1, 1]));
const height = anchorBoxes
.slice([0, 2], [-1, 1])
.sub(anchorBoxes.slice([0, 0], [-1, 1]));
const x1y1x2y2 = tf.concat([0, 1, 2, 3].map(i => {
return anchorBoxes.slice([0, i], [-1, 1]).add(boxDeltas
.slice([0, i], [-1, 1])
.mul(std[i])
.add(mean[i])
.mul(i % 2 === 0 ? width : height));
}), 1);
// TODO: Add support for class-specific filtering and turning nms on and off, just
// like in retinanet.
return [x1y1x2y2, classScores];
});
const selected = await tf.image.nonMaxSuppressionAsync(coords, classification.max(1, false), 300, nmsThreshold, threshold);
const detections = tf.tidy(() => {
const classificationNms = classification.gather(selected);
const coordsNms = coords
.gather(selected)
.div([
this.width - padX,
this.height - padY,
this.width - padX,
this.height - padY
]);
const x1y1x2y2ls = tf
.concat([
coordsNms,
classificationNms
.argMax(1)
.expandDims(1)
.cast('float32'),
classificationNms.max(1, true)
], 1)
.arraySync();
return x1y1x2y2ls.map(r => {
const [x1, y1, x2, y2, labelIdx, score] = r;
return { label: this.classes[labelIdx], score, x1, x2, y1, y2 };
});
});
X.dispose();
y.map(t => t.dispose());
selected.dispose();
return detections;
}
/**
* Remove the model from memory.
*/
dispose() {
this.model.dispose();
}
handleImageTensor(imageTensor) {
return tf.tidy(() => {
const inputHeight = imageTensor.shape[0];
const inputWidth = imageTensor.shape[1];
const [outputHeight, outputWidth] = [this.height, this.width];
const scale = Math.min(outputHeight / inputHeight, outputWidth / inputWidth);
const padY = outputHeight - Math.round(scale * inputHeight);
const padX = outputWidth - Math.round(scale * inputWidth);
const scaledTensor = scale === 1
? imageTensor
: imageTensor.resizeBilinear([
Math.round(scale * inputHeight),
Math.round(scale * inputWidth)
]);
const paddedTensor = padX === 0 && padY === 0
? scaledTensor
: scaledTensor.pad([[0, padY], [0, padX], [0, 0]]);
const normedTensor = this.preprocessingMode === 'tf'
? paddedTensor.sub(127.5).div(127.5)
: paddedTensor.sub(tf.tensor3d([[[103.939, 116.779, 123.68]]]));
return [normedTensor.expandDims(0).cast('float32'), padX, padY];
});
}
}
/**
*
* @param modelPath The path to the model or a `tf.io.IOHandler` object
* @param classes The list of detected classes
* @param preprocessingMode One of `tf` or `caffe`. Check the `preprocess_images`
* method of your backbone to see which you should use.
* @param onProgress A callback to report progress
* @param anchorParams The anchor parameters for your model
*/
export async function load(modelPath, classes, preprocessingMode, onProgress, anchorParams = defaultAnchorParameters) {
const tfOnProgress = (progress) => onProgress(0.9 * progress, 'Downloading');
const model = await tf.loadLayersModel(modelPath, {
onProgress: tfOnProgress,
strict: false
});
const detector = new RetinaNet(model, classes, preprocessingMode, anchorParams);
if (onProgress)
onProgress(0.92, 'Building'); // tslint:disable-line
setTimeout(async () => {
await detector.detect(tf.ones([100, 100, 3]));
if (onProgress)
onProgress(1.0, 'Finished'); // tslint:disable-line
}, 100);
return detector;
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmV0aW5hbmV0LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2xpYi9yZXRpbmFuZXQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUN2QyxPQUFPLEVBQUUsS0FBSyxFQUFFLE1BQU0sMkNBQTJDLENBQUMsQ0FBQyxzQkFBc0I7QUFFekYsT0FBTyxFQUVMLGVBQWUsRUFDZix1QkFBdUIsRUFDeEIsTUFBTSxXQUFXLENBQUM7QUFFbkIsTUFBTSxZQUFhLFNBQVEsRUFBRSxDQUFDLE1BQU0sQ0FBQyxLQUFLO0lBQ3hDO1FBQ0UsS0FBSyxDQUFDLEVBQUUsQ0FBQyxDQUFDO0lBQ1osQ0FBQztJQUVNLGtCQUFrQixDQUFDLFVBQXNCO1FBQzlDLE9BQU87WUFDTCxVQUFVLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO1lBQ2hCLFVBQVUsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7WUFDaEIsVUFBVSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQztZQUNoQixVQUFVLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO1NBQ2pCLENBQUM7SUFDSixDQUFDO0lBRU0sSUFBSSxDQUFDLE1BQXFCLEVBQUUsQ0FBUztRQUMxQyxNQUFNLENBQUMsTUFBTSxFQUFFLE1BQU0sQ0FBQyxHQUFHLE1BQU0sQ0FBQztRQUNoQyxNQUFNLFdBQVcsR0FBRyxNQUFNLENBQUMsS0FBSyxDQUFDO1FBQ2pDLE9BQU8sRUFBRSxDQUFDLEtBQUssQ0FBQyxxQkFBcUIsQ0FBQyxNQUFNLEVBQUU7WUFDNUMsV0FBVyxDQUFDLENBQUMsQ0FBQztZQUNkLFdBQVcsQ0FBQyxDQUFDLENBQUM7U0FDZixDQUFDLENBQUM7SUFDTCxDQUFDO0lBRU0sTUFBTSxLQUFLLFNBQVM7UUFDekIsT0FBTyxjQUFjLENBQUM7SUFDeEIsQ0FBQztDQUNGO0FBRUQsTUFBTSxnQkFBaUIsU0FBUSxLQUFLOztBQUNsQyxzQkFBc0I7QUFDdEIsa0JBQWtCO0FBQ0osMEJBQVMsR0FBRyxrQkFBa0IsQ0FBQztBQUcvQyxFQUFFLENBQUMsYUFBYSxDQUFDLGFBQWEsQ0FBQyxZQUFZLENBQUMsQ0FBQyxDQUFDLDRCQUE0QjtBQUMxRSxFQUFFLENBQUMsYUFBYSxDQUFDLGFBQWEsQ0FBQyxnQkFBZ0IsQ0FBQyxDQUFDO0FBZWpEOzs7R0FHRztBQUNILE1BQU0sT0FBTyxTQUFTO0lBUXBCLFlBQ0UsS0FBcUIsRUFDckIsT0FBaUIsRUFDakIsaUJBQXlCLEVBQ3pCLFlBQVksR0FBRyx1QkFBdUI7UUFFdEMsTUFBTSxDQUFDLE1BQU0sRUFBRSxLQUFLLENBQUMsR0FBRyxLQUFLLENBQUMsTUFBTSxDQUFDLENBQUMsQ0FBQyxDQUFDLEtBQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBYSxDQUFDO1FBQ3ZFLDJCQUEyQjtRQUMzQixJQUFJLE1BQU0sS0FBSyxDQUFDLENBQUMsSUFBSSxLQUFLLEtBQUssQ0FBQyxDQUFDLEVBQUU7WUFDakMsTUFBTSxJQUFJLEtBQUssQ0FBQyw4Q0FBOEMsQ0FBQyxDQUFDO1NBQ2pFO1FBQ0QsMkJBQTJCO1FBQzNCLElBQUksaUJBQWlCLEtBQUssSUFBSSxJQUFJLGlCQUFpQixLQUFLLE9BQU8sRUFBRTtZQUMvRCxNQUFNLElBQUksS0FBSyxDQUFDLG1EQUFtRCxDQUFDLENBQUM7U0FDdEU7UUFDRCxJQUFJLENBQUMsS0FBSyxHQUFHLEtBQUssQ0FBQztRQUNuQixJQUFJLENBQUMsTUFBTSxHQUFHLE1BQU0sQ0FBQztRQUNyQixJQUFJLENBQUMsS0FBSyxHQUFHLEtBQUssQ0FBQztRQUNuQixJQUFJLENBQUMsT0FBTyxHQUFHLE9BQU8sQ0FBQztRQUN2QixJQUFJLENBQUMsaUJBQWlCLEdBQUcsaUJBQWlCLENBQUM7UUFDM0MsSUFBSSxDQUFDLFlBQVksR0FBRyxZQUFZLENBQUM7SUFDbkMsQ0FBQztJQUVEOzs7Ozs7O09BT0c7SUFDSSxLQUFLLENBQUMsTUFBTSxDQUNqQixHQUtvQixFQUNwQixTQUFTLEdBQUcsR0FBRyxFQUNmLFlBQVksR0FBRyxHQUFHO1FBRWxCLCtCQUErQjtRQUMvQixNQUFNLENBQUMsQ0FBQyxFQUFFLElBQUksRUFBRSxJQUFJLENBQUMsR0FBRyxFQUFFLENBQUMsSUFBSSxDQUFDLEdBQUcsRUFBRTtZQUNuQyxNQUFNLFdBQVcsR0FBRyxDQUFDLENBQUMsR0FBRyxZQUFZLEVBQUUsQ0FBQyxNQUFNLENBQUM7Z0JBQzdDLENBQUMsQ0FBQyxFQUFFLENBQUMsT0FBTyxDQUFDLFVBQVUsQ0FBQyxHQUFHLENBQUM7Z0JBQzVCLENBQUMsQ0FBQyxHQUFHLENBQUM7WUFDUixPQUFPLElBQUksQ0FBQyxpQkFBaUIsQ0FBQyxXQUFXLENBQUMsQ0FBQztRQUM3QyxDQUFDLENBQUMsQ0FBQztRQUNILGdCQUFnQjtRQUNoQixNQUFNLENBQUMsR0FBRyxJQUFJLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQWtCLENBQUM7UUFDakQsTUFBTSxDQUFDLE1BQU0sRUFBRSxjQUFjLENBQUMsR0FBRyxFQUFFLENBQUMsSUFBSSxDQUFDLEdBQUcsRUFBRTtZQUM1QyxNQUFNLENBQUMsU0FBUyxFQUFFLFdBQVcsQ0FBQyxHQUFHLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO1lBQzVELE1BQU0sV0FBVyxHQUFHLGVBQWUsQ0FDakMsSUFBSSxDQUFDLEtBQUssQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLENBQUMsS0FBTSxDQUFDLEtBQUssQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFhLEVBQ25ELElBQUksQ0FBQyxZQUFZLENBQ2xCLENBQUM7WUFFRixNQUFNLElBQUksR0FBRyxDQUFDLENBQUMsRUFBRSxDQUFDLEVBQUUsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDO1lBQzFCLE1BQU0sR0FBRyxHQUFHLENBQUMsR0FBRyxFQUFFLEdBQUcsRUFBRSxHQUFHLEVBQUUsR0FBRyxDQUFDLENBQUM7WUFDakMsTUFBTSxLQUFLLEdBQUcsV0FBVztpQkFDdEIsS0FBSyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUM7aUJBQ3RCLEdBQUcsQ0FBQyxXQUFXLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO1lBQzNDLE1BQU0sTUFBTSxHQUFHLFdBQVc7aUJBQ3ZCLEtBQUssQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDO2lCQUN0QixHQUFHLENBQUMsV0FBVyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQztZQUUzQyxNQUFNLFFBQVEsR0FBRyxFQUFFLENBQUMsTUFBTSxDQUN4QixDQUFDLENBQUMsRUFBRSxDQUFDLEVBQUUsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsRUFBRTtnQkFDbkIsT0FBTyxXQUFXLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsQ0FBQyxHQUFHLENBQzNDLFNBQVM7cUJBQ04sS0FBSyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUM7cUJBQ3RCLEdBQUcsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUM7cUJBQ1gsR0FBRyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsQ0FBQztxQkFDWixHQUFHLENBQUMsQ0FBQyxHQUFHLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDLENBQ3JDLENBQUM7WUFDSixDQUFDLENBQUMsRUFDRixDQUFDLENBQ2EsQ0FBQztZQUVqQixrRkFBa0Y7WUFDbEYscUJBQXFCO1lBQ3JCLE9BQU8sQ0FBQyxRQUFRLEVBQUUsV0FBVyxDQUFDLENBQUM7UUFDakMsQ0FBQyxDQUFDLENBQUM7UUFDSCxNQUFNLFFBQVEsR0FBRyxNQUFNLEVBQUUsQ0FBQyxLQUFLLENBQUMsc0JBQXNCLENBQ3BELE1BQU0sRUFDTixjQUFjLENBQUMsR0FBRyxDQUFDLENBQUMsRUFBRSxLQUFLLENBQUMsRUFDNUIsR0FBRyxFQUNILFlBQVksRUFDWixTQUFTLENBQ1YsQ0FBQztRQUNGLE1BQU0sVUFBVSxHQUFHLEVBQUUsQ0FBQyxJQUFJLENBQUMsR0FBRyxFQUFFO1lBQzlCLE1BQU0saUJBQWlCLEdBQUcsY0FBYyxDQUFDLE1BQU0sQ0FBQyxRQUFRLENBQWdCLENBQUM7WUFDekUsTUFBTSxTQUFTLEdBQUcsTUFBTTtpQkFDckIsTUFBTSxDQUFDLFFBQVEsQ0FBQztpQkFDaEIsR0FBRyxDQUFDO2dCQUNILElBQUksQ0FBQyxLQUFLLEdBQUcsSUFBSTtnQkFDakIsSUFBSSxDQUFDLE1BQU0sR0FBRyxJQUFJO2dCQUNsQixJQUFJLENBQUMsS0FBSyxHQUFHLElBQUk7Z0JBQ2pCLElBQUksQ0FBQyxNQUFNLEdBQUcsSUFBSTthQUNuQixDQUFDLENBQUM7WUFDTCxNQUFNLFVBQVUsR0FBRyxFQUFFO2lCQUNsQixNQUFNLENBQ0w7Z0JBQ0UsU0FBUztnQkFDVCxpQkFBaUI7cUJBQ2QsTUFBTSxDQUFDLENBQUMsQ0FBQztxQkFDVCxVQUFVLENBQUMsQ0FBQyxDQUFDO3FCQUNiLElBQUksQ0FBQyxTQUFTLENBQUM7Z0JBQ2xCLGlCQUFpQixDQUFDLEdBQUcsQ0FBQyxDQUFDLEVBQUUsSUFBSSxDQUFDO2FBQy9CLEVBQ0QsQ0FBQyxDQUNGO2lCQUNBLFNBQVMsRUFBZ0IsQ0FBQztZQUM3QixPQUFPLFVBQVUsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLEVBQUU7Z0JBQ3hCLE1BQU0sQ0FBQyxFQUFFLEVBQUUsRUFBRSxFQUFFLEVBQUUsRUFBRSxFQUFFLEVBQUUsUUFBUSxFQUFFLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQztnQkFDNUMsT0FBTyxFQUFFLEtBQUssRUFBRSxJQUFJLENBQUMsT0FBTyxDQUFDLFFBQVEsQ0FBQyxFQUFFLEtBQUssRUFBRSxFQUFFLEVBQUUsRUFBRSxFQUFFLEVBQUUsRUFBRSxFQUFFLEVBQUUsQ0FBQztZQUNsRSxDQUFDLENBQUMsQ0FBQztRQUNMLENBQUMsQ0FBQyxDQUFDO1FBQ0gsQ0FBQyxDQUFDLE9BQU8sRUFBRSxDQUFDO1FBQ1osQ0FBQyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxPQUFPLEVBQUUsQ0FBQyxDQUFDO1FBQ3hCLFFBQVEsQ0FBQyxPQUFPLEVBQUUsQ0FBQztRQUNuQixPQUFPLFVBQVUsQ0FBQztJQUNwQixDQUFDO0lBRUQ7O09BRUc7SUFDSSxPQUFPO1FBQ1osSUFBSSxDQUFDLEtBQUssQ0FBQyxPQUFPLEVBQUUsQ0FBQztJQUN2QixDQUFDO0lBRU8saUJBQWlCLENBQ3ZCLFdBQXdCO1FBRXhCLE9BQU8sRUFBRSxDQUFDLElBQUksQ0FBQyxHQUFHLEVBQUU7WUFDbEIsTUFBTSxXQUFXLEdBQUcsV0FBVyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQztZQUN6QyxNQUFNLFVBQVUsR0FBRyxXQUFXLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDO1lBQ3hDLE1BQU0sQ0FBQyxZQUFZLEVBQUUsV0FBVyxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsTUFBTSxFQUFFLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQztZQUM5RCxNQUFNLEtBQUssR0FBRyxJQUFJLENBQUMsR0FBRyxDQUNwQixZQUFZLEdBQUcsV0FBVyxFQUMxQixXQUFXLEdBQUcsVUFBVSxDQUN6QixDQUFDO1lBQ0YsTUFBTSxJQUFJLEdBQUcsWUFBWSxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsS0FBSyxHQUFHLFdBQVcsQ0FBQyxDQUFDO1lBQzVELE1BQU0sSUFBSSxHQUFHLFdBQVcsR0FBRyxJQUFJLENBQUMsS0FBSyxDQUFDLEtBQUssR0FBRyxVQUFVLENBQUMsQ0FBQztZQUMxRCxNQUFNLFlBQVksR0FDaEIsS0FBSyxLQUFLLENBQUM7Z0JBQ1QsQ0FBQyxDQUFDLFdBQVc7Z0JBQ2IsQ0FBQyxDQUFDLFdBQVcsQ0FBQyxjQUFjLENBQUM7b0JBQ3pCLElBQUksQ0FBQyxLQUFLLENBQUMsS0FBSyxHQUFHLFdBQVcsQ0FBQztvQkFDL0IsSUFBSSxDQUFDLEtBQUssQ0FBQyxLQUFLLEdBQUcsVUFBVSxDQUFDO2lCQUMvQixDQUFDLENBQUM7WUFDVCxNQUFNLFlBQVksR0FDaEIsSUFBSSxLQUFLLENBQUMsSUFBSSxJQUFJLEtBQUssQ0FBQztnQkFDdEIsQ0FBQyxDQUFDLFlBQVk7Z0JBQ2QsQ0FBQyxDQUFDLFlBQVksQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUMsRUFBRSxJQUFJLENBQUMsRUFBRSxDQUFDLENBQUMsRUFBRSxJQUFJLENBQUMsRUFBRSxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7WUFDdkQsTUFBTSxZQUFZLEdBQ2hCLElBQUksQ0FBQyxpQkFBaUIsS0FBSyxJQUFJO2dCQUM3QixDQUFDLENBQUMsWUFBWSxDQUFDLEdBQUcsQ0FBQyxLQUFLLENBQUMsQ0FBQyxHQUFHLENBQUMsS0FBSyxDQUFDO2dCQUNwQyxDQUFDLENBQUMsWUFBWSxDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxDQUFDLE9BQU8sRUFBRSxPQUFPLEVBQUUsTUFBTSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQztZQUNwRSxPQUFPLENBQUMsWUFBWSxDQUFDLFVBQVUsQ0FBQyxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDLEVBQUUsSUFBSSxFQUFFLElBQUksQ0FJN0QsQ0FBQztRQUNKLENBQUMsQ0FBQyxDQUFDO0lBQ0wsQ0FBQztDQUNGO0FBRUQ7Ozs7Ozs7O0dBUUc7QUFDSCxNQUFNLENBQUMsS0FBSyxVQUFVLElBQUksQ0FDeEIsU0FBbUMsRUFDbkMsT0FBaUIsRUFDakIsaUJBQXlCLEVBQ3pCLFVBQXdELEVBQ3hELFlBQVksR0FBRyx1QkFBdUI7SUFFdEMsTUFBTSxZQUFZLEdBQUcsQ0FBQyxRQUFnQixFQUFFLEVBQUUsQ0FDeEMsVUFBVSxDQUFDLEdBQUcsR0FBRyxRQUFRLEVBQUUsYUFBYSxDQUFDLENBQUM7SUFDNUMsTUFBTSxLQUFLLEdBQUcsTUFBTSxFQUFFLENBQUMsZUFBZSxDQUFDLFNBQVMsRUFBRTtRQUNoRCxVQUFVLEVBQUUsWUFBWTtRQUN4QixNQUFNLEVBQUUsS0FBSztLQUNkLENBQUMsQ0FBQztJQUNILE1BQU0sUUFBUSxHQUFHLElBQUksU0FBUyxDQUM1QixLQUFLLEVBQ0wsT0FBTyxFQUNQLGlCQUFpQixFQUNqQixZQUFZLENBQ2IsQ0FBQztJQUNGLElBQUksVUFBVTtRQUFFLFVBQVUsQ0FBQyxJQUFJLEVBQUUsVUFBVSxDQUFDLENBQUMsQ0FBQyxzQkFBc0I7SUFDcEUsVUFBVSxDQUFDLEtBQUssSUFBSSxFQUFFO1FBQ3BCLE1BQU0sUUFBUSxDQUFDLE1BQU0sQ0FBQyxFQUFFLENBQUMsSUFBSSxDQUFDLENBQUMsR0FBRyxFQUFFLEdBQUcsRUFBRSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7UUFDOUMsSUFBSSxVQUFVO1lBQUUsVUFBVSxDQUFDLEdBQUcsRUFBRSxVQUFVLENBQUMsQ0FBQyxDQUFDLHNCQUFzQjtJQUNyRSxDQUFDLEVBQUUsR0FBRyxDQUFDLENBQUM7SUFDUixPQUFPLFFBQVEsQ0FBQztBQUNsQixDQUFDIn0=