From fb61e4905b76e42aa8e63a5d665846f2ba3c0bf2 Mon Sep 17 00:00:00 2001 From: Kota Yamaguchi Date: Wed, 8 Jul 2015 13:33:59 +0900 Subject: [PATCH] v2.0 initial commit --- LICENSE | 46 +- README.md | 398 +--- css/main.css | 172 ++ data/annotations/.gitignore | 0 data/annotations/1.png | Bin 0 -> 5722 bytes data/example.json | 19 + data/images/.gitignore | 0 example.jpg => data/images/1.jpg | Bin data/images/2.jpg | Bin 0 -> 144662 bytes index.html | 254 +- js/app/edit.js | 404 ++++ js/app/index.js | 56 + js/helper/colormap.js | 104 + js/helper/pagination.js | 64 + js/helper/segment-annotator.js | 538 +++++ js/helper/segment-viewer.js | 134 ++ js/helper/util.js | 51 + js/image/canny.js | 225 ++ js/image/distance-transform.js | 106 + js/image/layer.js | 216 ++ js/image/morph.js | 40 + js/image/morph/max-filter.js | 49 + js/image/morph/neighbor-map.js | 47 + js/image/segmentation.js | 32 + js/image/segmentation/base.js | 19 + .../binary-heap-priority-queue.js | 90 + .../image/segmentation/pff.js | 190 +- .../image/segmentation/slic.js | 249 +- js/image/segmentation/slico.js | 507 ++++ js/image/segmentation/watershed.js | 322 +++ js/main.js | 42 + js/require.js | 2087 +++++++++++++++++ pre-segmentation.js | 148 -- segment-annotator.js | 573 ----- 34 files changed, 5580 insertions(+), 1602 deletions(-) create mode 100644 css/main.css create mode 100644 data/annotations/.gitignore create mode 100644 data/annotations/1.png create mode 100644 data/example.json create mode 100644 data/images/.gitignore rename example.jpg => data/images/1.jpg (100%) create mode 100644 data/images/2.jpg create mode 100644 js/app/edit.js create mode 100644 js/app/index.js create mode 100644 js/helper/colormap.js create mode 100644 js/helper/pagination.js create mode 100644 js/helper/segment-annotator.js create mode 100644 js/helper/segment-viewer.js create mode 100644 js/helper/util.js create mode 100644 js/image/canny.js create mode 100644 js/image/distance-transform.js create mode 100644 js/image/layer.js create mode 100644 js/image/morph.js create mode 100644 js/image/morph/max-filter.js create mode 100644 js/image/morph/neighbor-map.js create mode 100644 js/image/segmentation.js create mode 100644 js/image/segmentation/base.js create mode 100644 js/image/segmentation/binary-heap-priority-queue.js rename pf-segmentation.js => js/image/segmentation/pff.js (61%) rename slic-segmentation.js => js/image/segmentation/slic.js (72%) create mode 100644 js/image/segmentation/slico.js create mode 100644 js/image/segmentation/watershed.js create mode 100644 js/main.js create mode 100644 js/require.js delete mode 100644 pre-segmentation.js delete mode 100644 segment-annotator.js diff --git a/LICENSE b/LICENSE index 42ce27d..110216b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,26 +1,26 @@ -Copyright (c) 2013, Kota Yamaguchi -All rights reserved. +Copyright (c) 2015, Kota Yamaguchi +All rights reserved. -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of nor the names of its contributors may be used to - endorse or promote products derived from this software without specific - prior written permission. + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index c17e96e..1877d9c 100644 --- a/README.md +++ b/README.md @@ -4,373 +4,87 @@ JS Segment Annotator Javascript image annotation tool based on image segmentation. * Label image regions with mouse. - * Written in vanilla Javascript. No jQuery dependency. + * Written in vanilla Javascript, with require.js dependency (packaged). * Pure client-side implementation of image segmentation. A browser must support HTML canvas to use this tool. There is an [online demo](http://kyamagu.github.io/js-segment-annotator/). -Usage ------ - -Following example illustrates the basic usage. - -
- ... - - - - -Load `pf-segmentation.js` instead of `slic-segmentation.js` to use -`PFSegmentAnnotator`. - -API ---- - -### SLICSegmentAnnotator - - new SLICSegmentAnnotator(imageURL, options) - -A class object to generate an annotation canvas from the given image URL. It -internally calls `SLICSegmentation` to generate segmentation. - - * `imageURL` - URL of an image to annotate. (Caution: do not use a large - image with more than 600px each side.) - * `options` - Optional input arguments. Following options are accepted. - * `onload` - Function to be called upon intialization. The annotator object - is accessible by `this`. - * `annotation` - Optional URL to an existing annotation PNG image. Use the - output of `annotator.getAnnotation`. - * `labels` - Labels to annotate. It can be an array of strings or an array - of objects that has `name` with optional `color` field. For - example, `{ name: 'background', color: [255, 255, 255] }`. - You may use the output of `annotator.getLabels()`. By default, - `['background', 'foreground']` is specified. - * `container` - Container DOM element to place the annotation tool. e.g., - `document.getElementById('annotator')`. By default, the - tool is appended at the end of the `document.body`. - * `highlightAlpha` - Alpha value for the segment under mouse pointer. It - takes a number between 0 to 255. Default 128. - * `backgroundColor` - Color of the background of the annotation image. - Default [192, 192, 192]. - * `regionSize` - Parameter of superpixel size for SLIC segmentation. - * `regularization` - Regularization parameter for SLIC segmentation. - * `minRegionSize` - Minimum segment size in pixels for SLIC segmentation. - -The class inherits from the `SegmentAnnotator` class. - -### PreSegmentAnnotator - - new PreSegmentAnnotator(imageURL, options) - -A class object to generate an annotation canvas from the given image URL. It -internally calls `PreSegmentation` to retrieve a segmentation mask from a PNG -file. - - * `imageURL` - URL of an image to annotate. (Caution: do not use a large - image with more than 600px each side.) - * `options` - Optional input arguments. Following options are accepted. - * `onload` - Function to be called upon intialization. The annotator object - is accessible by `this`. - * `annotation` - **Required** URL to an existing annotation PNG image. Use the - output of `annotator.getAnnotation`. - * `labels` - Labels to annotate. It can be an array of strings or an array - of objects that has `name` with optional `color` field. For - example, `{ name: 'background', color: [255, 255, 255] }`. - You may use the output of `annotator.getLabels()`. By default, - `['background', 'foreground']` is specified. - * `container` - Container DOM element to place the annotation tool. e.g., - `document.getElementById('annotator')`. By default, the - tool is appended at the end of the `document.body`. - * `highlightAlpha` - Alpha value for the segment under mouse pointer. It - takes a number between 0 to 255. Default 128. - * `backgroundColor` - Color of the background of the annotation image. - Default [192, 192, 192]. - -The class inherits from the `SegmentAnnotator` class. - -### PFSegmentAnnotator - - new PFSegmentAnnotator(imageURL, options) - -A class object to generate an annotation canvas from the given image URL. It -internally calls `PFSegmentation` to generate segmentation. - - * `imageURL` - URL of an image to annotate. (Caution: do not use a large - image with more than 600px each side.) - * `options` - Optional input arguments. Following options are accepted. - * `onload` - Function to be called upon intialization. The annotator object - is accessible by `this`. - * `annotation` - Optional URL to an existing annotation PNG image. Use the - output of `annotator.getAnnotation`. - * `labels` - Labels to annotate. It can be an array of strings or an array - of objects that has `name` with optional `color` field. For - example, `{ name: 'background', color: [255, 255, 255] }`. - You may use the output of `annotator.getLabels()`. By default, - `['background', 'foreground']` is specified. - * `container` - Container DOM element to place the annotation tool. e.g., - `document.getElementById('annotator')`. By default, the - tool is appended at the end of the `document.body`. - * `highlightAlpha` - Alpha value for the segment under mouse pointer. It - takes a number between 0 to 255. Default 128. - * `backgroundColor` - Color of the background of the annotation image. - Default [192, 192, 192]. - * `sigma` - Sigma value of gaussian filter in PF-segmentation. - * `threshold` - Threshold value `k` of PF-segmentation. - * `minSize` - Minimum segment size of PF-segmentation. - -The class inherits from the `SegmentAnnotator` class. - -### SegmentAnnotator - - new SegmentAnnotator(segmentation, options) - -Annotation tool class. The constructor takes a result of an image segmentation -algorithm with options, which is internally called inside `PFSegmentAnnotator`. -The class has the following public methods. - -__disable__ Disables mouse input. - - annotator.disable() - -__enable__ Enables mouse input. - - annotator.enable() - -__getLabels__ Get an array of labels. - - annotator.getLabels() - -The return value is an array of objects that looks like -the following. - - [ - { name: 'background', color: [255, 255, 255] }, - { name: 'foreground', color: [255, 0, 0] }, - ... - ] - -__setLabels__ Reset the label definitions. - - annotator.setLabels(labels) - -The input can be an array of strings or an array -of objects that is the same format with the output of `getLabels`. - -__removeLabel__ Remove a specified label definition. - - annotator.removeLabel(index) - -__setCurrentLabel__ Set the current label for annotation. - - annotator.setCurrentLabel(label) - -It can be an index of labels array or the name of the label. For example, -`annotator.setCurrentLabel(1)`. - -__getCurrentLabel__ Get the currently chosen label. - - annotator.getCurrentLabel() - -__setImageAlpha__ Set the alpha value of the image layer. - - annotator.setImageAlpha(alpha) - -A numeric value between 0 and 255. - -__setBoundaryAlpha__ Set the alpha value of the segment boundaries. - - annotator.setBoundaryAlpha(alpha) - -A numeric value between 0 and 255. - -__setFillAlpha__ Set the alpha value of the segment fills. - - annotator.setFillAlpha(alpha) - -A numeric value between 0 and 255. - -__getAnnotation__ Get the current annotation in PNG-format data URL. - - annotator.getAnnotation() - -The PNG image contains at each pixel the index of the label in RGB value. The -index of the label can be retrieved by the following way. - - var label = (data[offset + 0]) | - (data[offset + 1] << 8) | - (data[offset + 2] << 16); - -Here, `data` is the array of RGB values and `offset` is the location of pixel. - -__setAnnotation__ Set the current annotation from a PNG-format image URL. - - annotator.setAnnotation(imageURL, callback) - -The optional callback takes an annotator object. - -### SLICSegmentation - - SLICSegmentation(imageURL, options) - -Javascript implementation of an image segmentation algorithm of - - SLIC Superpixels - Radhakrishna Achanta, Appu Shaji, Kevin Smith, Aurelien Lucchi, Pascal - Fua, and Sabine Süsstrunk - IEEE Transactions on Pattern Analysis and Machine Intelligence, vol. 34, - num. 11, p. 2274 - 2282, May 2012. - -based on the VLFeat implementation. The function takes the following options. - - * `regionSize` - Parameter of superpixel size - * `regularization` - Regularization parameter. See paper. - * `minRegionSize` - Minimum segment size in pixels. - * `toDataURL` - Callback function to receive the result as a data URL. - * `callback` - Function to be called on finish. The function takes a single - argument of result object that contains following fields. - * `width` - Width of the image in pixels. - * `height` - Height of the image in pixels. - * `size` - Number of segments. - * `indexMap` - Int32Array of `width * height` elements containing - segment index for each pixel location. The segment index - at pixel `(i, j)` is `indexMap(i * width + j)`, where - `i` is the y coordinate of the pixel and `j` is the x - coordinate. - -### PreSegmentation - - PreSegmentation(imageURL, options) +Importing data +-------------- -Existing mask loader. +Prepare a JSON file that looks like the following. The required fields are +`labels` and `imageURLs`. The `annotationURLs` are for existing data and can +be omitted. Place the JSON file inside the `data/` directory. + + { + "labels": [ + "background", + "skin", + "hair", + "dress", + "glasses", + "jacket", + "skirt" + ], + "imageURLs": [ + "data/images/1.jpg", + "data/images/2.jpg" + ], + "annotationURLs": [ + "data/annotations/1.png", + "data/annotations/2.png" + ] + } -The function takes the following options. +Open a Web browser and point to the `index.html`. - * `annotation` - Existing annotation URL (required). - * `toDataURL` - Callback function to receive the result as a data URL. - * `callback` - Function to be called on finish. The function takes a single - argument of result object that contains following fields. - * `width` - Width of the image in pixels. - * `height` - Height of the image in pixels. - * `size` - Number of segments. - * `indexMap` - Int32Array of `width * height` elements containing - segment index for each pixel location. The segment index - at pixel `(i, j)` is `indexMap(i * width + j)`, where - `i` is the y coordinate of the pixel and `j` is the x - coordinate. -### PFSegmentation +Matlab tips +----------- - PFSegmentation(imageURL, options) +_Annotation PNG_ -Javascript implementation of the image segmentation algorithm of +The annotation PNG file contains label map encoded in RGB value. Do the +following to encode an index map. - Efficient Graph-Based Image Segmentation - Pedro F. Felzenszwalb and Daniel P. Huttenlocher - International Journal of Computer Vision, 59(2) September 2004. +Encode: -The function takes the following options. + X = cat(3, bitand(annotation, 255), ... + bitand(bitshift(annotation, -8), 255), ... + bitand(bitshift(annotation, -16)), 255)); + imwrite(uint8(X), 'data/annotations/0.png'); - * `sigma` - Parameter for Gaussian pre-smoothing. Default 0.5. - * `threshold` - Threshold value of the algorithm. Default 500. - * `minSize` - Minimum segment size in pixels. Default 20. - * `toDataURL` - callback function to receive the result as a data URL. - * `callback` - function to be called on finish. The function takes a single - argument of a result object that contains following fields. - * `width` - Width of the image in pixels. - * `height` - Height of the image in pixels. - * `size` - Number of segments. - * `indexMap` - Int32Array of `width * height` elements containing - segment index for each pixel location. The segment index - at pixel `(i, j)` is `indexMap(i * width + j)`, where - `i` is the y coordinate of the pixel and `j` is the x - coordinate. - * `rgbData` - Uint8Array of `width * height * 4` elements containing all - RGBA values in the input data. +Decode: -_Example_ + X = imread('data/annotations/0.png'); + annotation = X(:, :, 1); + annotation = bitor(annotation, bitshift(X(:, :, 2), 8)); + annotation = bitor(annotation, bitshift(X(:, :, 3), 16)); -Drawing the segmentation result to a canvas. +_JSON_ - function colorRandomRGB(size, indexMap, imageData) { - var width = imageData.width, - height = imageData.height, - rgbData = imageData.data, - colormap = new Uint8Array(size * 3); - for (var i = 0; i < colormap.length; ++i) - colormap[i] = Math.round(255 * Math.random()); - for (var i = 0; i < height; ++i) { - for (var j = 0; j < width; ++j) { - var index = indexMap[i * width + j]; - rgbData[4 * (i * width + j) + 0] = colormap[3 * index + 0]; - rgbData[4 * (i * width + j) + 1] = colormap[3 * index + 1]; - rgbData[4 * (i * width + j) + 2] = colormap[3 * index + 2]; - rgbData[4 * (i * width + j) + 3] = 255; - } - } - } +Use the `matlab-json` package. - PFSegmentation('/path/to/image.jpg', { - sigma: 0.5, - threshold: 500, - minSize: 100, - callback: function(result) { - var canvas = document.createElement('canvas'); - canvas.width = result.width; - canvas.height = result.height; - var context = canvas.getContext('2d'), - imageData = context.getImageData(0, - 0, - canvas.width, - canvas.height); - colorRandomRGB(result.size, result.indexMap, imageData); - context.putImageData(imageData, 0, 0); - document.body.appendChild(canvas); - } - }); - -Exporting data --------------- - -Check `index.html` for how to design a UI. - -_Matlab_ + * https://github.com/kyamagu/matlab-json -To load the exported annotation from Matlab, download the JSON and byte -encoding tools for Matlab. +_Using dataURL_ - * https://github.com/kyamagu/matlab-json - * http://www.mathworks.com/matlabcentral/fileexchange/39526-byte-encoding-utilities +Get the byte encoding tools. -Then, do the following. + * https://www.mathworks.com/matlabcentral/fileexchange/39526-byte-encoding-utilities - annotation = json.read(‘export.json’); - png_data = base64decode(strrep(annotation.annotation, 'data:image/png;base64,’, ‘’)); - segmentation_map = imdecode(png_data, ‘png’); +Do the following to convert between dataURL and Matlab format. -The resulting `segmentation_map` contains an index of label at each pixel. -Read the `annotation.labels` field to get information about labels. +Encode: -Contributing ------------- + png_data = imencode(annotation, 'png'); + dataURL = ['data:image/png;base64,’, base64encode(png_data)]; -Please send me a pull request. Briefly check -[the style guide](https://github.com/airbnb/javascript) before submitting. +Decode: -### Acknowledgement + dataURL = 'data:image/png;base64,...'; + png_data = base64decode(strrep(dataURL, 'data:image/png;base64,’, ‘’)); + annotation = imdecode(png_data, ‘png’); - * Special thanks to [Long Long Yu](https://github.com/lolongcovas) for SLIC - implementation! diff --git a/css/main.css b/css/main.css new file mode 100644 index 0000000..1b24f36 --- /dev/null +++ b/css/main.css @@ -0,0 +1,172 @@ +.segment-viewer-overlay-container { + background-color: rgba(255, 255, 255, 0.5); + color: black; + display: inline-block; + left: 0; + position: absolute; + top: 0; +} +.segment-viewer-legend-container { + display: inline-block; + bottom: 0; + position: absolute; + right: 0; +} +.segment-viewer-legend-item { + font-family: monospace; + font-size: small; + white-space: nowrap; +} +.segment-viewer-legend-label { + color: gray; +} +.segment-viewer-legend-colorbox { + background-color: white; + border: 1px solid gray; + display: inline-block; + height: .7em; + vertical-align: middle; + width: .7em; +} +.segment-viewer-container { + background-color: gray; + display: inline-block; + position: relative; +} +.segment-viewer-layer { + left: 0; + position: absolute; + top: 0; + zoom: 1.0; + -moz-transform: scale(1.0); +} +.segment-annotator-outer-container { + display: inline-block; + overflow: auto; +} +.segment-annotator-inner-container { + background-color: #ccc; + position: relative; + zoom: 1.0; + -moz-transform: scale(1.0); + -moz-transform-origin: top left; +} +.segment-annotator-layer { + left: 0; + position: absolute; + top: 0; + cursor: pointer; +} +.edit-sidebar { + font-family: monospace; +} +.edit-sidebar p { + margin-bottom: 0; +} +.edit-sidebar-button { + position: relative; + background-color: #eee; + cursor: pointer; + padding-left: 2px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +.edit-sidebar-button:hover { + background-color: #aaa; +} +.edit-sidebar-button-selected, +.edit-sidebar-button-enabled { + background-color: #ccc; +} +.edit-sidebar-legend-colorbox { + background-color: white; + border: 1px solid gray; + display: inline-block; + height: .7em; + vertical-align: middle; + width: .7em; +} +.edit-sidebar-legend-label { + color: gray; + display: inline-block; + width: 10em; + padding: 0 0.3em; +} +.edit-sidebar-legend-label-active { + color: black; + font-weight: bold; +} +.edit-sidebar-popup-trigger { + display: inline-block; + position: relative; + min-width: 1em; + text-align: center; + background-color: #ddd; +} +.edit-sidebar-popup-trigger:hover { + background-color: #999; +} +.edit-sidebar-popup { + display: none; + position: absolute; + top: 1em; + right: 0; + padding: 0.1em 0.3em; + background-color: #ccc; + z-index: 1; +} +.edit-sidebar-popup-active { + display: block; +} +.edit-sidebar-spacer { + height: 1em; +} +.edit-sidebar-submit { + margin-left: 1em; +} +.edit-image-top-menu { + height: 1em; + font-family: monospace; +} +.edit-image-top-menu-item { + display: inline-block; + background-color: #eee; + padding: 0 2px; + min-width: 1em; + text-align: center; +} +.edit-image-top-button { + display: inline-block; + background-color: #eee; + cursor: pointer; + padding: 0 2px; + min-width: 1em; + text-align: center; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +.edit-image-top-button:hover { + background-color: #aaa; +} +.edit-image-top-button-enabled { + background-color: #ccc; +} +.edit-image-top-spacer { + display: inline-block; + width: 1em; +} +.edit-image-display { + display: inline-block; + vertical-align: top; +} +.edit-main-container { + white-space: nowrap; +} +.edit-top-menu-block { + display: inline-block; + margin: 0 .5em; +} diff --git a/data/annotations/.gitignore b/data/annotations/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/data/annotations/1.png b/data/annotations/1.png new file mode 100644 index 0000000000000000000000000000000000000000..5db8d328fcb98427756e4af72fd11ada8f729b8e GIT binary patch literal 5722 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 literal 0 HcmV?d00001 diff --git a/data/example.json b/data/example.json new file mode 100644 index 0000000..41c5fac --- /dev/null +++ b/data/example.json @@ -0,0 +1,19 @@ +{ + "labels": [ + "background", + "skin", + "hair", + "dress", + "glasses", + "jacket", + "skirt" + ], + "imageURLs": [ + "data/images/1.jpg", + "data/images/2.jpg" + ], + "annotationURLs": [ + "data/annotations/1.png", + "data/annotations/2.png" + ] +} diff --git a/data/images/.gitignore b/data/images/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/example.jpg b/data/images/1.jpg similarity index 100% rename from example.jpg rename to data/images/1.jpg diff --git a/data/images/2.jpg b/data/images/2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d8bd7b0cd1143175ae667da4b2127bc8f0f59718 GIT binary patch literal 144662 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 mi&I{ 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%0DW=H$naq|JmIq6vT8q;hd z7h`~+l1ab-b;+#iQ;fG9YPGSKrRi2WL}n`#AifK;F=ui*Zex;gI39zJm7Lxn7cv+$ zNDiSQw$@GX*9GI?jl->HR-N={ahcJ{6n3IJ z=mzAF;~40D2=p};k$G&B8(mBmvbgyr$KF7CIQLV{c^I5MlTuvK>~+(vcXgvab@3O$ zkBQo+#62%d_(JJEpW?ZhVS%A%78e0o=QBvZa@kaG3~|Zw!6lGa>p#N1C&7OQd=LKs z31+*gM|G&`{{ZVIMLU=;-M4~1P84$9r!2$_WAm(b3X81O4OgM`G-}I}-)EqF68JZu z{1ot2w}te{TK@Thz4iQ{dDnB}mTyE}k)YuhfnPOzPyMJaEC{x>SuGZIEe*B5Gs>fl z(oOeOeRiHYWYL4mXE)16A96FTiKhj5J0tClSH$`jir!wKc!@FT66NJTL|nJ&Uyd5L z#Qy*uc$`R|Y+WMF@>(l%hj4$tA?1(PKU(&o!#IcWS$h8fz&y$4G#1Y1>EDPxJNQ#V z+%6=J*e`pD0B^!5_qXS^zZzz_x755rZKlR#lx_<%GnQv0@&P?K<0IVHOt@)6E>oZ2 zO)T9_+7Ulu7vtW8ABFAwSu{^ArSy)r{0L03$M@$@#GfQ|-o#||2O_^Hz9#%i);wLU zf5H{v6BO3I9EK5nD>#nd&IA1(%}56c82#gbIXg!@*U!8hBjBZvi{-yXAS(z z?>^)4*X@7eZ3!e6%cnkb`>_+Y2cazd{#E&#;(rS3x~;%@yhm=QpbKpyh_0m5yf@)}4)4kg$IpbKfT~@IV!!|t5>IORwD=D9!(R>c zO)}S3hCNeXQ)n(82g1Z$HP{KfHd#W%4(>VY(yBRKSxc2b=aykbeWR$K{t8LsO+E?l zuY4il>8)+!XMM3;!f+RG!B#)YzIpJ+z)e9sW8t`U9bU%n-%r1`yu68`k~W*jkWLf= z&HyD;5Oa}{RaY#l6@ErsF|=gtq>ta93XO96!CDT#;^m9O+Qhm_YI>curNz&aZt^Bp z6>l&x!6amWNmIva@V^)QO_8+iPUBIy3kiTU&76Mb6+bVq;j7E!XG^h-d;475_-k07 zNxh3yQ*E$2!*>PBhjkCPWzY;tsTt>~#eP%jzBx@yO|INbjd!TDmql5CD%UJoLF>^r zfnQID%P{JKsa{rZ(D7>HD*HzYaX)ALJNsaGHva%>O|C%$Ha<;K(pxY5j~RHsO8hOh z_>X9(t^N%4i;5xo(loZebw;4;~#=PHF$P0Wp!ySW^3nO6Qn)$!>y8Ve_p6$LRd?)iXjdII$iY;Y(a@pz@NXTW+uTFu170{}} zUk;wX=0~zj`%>>)wul|_q7OpAe>(nYb*cPiCEdlph&A0|t|E3x;<(gr1Eh=hm62st z1bYm391v@vVW#?!>BV(FqtoKOC>(vRFZ=kv<6q3*lk0Ig`pqdni7!XgrToJ7aqfTr z)%;}F^@;VX@hnLtov~Qg%UPL`e4u_~lgU3%rExOLG;tV)4>`!@zKW_+mD(a4@wY6C z{{U{f^8BQHqLGF?vCpYJYXHjM-u-JR(u7|$R;P7L!o2DG8BMtMI+VPfSxd2Dj&eB7 zb15NWK|Oot(zjJRo_m4P&8w^fsor}Iaa!IA(Jb{#w3_x*w1y%$b?-ux9Nz%MA1j0F2TS;2#^D0&-?I$H6 zNXY}t8-T$ip1lVZ&4|ZM{C}Y7(udhNPwjhTRhlb!%W^s7kI(tnIV?{TkW{V@JQ0KV z*FE*U&G}8&*Cdq+JZLlBMi1v)_Fu0!t6Ftpbk}0!*OIY5TNMZ1ulRpMz3~pKZKPPP z>x;Ju8k4jVa(QF-Kmd|`1yireuGrNx6v-hZmL#8g`vT+kc+qq#I1Ztx&8b~Fmyp{< z<=aL$;auiNBb=TGpsrcwkdIQ;Bj+y#_)AgocC`+j0+AUY%_|1k6n?BnbJ0NOzd&p0 z4K_Pp54-qZ!sldm(LL6yG9NN!fPlLR2O;DH41MCf4!InUG;()_-N!0+N78=_^sN&{ z{>|{dm2Dl(joUP{-bPkAwG(WksbB<&5g}t`LV@h zvLQ$;p^Zl3vnro1M{|rY0f^w!rCJQ1h!!(VKfuLc%TLc};kzJIr zI7K_Det+h9RsORmZT)@2)56pELrR&ofNgGC;iPCxYZy@9Fb%NrwLt68WOG^i_ltC| z8f&&XMYY&#ONR?AuGkHnyx%im{naW7{`Gx3g$Tkv$=*+?si<1leNU0c zXLV;$$vZBIc0G^bfA(3`ZS);#P5L~ftH$SeMUZSeN#rTW4e7z`E6L*2d6a5)%1C4SPHyG!wg>iS3XBE6PYW9odvjQs}{@hVHi&i}H>*6PlUPJ!?6Nu1t0U#Lq^2FAzp9YLIOG&TfTT6A79p&d{8`aqx{^Vr$}N}f zM+8>!vFrDpvyR;>omYR#{_=jX`yLT<;-4A3n{PGi-!d`C*$iYKa3d8Cp{f0%_U7mU z#(5r3n+34PBP4YoaB*EKcSZYG)UExktxPsHtAEe^H3KC50IKp_GovUa8G!+Z@Send zDd;6tlH6PPlHClGzND(WlG93{XTMMddY6xWCwO$EW9RkEmj$zXS;&^=CztA zfDESD@v+~oMnLt=a#E>L%F~6H$(pK~T{Fbq>gb$2w@vGtiLw6xp;e8p{{SE$`}D5l zqC&397Z=1?++FQ?1X0N3w6ZtdQhCc%*Lrk~r(av!DD$pic#%gXo>8#};=}<$loPu) zr&8|6)c*jrba3gKKf+%P#=caVZ-t;Q9!V?qgtu;|-AzOLZOS6}(E2i;mqpTa%OwM# z^5Qwb?eAX!T0GC6sr^5}*hLIH8|bV{`yGp$?-%O2b-l@sV`>^TE);C}gz^lP`PM&? zdC1?9Sbwq(w6T0rkHZ%zR?l0HHxn+_FfFn<5(Iu6{P@RipcB)F+CFr|ucDoaG{yw^+V z_;fyu@fD%I@NdHSt!!sgCWjFX9lx4x9!R8V8a0n|0KgV30WxEA0gilQ;r-Ij;VXNK z@D@!raxEEV-7fYG5hY#KV;fkg0hC~jdRK*u{v@00{%7WSgr$eY)m>Y<`|sv(o&>rU zekjpomp)wXE!!O#ZewV$eL*ZgO7p07Y4nSzri?2+oT$hc<1Gts08fyM))oPDR(4v<@vcJ~mZ!Yf1n@_YP4lY}8l1@kbvWOBr&MDs#^)qkc zc)T}ihA1GEyKZfvt}o;g%1O!KD=Lpcp4Ae^MtaR?zZQ59%_EG5www22uWs!vTV1;^ zyy)+si%sxOmiJBN%X04HVe=Mth!GOX;f~f&Lk=;<4P|)7+V<<>1&)nqki{^LG+72{kE}3zn zOMNDv3AwtQ%M_s+5~#cs8*+t9pSsoUPdcLybw!_{;qq#=Jq*7A_^@~v#dh8xduO_i z+8c#(#&~<{4h@nlf*|0c?)b(z=WGAl&00Unr z+OO_{w>bchO7t*!)oN4blfC}{uj`@t&SAi~EKMF*N0szz_3nPzsT)BDIqo}-)$z+@WrPXPQR@mGqz9 z-`IpdY@RQg-O#Sc^V#HL2GTnWn)>!n*=Eufne>koURtHZvP{rV3=_m2Ml9QKsezNg z9S0Os&6H~Uc)fb*W%zDkTLme>(~{8q-qrjSb7ukb^$j-Oci{w=TX4tMgVMgs_=Ed1 z_@2_yUrxEO*0YczXi^D2pgV1B_rV6eCB+ka$uE(|OCYAVUy1q4duemvt9!jlcS&c8 zBJsVu`RY}N47+-gNC0Cwcy<5cBji*JcX!nc#n;M`EZ#XgqC9-mz*yNsbUaz8R8jg^rcy*0tIFNF$ z9CtxL?xe9lrDF3{O*>Os`M>fn>PhL$c$a~0wbt>yx7#ow4D-vme74%ZF!>t|_pme4 zy%)uPJD%vxIq?L##j-9|Ic;LLXk9@(?NH|vj1a#t#Od?(O$# zbAKBCBslOcwP>!y>QcvGyARE_RuQ%_=rJUOzfWNOez@ z_eaUv$L^jFL&bQwnW!tH*n_$}+g{XdZsv892{4ucthcJ{{X~0 zLEA6`NG~C6GepAokUQ=+KuP0*F;;gki7Du=Nmu?P29mgXb z&#(AZ&2RfY+f}@Qh}{g07&gp&mnwex?&oPa$FEu{Q>L|yu4@^}+N%;tr0PpzB{vBq zY0PoNH{>2q%rSsH>z+3=yb?ufPW{{PG9R9}+kuZ-)^%yh7D$?LYFA|`E^jRyyMr5( z#ygK+{=H?~UQG;z%eu%99B>Ch25X)%(^PhIpF?~M_`&0Ehg!Se_GQmhmA8Iq+Yz2ttJ{@OpXZ2G9U@#lc;lxq;7GQzk~c@XDrdU=ER z3I70e@Ob;X;a|mH34YOE4k7U#knu??MuO%PA%t%0km>6jomBoEMRsAa?i(`+Qg=U= zS@laeCXx%AxZs*LD)Gp{Bt{1XSy+Os4+IgAcs2bXUw+Ph54yd$x|>I}l1Zd@W@r$W zEJ-Asa!q=F+EDHI{{X-%o;{EM*8D`DOTA>CjDSsaDdF20 z2Jaz18qzc*wM@!&p)DFS_5T1I-uQ+sHp^GIxxKNpDATOYf$jpf;-tm~#OyowJb_pL z0I)RmIQu$v^vy%T-*!0GirE~c{!+q+hef7jKljWs{8L;N{!o(N;15of9{6&3fb~x=jQt-{HGU{3sdV_Pd#mvwVk-+&B=N|oQwf@Vu$O;!H zwmHoYFJH{TV*HO<@D=|6hcC1XczicvEa%geTIxn!qtCnqNwl}j%Z0+=V0%;^8`FNn zYc$f^2e(^rE&_qMV<+!6K5PTQBc*v5tO8EXO!TFVQ-pognpV@VuXpBqM~D1XdEwVU zV_EGc1bI%!w;5CXBzj{WwU+)V)%;Q7iz9z!2Dy85v0GZkkt7mbjf5zahFHw_1PM;X zI8eii^8T+{68B8`<0Yx}e~0cq34BP?;?O*0;aFBgk!DN%HBFuTb0ENwn2ue5>(y9w z9joO5s_9-G)kd4)T|-m2vA2mMBUMF*P)nGZgz-x7{_Xsx04%YR8yN)SC#}|3wOIYc zdoSf@)|dYPv%ib{FKpf>_!La4=9aO!<2p#ye$}dL zdSo*(u)1jPV^~?<&7?dlE(63eK-xgsIV4xsp+UTWdE~vszntpG^R< zNmpw_a;ggzBw^9RV;xn1u8DB|Cdz4DcUFIq&aw2lxo>CL=ze%b;@frBVz$4Pvhh1) z5YTWZ=(=KlaKVp141Z-M19oPr5q z$RmuI#nz~tRC*fIOLNow1+M&2@g=%yx*nc2Ee6)sNiN>zMU8FJ+~dsi6?4cOj1V!` z*Tf?6KZ>mW8r=Lp)9$qU0eB<(NVL=f8;zvz-;>9g!EK)@{^+klhGm#i_<5K0*zz%X zb{eZ{P)$C)e*@YrK0o*x@+mF%N%oVK+ZYlidTtHJ%io@HUpA-1{{RzDZD%#^lNhqP zm|MlEg#$9M`;yBUDA>j`#~qF<(*FQjam{kcubMbx!#Ft0o-u0PXO{lbgUsRWai6A z^nZlj8$K`iso<{+Mhu5i*EIEEjQ;N4VRbHjytrGQ)#=}|6@;3P?FZr8$hiK*wAHmi z>Ao97Zn^f&&VHa*pO39*TdO{Uhj0?FiG&w(3--OgU0373i7ye+{ML!Gjko|o+CI3) z73F^v?3TmFI<=U;I;6137JHJrJj;TirNGJISnVH5`RUHuAJth-4Mq(umY;Fx-?Cg+ zU+}zic`X5Fiq}wLp?4Pz2bL573C`vPae_z#BQ@N9$68!c_@~8s>+jB+p|n7%NXkCf zFx(i&j1sXOppl+&(!7d`nyOEt{Qm&qk3Tz)G~q@%Usn4i{{Rp3JZIzO%l`nv!@kmf z&poQ>u|cOsv)oNPvOpP?^E!u3;y~V8XKzqQY}cQ7%KYj+B-HPOvfM1UGL?Cq-a-`Z z8<~p}fG{#iz#P}Nfum^M?#?P0c)~7SWvhMs?dRpEW4`!a(Mh7}QV$|qq`k9sO{{JJ z-l`iYtV-vE8%PbDgWE6RMdjVBy2C-`h~ZmlPuGCD1OP}wDaTONM;Wy@-|7gtl8>F2q% zB})>{zG97qzu8{>htj(HcaKu|i>BJ?@hyS5yog(_c#mMnPCtpZkUh^eRVpaZa@g^( z_=?V}v%Z#ohhO6;MW2i(zqGNE%gncy6D#a7$t!>pY=APq$^0h`k4`Hx=Gm_NRexZU zDTYhgSiuRJC5lBqI|#7Hc0t_3oQ#eu&Y+xWtDn%k8JBws6x(lIeJ|?&07K8_y1LVr zYe)#o1G&-59AgB1KU#LZJ;vg9DUC=|j!PVM2LP2mr@d`wEm}U;7d1&VpZOelwx3kE zTL^N&jzaQIK3jkL>ktoWYRDCU47;|1r{z#_&sy3kC8|F& z&hr+UhWB<$%=0Km01iN@t(0w0J1G8D(-)!X!Ut2~ZxCPjUtF`-G}%0@wbQV^~)oJAjGl%02Fc$yr=ql ziueQJPsE)^z?TvvFS_331!OrYaraIJNGsTm+#c2TIe!w6Z(J^V4_3gdP4L^{<&()j_J*L$6uWM= zT(;hR^v#_8Yv%D>ZCYAW(CeYY^w-6y^snsIqFQ`Fxz>D3;rly%Lfb@=D_2|LBqm9u zK`HXv=2L}b`HGR)bT#yUz>kNG@Snoh+Dl$XcG{iN-w6^k79)}r5vz`J$_5S&JuAST zV~DKVtt<9DoOK*6I&|TNT`w>3YqpC-t=#qdUldEBL=#u=&GmvP0LyNxdlm=0B)&i3 z1L<8>pZ2SOBtwPAUX?0mmtu~a$ALCg|Ol|qw?dOiiQgl%e5Ohz&XZpGr_KF$6pHd zFBNIcqwCtGhluTpl~mPj1a}Az_in9970>;1G!O}IE9CD5e#@R3_+{}!L-?a_;k{1g>%*es?8&F<5#Pd}XpF?!6PV{J z6r3&&c+PM!O{)sd=hl89_%Y!B01>MVLe?9GZub`POn>j75x-i{xA?o@V<@)N?1hz*$%_@aIfu{z0mJXBNEABrdJ|cd?{x-b0x4h7GM7K*EG-)-F z^GP3geXi0?!+7d^ql3sCSL^7KHpx;r^y^vA6H-r_Qf-<-j1sg_{ONe__J8>Q0HsMF zkHZ$%5F*M0==!tCddDEfS#K81{{X$o^dRQHqHCK=8~Dsuut#u*g0d>G{D?H3B5!sz zLLCwO#A=#%#BCG9@JHkQD*I2pljU}~w|K&)?W1&`G-r-J}8K#UR)6;rVB&m*a=A&asXv9`zZ;jQZWUB`*%7rIc_BaY8(aiq-~D?4+y zW_J^ghtj{HKOKI=e*-*MsV=d7rRk?rVyzX;&7>maG90N(hzv4<4qBR=JKDuFOiYO5iqjl-s-bKpikE^bg`+>;rw` zO(nFO4PNyvjlx{A+ug@=<`~mpMoZ+}6Bx)@Gm(&b=bll6mDB<4)iK1!Y z$n19eWK)c3^J^UrtIpe>2{(oHyKw1pUfx*7;x}yz$8vjp39q2_{{R5~(f%K_OMPF% z6W&2*6pQwo7K2fM=kO~X zwCcX7_^wub3=#hT>aNHB5}jQUl`S;zu5v)LTz@*>>J#pC`)?zVhD)|GtzRmH4DkDKPW$|`!h z9IiS^gVCD4aPaiACcma?(Mq@<8wlMbP)5sbv!n$C7_4*|FC(TQMY>wS|-J!7V+r#^dO~jDA)060I|mFpE5Y zSn!6grn3u+8_19z4A$)*AE>W&J~H@EeKOj3n(Wv+e7Lf^lXv~{-jpmvmaPfWT@M4( zye&Q0+_NlS?!yVFhNp z5uZba2mbmX*VfmcvpKtsC%W;K)yxEE5`i%QdK1>AW24;%)#cdu&1`La{{SRi#)tdN zaKD(%eQ5{64~91PAKAKxj$?x0aIrdhZT|qE^Y~W3U5|2J9iKJ-0AO1rUSzjo?QzJr zkh6ZV*VGO0O8(q=o)GaY_bGveK_aO??{Ys%&EnJ9jbYzoLPnPCbXH&?IJ^M&Jk-N9yUJY8e zk?5*+j;F(39Pri0h;?M}^yTdH6rNak$Y}x`;dg)t=dF2-hsPfo>T_ug;wz(hHI?7m zqPb8OWs2fIE@*bB3dx4tx#SfcMN)EWLOD;N^-qP~I?;B#R^Qri{{YiA`Pg>go?GbI zuVC<=sPCk~eWqS84mLVVnc$GG2XN{M9mQtqx1%jXsq%wrQ)w3qZ+0fOxH1<%!6bM+ z(BXp*PAk3m{iKVHQ%v!U7Sce{C)lpSx0?_-Ed+@nd&wv1a81MnFHSmtMbbV4K)NQowM#aaUHI}Oz zO>1<3AKkJkU!RT0-BpHgdRJ8$P7=L2H2qAf#xhotx;@w9KgUlMc&FjTjlYI&F7#WQ zd(kGPZ(}^M$23liiz<0VWKyhtU8CCK+Z-PJrWRBR+Y7z@8)?=!=H;f?}8+c zMmJMGhVCbdTb(uC&7mrP*GCY*j$jGHu06p#kFQhVAMD-YEf)7z@P>u#eAhZ6^ zp2kGEDt3kgCmHRugI=t#b!x|&3NyE7uic|O_m8C)%R9%_Y4@Ceh;`or`1fD5gW-f1 z{{UfV5nua66}d8y%OdRizF!E)-A~L280X9X0B28#aeOEEg=gXoS56wI_F-hZv7h&B z&k4klu)*5-1Y@^u4Sf9V2B%8*J$SZisp zg&q(1<*F}^d_SUpX1&aC-V1v<0J*e*z}(6s%y$;+5eGe#_qwG&Qxju)46f)Acy~+d1X6c}%g67>8Lxk`G=o zJu%Hxaf#oRH|eR$@RD1!kBsGxLPpdl>FZwE{{RgC0P$jJS**NMrbI`}nBsB#@vTAsz)&lag3Zgo}IdTW}h}A z{(UhzRMzuHgZ}_!-$uKog_j!6rx0yseMK8o%E?>9T#C zI!zuQEE3>nBLl80-QV_K@}e(&uj&^rqb#v`19$tmQ~c^3T8nyY(l~g(wvBIry5aa| z;V+0E7S)jICeAMl==HBaJ(o8jLBcppdjb)-AqNozA) zNhH#Sk}Io>%X7I<1ucdVgN^|rzIz*pm02rjkK6bUgeOmz6IZUw%f9E%7qiK6Jhu|F zJ4nTgG0QP0B$MiB*d9ej;4uL9;F{yBWA`XiP=%Jq+<&uvk)(KQ;J3tI9Qc|$hO^b$ zJt#(k5VApPhFP#@V22LnPCj9em#M8?QrAuJ@9bA&V|5-U)*G`;-LGY9X>H%%w~_htIeDF=WmGb9LN3(@&;WP? zpdOVph)7mqy29$|_S(rOl{q`HvCaX&(Q?~;0BpH6Ema!#AM;b%AqR-rC_isu{HhQZ1DpW4=Y!2$z1DnLr+C);NnKi7C_dclC)z_x z94bgGTuZstM{%A#iLWmWDvAl7J`aX!%9LiI30*B(Y5eT9C-9erqwrh<#~vuSVKIou zZ*3M#f=QEhWDGNy_J$-jKD6J6QC}^^j-e&(z3hKyirg|xw+;$}CJO9Ai-zfr4;iMS z>py*-_5*^crlp9Qw*0YHUw=}Eh9bCKMqNu%XLoyft^+a`CEX5R&w~4{Zfs7DaB%XQe+PY&D?{t0> z#MPY~eF~K{ihqIPF&(D^rAqeF+FVI(FdO1y8({wc5*?@Y71I}?`V~D@;$Dc3Gf5aH ziq37F)M9JdK4n$+#Wu`saKp88w$8;J52}0v<2&sSL`d!Kb@+D8Aq}NmqCkM-@5dp= zEgyDm`MKsBQ`<>1wdlN@k(;ppRc-BlXTH(&`b&|TRv{a zuDbhEu(z~LL8eBAH&eI}dhz{h$mZ18zby88bOGm)E#h7>#3=o1@xS)W{k85q1FH`Y zcx{s7Rh3MLr2g&*Hx*eN4oQ%M<*3F$EXSa&=~I(>2`2VGURT9_9Cp~6j3vL`#eXZ2 ze%=279-!1s;_Cs>{{YJ;jw2BN0D$b>?T<6@uAgHeI!ykkE_GWcytFAYyhyT2NIB$u zqxqWtf4&xf!Avi0?;y4KlcMGZ#BXz_5+n*vOPh8jTOD?zvk}k}#V1NjpqyGicJ+N; z29Uf*uk!6*ICxv)Z-+i0cy`yteiGE8)bz{M%yUB79^sVotU47uip8Tg+3m}&h2wGK z`qz^tua?`py-kWt_CcLOla#T@TY+ztHxw0Psf$V1tU2)i0E{nV0DQ3OF-3nD{q=;G{{SDXShYqU z#fqL`SmdW~_ShsqiZ9e8a^69f5?+__A z7CEmO>Y8_m^b0EuPJMU4_m)wCZ**;>Hsf*unOXxREW83Y5J%@;hMMG&NXn8G0Fi|_ z;BY8FkIzpR>OUX6J#;l)4^PrQAlg6<%}Y*UGXdMpfmw(>&&WBiy>Il-3TtwUjax~y zxwsuBF$9=r(pdJm{Od?g&r~TU&z*l`ui8uF7sT%tc(1@;7Q8fc`1C7lYr8x3f#FiJ zyF}LVZW~#FP0ClDrwlmB`UXD^$7PrB=D(@wAM7mac?mtTmnzMVqtQ91vIKn$mvG29 zJXeHAfAJo}1uk^0dNt@IS#;_D0PmsZ#!uiXIfrx8wa)|iH^i_@7N@1#Nb;#3XpL4? zC%AP0f;x)x)%b&`Y2cp_cym(I*SxTz-$V7?ZcF3pf{t)Z{F(isKj4}_7(7km`_C5q zFq1{SwA8To5ZzwOX(62k-!*@8D9nlm@JPc9f^uv214;Og;ayrmZ>MV((p(O(w4PMM z{@Kg5v+at0-b<;pYRM}j`PtRK;GJK!u8^+H;ArpmbQT(=w57hDcavYziPe}4w;A>C z_|f&bcQ4ztwm+4vZ}vX@rnCrr{Zrw7w{0Hap^MoSEP4P|hUMB-MjRcX$=mp1y{P4NCA^MlW)M!&N5XCK zSH(q%bAHz7U{{XRSe=z?5w5b08FXVs!(D(b#4|sZJ zklffoAyN05HE-7yZwZj=*yIfN@9kXv)?Lo}bkaFl{14!p)y2k*YE^~`h?E{Zc&??~ zvh7Uo-PhCc?@88|b4N{fID3DFKM(Aib*7CJEO;bw?gl+8yW+<;~f70sgdJ6_xVW0cjF)G`5f-SJw18;lt!!WOM>c;nzfJF zm%zG)^h>=$JoC-ejsg5nYWiFRau}SBI{F&fVy3!@(SzuGv7~$e)OIlYu zSr>lO_}A2OL{yQ-A1^&e*B;eN6(_j8lu_YF!T$gh;UDP!DoFy2ZI%@vPeyM3eRi~$pXe%NM4|jITi1>_LjPh zjqQ!%2ZHJuA(xEhWI~KR`qpk*+|p6JY<@F*68I}|uY60>d?Tu;kQgl?c(6fx+vr4* z#29_k8fDmy)%Rz>O+EqfGsZd%?3S==x?hMATPBhCGex8bBMRf8JGjpo<2BHx+OlUY z?2iukMEYNbwXYQXEAew3{u{rZ)o!hUS#V`#S&<~-SV;r~Y#e>o$gN)#Xj)#G;~D%% zXL>H&5!i~ABTh!ofc#UV z9Sc<&Y|?q<0UP#}EwQoAMl+0NzU=XL!XFKMNwRr7GRtpzuf97n{HbAs_F1IvNXL;k zUWnuXE2b5bFqJmCAD^Re9%2$kdisBbb{hAEHBSU;7Zw`*i@Yl4GT<~!t=JI5DE_G9 zpL*Mvoeo(q<^(AuNCgMd~{FOe`m>fi3LJ9lRl`T67BQ&G8N zs9)HvhOKXzW)yZ zUfMf*r?S0-!FLxA8*spm>UhZaubcEQjDq6UEeFKE=~m1BqHLeETle`G^jC`*PLB=kzt80XOS*!<;RzEAKh+xKXq<7 z&m+IBdsOkzlDcOEu+m4PKDqnGK-=(h&lw)y%Dnc%L#$m#XQh{GsoN1mqj__Ja0h+? zt==UicFf~K?#Fi)xd@oaBYda}xC@L08;JcY3Lgyk((hKh@Rx`5=G1kzZ?gGt10uvo z-6K1IRRA0pYyrp_JXZ!YGmL58FLUg;Lx-V=#QRK3N}M%!)6pLmcV^b2o=9wzb6#jAgXz7KnCZ&bay`zV&cGg_p?Ge}*|KX(xy zy_4&Yn*zQ{GPo06E4?$y>!(7aEP z1e;m5?j(@I9yNLM$CUB2em$W$dpsg?lkA?Jdmk(Q(l!$Kk4c}yIwqjjIxmFaTS%h| zc;{HUwTmVM5&*0qDoFdma7p=;*S`E%_)5KarHqA12h8u+YM50OV z64%U)o6I1Y9a)T}o2Md*Ra{-!MpK5qwFCeM9%}ypc(DR8DjK-@Elj7N8VN}hUgisFj>%13Td zhHD8){{VG=U)SJY_^T(KrD=Lg8J z#LQLvuQD>HV96)8M|y+8`p<^6hK=^?h z(xr6nufCcbSBmuAE*r~?|n z7De1jpmyY_7#xhA{MMA|xY;utZ5Vq#zlqm)4jCiTXPI6Z8fRQ4@#X*v4uj?!k4*Kf zKZi2w7ZY7*wy`|0-2*ZxLR-v00TF;VGdRHX7~{Qm;Ufu2#z*0PAtyK~!L1*CPH)8a zk7eQ;xY|mfQ`8hXz1E0M(Ri)|k1Yw**=y5Ga}@#cv6K=BP_e58H1Zc%B~)_(N0G+EE@6lYjSDX=zW?B9YU*ekl06_R#pD z;_rj{=Yza%)|&M4>C?w&u0jR1+X>JjwbDimQ?jojLW~| zL*jppwND%B0THzq-5SKD6>X!)fECI4yu5Bb&u*2@_-5MJ-d<}rkomTH94jxE#vW!q zU9@LCiwvKjuS=OnL~G=YdmR@+(n7zB66)^cfK8>OiwKN2E9TBY8Va<&qepw^Wk%QM!!-umV6*7ztnj!FTGr00p7fBbhDFm2CVw z@Z-eR&YDb@-WG;>!ejWXtJ{9=y#lI0g8~9o8^zRY)Az(4z zx+mFK&4otY4mL59Aj2pj&<8<~#6zmq%v{Le{{UqF0E|8)_!03!F9vwBZ}_HsQtJf3 zw&>%#BM{DxNa4WP+Pd!Qj*I%Ht? z#XIu^sp)h2I*#PKtHO7qfXaPT^fmLR?6dJ^4~U-%w0&Ot_m_64kj}aJ)RhEh+#HJM zmWMlu^|iT|%LfG4Poio%4dhCKbw)gGkZeDh;MOi$jU%5~SP%|)?Oug8fon4EKnV35 zkIYi$DWig0mdeRm2jOD>D{ z+)jIwSWOkwC+1&T6CI9`H39~6T)&>_!N@ez7N=((rr+MKKwOeWPAC?qc^0h0qgNCz zv5X$Q=o&o|9cj?z7^xnI2lTE5ZwmLQxb!^&`fW2>h{3Gc+v*XwYlxCx)qyp`rRg2$ zV2$|vH=$b$=yTrajCu(7jpDzz zHu5?!!3Jw`S3dqX(XBjTqRpdUV=t2vu;E;cbN)_vt;klY6wVMd+qxUS_1CFDE^%d&qVUk7#WCVYxkT4*h?vvN+gI-iz6(mLgW08}%{&jQgfksdbjrX#8eLX2D>tY%xs=J57 z5J2hBpUe5ykCPAxP=yL}@}7S>r!{K{H`Mu);pP7T_J77-82CKfBi(9#BuzpKiEV_U zC|ga1SflV%xl)WiVYfVYuSvP^_06}$4Qs@DV#sgqbjW74F@5uKYRs*)0iDeo72}%T z5>~T0=LVh6lYT1r2Tk~MsoQC(;tO4GPVp2Mm!3={LvIee77a5Kzm=38Z z&y?q=Jr8l-xaCS$I(d(063=ZX+96{e?1D#ddlAr&uN4aGdSr+e-2=w9tD$^AgY7Iwl@0Rk`${iGLeIpAKe;YP_gru* z&MxiQBzTJXY`kqF*T2yE^rot@I#E3P;&<%vsd#P~HO&Rm^Ge79X!i(#Jr&im_U?bW z`BTQ$JuBJ#FXQcN!we$x8sY^~hBJahXR3|aR!{n7;IF1Dc;a7D9WKYfH;`QE(lj?Q zGBR+=!vua6_xFr`8GJqXp`=Nvcz)qD9ZDgS7RGB_kWL3zx96(1==yDMO{0j5GI5eW zInzzEy}nDxn8!S4C3`ThTh_b)JRzsu_=i=zNFcL7$0~R7KJL@}U4X5YA#9kOZMmuN z!qWQlU(@Yu&+lGQ1ZN+^G30)S72$dx#qSdMM^k(M01VtoraQD|K)WUL1Z-IrMR9_; zJcTNGAT@N*sVK=x#x<#PM#tRW9Cb^1HGLPtx*gL#siVmA`Jo%^>n!S3)HwUhi^vPr zusT=A^7!U&5bDk1uROO?~DZKk+1hudY-IL zB-b?5QrzjLr1~A6gX4>>HbH1EqQ%e4Gt=KYp}&M9&#&X?edbvxq;xJ5+Z^3gPP`8nN#}%|Kz1v*%@7R@mC#`C}GSt9% zH7z>QCb5*VJXW#4{Cq_l0)Ko?8zc|il?%@5{YJj=2ZKCUZkpeL^!t4>8|YD_aU5zJ zc>ASP4s*tOci`7WOu}_yl)4i~2$Y_N*TH`a>HZq{VRx!eWV^3!C%T#_R4d(i_*7Zn zs{}d7&OLKmx(2drd@~-Md1$R`Y+5!cAt~|?lQ`{?3FjTFnsr-uZ1m$zT=D+^fj_hU zwcziMy1&Lx6zjJ7_1?E&_E0FdNF=g}r1KUu@`A?5sseDJ;YLdmO?wB1yj5!+zLs$T zAP$(|0qL5?b8T4kaTuvopr~KnTBUBMrFhfAqs4CUyzUTf+5-g$$U_X|bU5d+ z>0EB7<4CRKnp=C#%X1@yfxz4X9$nj}-QOwY4PARB=lM$#}&O>S9yM2T)dGfE?7`GEsq80Vk8ka}Z|)Y7rtM;%(0 zw5HSUuAiod&fYBjmo%&BWVN=`C1@>VSYB;G{{XZV0bK8ur3PKO;B0P4>MQBJPsJ9R z{e+R(My)ac9h4L}CmS*`^&tIE6#Crszt((ecXc)H^?!-^G4V_GU$OW>cWtjtt7tcg zYGb%a>>lnbnH67g3uX*}k3|j1^fmfnrRiQAySu;D^-Ieusbg#+ADusNzcL{s8Tpir zr212or6kimEFT_Xsopi_=NHp${XfY5e|4YuNA*t;YZ|mrO>ZE&l3YBfq~|UPhEek8 z2jyd*Q=HfAFN*#GcpKt}pQ&m(gq|JKG}3&_n>AB%``2`v7i$KQ+Z=5^nB$#P?G+S# zMgzng1ZgN#jN@f(@A{+h3tD{&;`&Wst>Js1&m>@5#Skn`LZ~ML1FHJ^*XWnWAKClE zT8Pu_{sjCy)?Vr*X)i8rFCOOmP>8E8<7)erTsa>$<-qxH1~Jn{SJ57x7mTv(Lu;OJ zORSn#{{YFJztzvp+bvhZ@mZLzG|8llO_91_(q{m6!P|`SoM#p6+Lyr36?j)z)1cL~ ztF1go`{FMlF&Pm~+%9vwYh!mc+x0$or{+~2Ex^)*q^Es7=&W$+s`^w$R+< zW4Z6P+I>d;DaRN+q46p zPQCM7mFcyy_1RYyN^6#_uiszL`KrNzWj&2)Ou9^0VjV2Z=m+1GHU{?!dHm~wZ7!$W z;PV=FRg>4!#AEF`=ZtN%8z%n%NV;gF1-J+{<(Qs;DuQ$PSF3y~@T^j6dcTP;(Mg;w zm9o0z1u6`#52@it(>?38W^jxZB=tX@_{xp*8ncSh=h%J;z16gfYYi_-xZ7=Dixg3* z>Z7h|EFZT3Eh!|W*>i@Y;I zm-7ns+Qi^DsK;98q|{ZUdQ|1q`2PS$(k}IH0@&!fe26YQOX5Ve8{5kSDy_Ugz77D9 z!xMwI8NtRY(fnm&Z{Wk=>2%{6gG|;TyaEA*3?+?^2OOMmd(__Tv}=7EKT;oS@h?j8_kpC;^ktUf z;>S)A>QM-vHs<;@DCDRhu289Az%`sO)0CPPpA2g{t+$7??+9uZ z^9!w34L&7`3yEY=8s0N~_h(cI^YWP5I2k_{_!GfVT>J^~Ua_Dx%o^UirMx!xQnN<` z!jrN<%EaWCkh80W10Qse&0nyRmm^w(lwQ3K2lkzlMe#+|&x|}iip_Iy*7q!glJkHW z2?wFu^5f}T)~oQ(#{U2gegSx*4<6`}=(l$|{C7|!(nld=Yk1JeuM;?YvBAMS_2gA4 zd&)L9q_;=2{>Ab9Oz>o}Nq^yuSa?gtmXC95sU@6}*#fR!H)&$tBMwX8$`d>QE8_nE zhkq9SCHy>{CGba!wT};Z7}}Os(j+7dfEo!5kdQkKfDYKK>c&^pmj`qD7tlOK70g+N zMU)P^NNjr?bg#AH?P5qo9EM?;vo>&HXN>9II7 z_Au z$K!}_V&y+XEj)LrrabbOE7Rs1TOU>b01ELh2y5OQ_=9ZMzYIK2qxjMk#^I#j#U$y* zaQ5;Pl}-;#6Go_;A4eyQ^t-PiZy|U;c&x-?MZyBzx$BDXTdT|6GGPX#1h!J9a<>T3 zwn*$84E_eCu(9;4a%g&>#kBFcJ+p@VjtzYE9;`Aq_3TY6jkDgayf*{#qXY7gb!aUKEutFME_ zdIi`5a@OpB=c42f_!8!hU}*FAONjpZn)M&C>AHgNwUH$U`-B=-1ry6W=2OoV-(2`= zC(h2aTy7@_E>9knwRPdSQ2d-?i;I<+v0RczJ!-|ik93dIIQ691(b8#q-GdRF^#-?e zZ8k+Jq;rw^))SGUgEkc<3IGj&J61K`y(~9VMGU_pJ1YGF!0k(!1&{yI`dw~hv_^^B zcI4tnnr(d>c0@a z9^80dL#$~?&7w$fGIRE3{^Otd=6`{^ebd^#Yr#JdSH{|Wnr5XOM^s44+`!BGtC+eO z6-mJd_}8kQM;JBD?dn70S`9%xkCbIG2$fm4x#ws(!5`;?UWeoFhxa}ellw}-HkU}5 zf`=q-wsZGmasAH7-+|{9*G{x#z1Aldw>;3c*W{8I;~D4Q6v+URF%)V+GG&P)_;}=2 z+}aUxRg@4OFn(P1=cixtYcY%N5yph4@ecn0T#h-SELS#ML`s**IL>o|K9xurR$M`s z0|kizNIsb7r{z-OuW;PD*^?qN2HrTw=gm}C!hFH9Td(RyJt@5zCVs7`-uQkq;%gf@ zH4B|3UT-26Rgz^0%Za%f&OA{5j%j{Y|66d&hr7sf|Vcb{N-w^)*V-Jcx5-)L}q<$>3 zjdwJb1IoBi21$EaTRnLB9QEOA(WhRVmgGw3j(98L?z5oWK^Chc-rGXhjf;^zxNfEy z4o*E{Y@=fzEqSJ+pxw;uJYp$RPr=RZz7H{yLS z#ad;o?J`Rg9k=!GGwbg`*<}s_$A2H|}6#gUE?evHqIOMr~p{0K=Fi$|wgS0Wg&JHu0 z?>-NBQg0e~(@*g1Dz5sS-My|H;1Mf){QDqg$9w@^o+iI6blfS^Pjl#>82-%P@Q(aE z9}zqosZDbxo>nbeO1yz0y0(!{B=VjtEXtV-4hfNPQ1j2%9w>`X@P~xqwt^Y0V4iEw z4_+6UrjB=lHj>Wm9mZm5Asb~*Kx}L{9Pq~xEBnk+gnA#AvRqAW3Aur`gN@*2c=bIh z^_=Y-HxRtd6p*vT`*NVNF;l_M1dQ|Bt#w5$>}Sl({Wns(hf<4B)0{(bXu?>ZuGU_M z>DY|&39C@)6ZuB=5*(Ax+;{b@TS8K?`mOLY_Kd#q2Za|;(~Yn8hMJoswz+1GNP*~4 z)M4Z5MnL1fK^6I*qFn0w7NcXRz}GgS+GyFD*rLp0-o9$4{GlwHf``g(#_lsw8Y=g( zE^Rxee?|WQ3avgbOW_X@P2!}E*5<hd(dilfPf9;##FBR$5 z{{RB~9S4XF#Bob1%iOoWVs&%fti3H)RIr92&I_nKaXtJ~aa2X~Z;+!LGtTx0@!4)w3qq}9|}@8I4U zw54mmGwqvihzC;LByYcO3|QlC-o%>xZ}8X0FB*7G^7*ffnuWdjE@LmWM1R-t7AYkh>`;T{HGC{u9`wxl3Tdn<{pe##EdFPhfC)ug2RikN*G>bg3`qwtHbV zq=+OkM9;QF$Pz{K4ZIv}IbH{RB%`@$$Z&x>uVYlI^=}DguB% zgdgsc_l|Pi&N#2jUkZNH)^hkl;_t+_D`~4*?Li6x#^H$0=+tr$9=$MWq_1Paj||j| zoLpOH=>6ZsAp~m`yuL%g;zV`8DpV1X^#qFibJo9Ue;R6k=@2A#k_N#*3!_F)I41-* z9mQHlY&ce;dc^&G(!MIg1V3f-j=?VSRA(m`JXh!6!VeqjUN5#cw?DdwhuWFT21A3- zrEeIgx;U|UEhy;E(>wnFjFhqiJejPSdGfJqho(abtd)8|*eNO6`Ej1k(*GY8(OXVqI z&UpicA6n<_d_keksS?V8mKIQTR{7+T?;+tt7Sp<)srC zjDPO~^5Y+cUjG1us9(!*1RftclgAUenbB4y!1-0ToDdE=3eVa_>0{LT&3X}aF4A6S z6LayC!F~pkOHCcWwe>q)=@*he(V}s{XGGIi?8qg$KaD)_(SFB;y7wM)g06(EQsQ|0`?QT_*ch<)?FGqSDkNC z1%WKQoDt^y%%vC{bA|+W2P5LWXT@5Ut6?sjc3W7gIa_#FWO)imE!~Ls`t`3ewL9IV zW2Hr-?|+V3E`_dmG8_Fq*jpInS7~Ad4=z!F%1=@lh6cW<)BHD}c$>i18h)o@i*Kmu zw(-h|le2tF_g}9EoO5=%)$ zZml1ixDd{a9g$cKs>hR&Up@RW_}}7h*)PUAr+_?NV0FI<>MX8Rp%)1(MB!x+vo7!l z4iSWkN8Mn`2W;xPnx=AlE>>&&PyMF+SK^G11Y(+rkclcSopK?H(2g;eySdjk!0%xb!*1ItD`E_Nqk>LR8&$ICJmIRzzL zNQ54nSP@=#7N;Wp^$+jR)((izwmcpCS$uc!B3UJejBa&%I2qL}b=CgawM3JK1MLhZ z8R^W&0Fa}gOwZ9_fxo5E}9QykKRuSe$YR&_l~DqKMedtw9~KTDi*`U zQAwj+GdD$6lzC2j0=yGnhH>iZ_QujcvP9Vp9EurOA60CR(xp>EbdTuvZ8yYL7Rz}( zl#*Jo$hd&5%mjLgVJ^7(;=i9i1N=?#x5JyUb@21VdY6TyM%}kfyOJ0Q@4DHGTef@d zjQaJY=@RKP`U1Ds!w-?eW6+IaOF3wvxBAL5O_qgRABr!}1a0B5{cwwQkes$*}- zuxV@5X*23e4-$BO=L60N$F!qi{Q6hVx_+!3)E2005@`CpHA@I$Yo&GD3bW)9kCYQ$ z6>o4PwkV_hVMPA`zSsl$W}%nOq3X7_+8^1X`$VA2v3H%M{MhH8t#U7R1F-WHa(LZ} z&)L4h=Rg0``R~Jj5B~sa9a?YgU0323r)I$AO>yVC+v!A-r zmpKE12sp>Nt_M544uq^|Y4`U!edKzUm2viKt9Mx;W&6s@-<}S61au>VR8gM@(`;oQ zcy0Yb?0&VBnr4?i%kY=MZ@AJBXhw2 z0Q1e;*ZZyfT{_piX$>Y>bz&+3!e#1m2)x!Pbl;X`xIP{6P zCaHZjuu^~3Mdks4yMQ{L*!AuOWI+nb0_4e#qwcWX(+%Gq^=+$ai7Z{)>E?LOojj8% zO~=iT%4Bo=I1EqJgY>CZ-b6^?xRqjvF*#+!Wcm@_vyJVkEu)6QhIL1VPZbR>pUXKyESSGZOQF!B&<(4)p9GK`E9G}+*nzdF-W;Yc2AD2HF zKVn;rKH2rpgwsi=qUEI0?xYPmuUy*9f7W_zl!l$wP@moo+_(xXzO_SVdT9un@JUlbP;N-_595Cc^jxc=)=D%t_FMJR9 zckzQyX*DZ1)4WL>W;k_QVxDTJRgPH7;&}IVen8IB4SE#wC{26uF`gPx*`E;nEBM=? zc(zFN4;XCH^@&JSpQXLabm#l6?VS2%zGC>7@Uz7q40Yk*?-bey<(*mJkuvu8Q7Pr& zbN7JbDt>kDF@`!z2~MW4W@@8bpHu$R{u-Xg!Fo*Ll5eqUD*j?N)-VxHZ%tWrtWYhVQ@=y}C+#|=7CyV$wSNqL^1`xtmf zS@BQBdl+;i-+N;q)B+VP=0#{fe;WS)x8=U&9Zon0n(F@mV|{7#4;@?hg4$^a(7qYk_ybh8jB6Ui*BZLatTw_TW;U2B z{{THAUcP|mn(4kM{1MQ;Dck9K>^>&G($0@=5^GzFMY;P)NSkX#Fmy3};0R@GjNk*! zW$hyU>)Dv7G=3_y{{U~jp61wp$M5<;QJ>@ai1qJL+6lD%P85@9k|||@v7Na`WaHcA zuSvxyT?!{embb9&cw(JNz&mlz;a%hjaLBVr!A{T!ALCpuzNWmVG5-Jw4y~u{S>b6R z^mXU+9V#7WdE$0ZM9AGhIC1pP2D+(KY{|tlPG5?*Gx%n21~t@{T1CXIEYL)vF*~rq z6UexE5DtluzMzgP$1j(3t+4!=&-oSTQK+M9Ll7Ke0At*Au4u&2 zMd!&7DipR4a5=&DHR*qC*=%O=_r1@gFKs27AqClB>VR(;>5PDVf5N-HXTxn{rr6y& zEJdy2E%OYPWDE!$LiS$%weweZx%at#VTz#@$t@2Vn#%GrK*XxPho|9PM5q7^tTV_2 z9-hYntxvi=>O5awKNM3qZO*wvk?Jd_xE9`HLp&iZfxaa8hrn}>?B`jn3$wkv zgXLn&gNGk7_UXqJ&bF0q(8kDQ55l(~f_t#fLG4k_md16n+_tj)2UK$C$|da98|0V-?70x)r{gzDQn0ZZdZ^54lmt9f_=ERdhT|U1~~yxr4Ww zvC!xqBfZhQOCWh>xv-irq`EQ)nNCy<*e8ylbf|vJ@`rilG9wTOely1dBR{8F!d2za z&Q+>PDiMdzso8jg#J(T#V=k76lU<0(RxD$6JutqcdJ)pRw^6Z^&W6S~O3Nc*Hiimz zpH(@)2a}u+tuJpQB~GGtmW~D8V_vcF z%SX{IJSXC>40w{=C4Uh~ZEY9uozba~NX7GpQZl&CcO*#pzJUb%k%>Mu@O$1T96VQNV-U^Yy8HpnW zRySWURU056B;vVi)~KS}V}niD`>xK?8*3|D2n!@pEOCI%xwghOjGx0FTKXSZ_z$Sw zLT`Kzt6J;U<+4dD8Nuocrs&DzjAFc5Rg3C&$7@*m8^*s5ydCiaN&d_6cAmEp#Kog+ zt3DW4-a^c!#7I@h-@XXl`)?=K2liY2;8aTzD-;zH2v0FOR@R$ z;?LNg?VQWtPZB9or(_zU2j{2)cs7u|vdG8W*WOV>mi^;xL*7EiXy=2pXWu=mf3DPz zQhk(Or{o8V{{UvcjNTZPgJ@D}_QEeW&(jLrFMO$tktX4ez~m5m@IPYwqXeX~hGxiD zje*WM1Tn`a*0-mcO=$HdbW>jG_yMSCcbZJg2CW3PGB-uKhDZ+`f7U1*eQWv+{>p*X zhO25A$$_?bm9m4C$Y4hvfb(5(&5~Ng=XiJA{(ED!l0aS@N~5Vp4hPh(YxfJp{{R7g z4tTL;n^^Fcp3+Qm)4?CvXBiwHGA*QI+O204`l4M4ABQF;2OgFBjpBdU+u@w@JlamP zsc4I|lCLCp(Jn>?1mL~_?i(hxhGR{GrzN@hXh9sF)%ItI{{Um38`z!R9o4kWaYB=~ zGFZ$=8T+s2DbH?3YgprGzqBTr-5(fkHR?Vp_&xC-!ItLt#NHgfu(ej&)0oy+LFzK` zGlkEq1s=7uYQ^Z8H$1^7k|UVhsrERje#p+gWLR|m@{mX7Dx6Vqk$N3w?Jy?9$~!RU z^Q^NQ0m!W71Z=F%vJGy@{{Yq(g!}x5nK#fCNt`X`c!*14hZ{0!oRhT$Bzd48O)zSSOd^Gsc@NeQZ<(;2^{Bx%2 z^Rs!cX>eOh)FauEvF&d*o$PsB9zfhU#($lOZmqB7wu%l$XUQeRw1wle_IIT$EL2|dGNznwASO<_JrwQWW;UKLYOy;V>;0HR<39e@}W zs%;oNBkq6y)cPaN5s|fv0;FVg$UQOZS=SyJ@d&zmb+NjUh86a6f}^O%Adg-_739gQ z*!7aq_b)x9vrz93F?SUU;nCu4%r-kvNPp zXXZc#4}bpvUuxz=lP%M3%E_E0WDv@uo_8GNp1-9^E0LYN_c6VZic46QW!ti4Vh#!9 zGL8@FT${~(&9oQ|Ap{^GG7NLLoE8VL6-f}Hcm5Iao!5f(p?wBsg|L?}t9-IWo$6#C&NB2(U~NbZ*6cd5H!$A}#PMwgH|mljV1h?c*pF)7 zoq25|Ii<1jJW#=%(TuS<1Du@o!0XLh@t1`>LEz11T@zD?NbsPNIgpT(M8}5*voA&i zkbes3PUwi)yBXW;yui++j(8-GyV&{>SQ6aG(hx@WkT53!j~#!>t5*{@)u8h?Zp==g z;0$MV0aQQo|?jWmb!DBXGtS zo<Gx)=;Fl1z4VO>JB|c7n2m4wb8(M zyTQI0@y3I3;g1(-@Zah8Uol6>D#IX26oBqWAHvImE4I3}jkercsUh&jDGwWmbU5S@ z+cgSKFMAE8Ez$VV@zeHT@jt@1ZEHsJb-xT1EN?c0J*Wev+91FllPKDMj49;)zsg#2 zV3=->P-9@=Fe>BjkT@!R>(Hf{&Oa8W6T>F`)AG;uG4TXG81aU&d8k7a>20OUbQXBT zh@!}nah!m~Bse{~jMvxNU%ZstH$I6bQ z9V+n{h}70P5~)coQP_UcekAcWm#BC<#$OL$x0Zb*TgK)%ix@@djI1(%Kp~@MOydej z$4pnQ_>)c1JSp)&=uAG-qg`mJcpxM$>u48t2V88<)o-VIbn3xfPB%|y*_b#xCl-DJ z_<^MU+SDeVA0d-ayDb?10G4OZa#NmuQ;|;BFD5_TQ z(mL0D8EIw_-siJfT0mRbO#N9JTx7YNq?5%T1xsEAfyo&H!hFO3ylCmofasVN@K9w)RkJ?X0@ZW?Z_)YN^ z$@M=CTTka&&Agedf|APvt_f(Bj^ts=aJa?|XyaP?E1{;367fgEUx*rZojt|>0EX^k zjZyI;JQAk>fO8zA<3H@z*E%1@PlR@w{*8KAa^^=Meeu`=0irHR zHqc6|*)iTG(nARWPbJ`A4r6tr}9j(f2rRSksOBA2CgA;UZ$BZd~IzBzm7pyEf4R z#K@~6F%7hQ%n$d!t#nEj+*eK7$2#`Ow5&{{1hzKikJOIUYD6ge6lFWct+js#?ewIz z*wIrU1Oj5H$Ns`fSZn}Yjuu~(e^!H;3r>r89uO^nhiFyxPy z8%BBp4?mT1q;`9b?esLsCQ{KJCIDj?`GDcKb`KLYX)w2f-Gm_I33y%JxbO6?%Gz*F?3zs? z9NFl94fUO8;lGH7jBKH_)Nj@|h{tda&n`jph|2uP?SKp6u%iHTT)bCbbcXr8&M{cZ zoMohuJnd?Jj{G z9`(;Gs*IMGM#~vBx&04#WA=f!zS0se7DB!vieHe)ZRG`I>+_ApPIJe~Ghff&g*;v2 zKLy;2uL4Y4Z;W&Z&{P3UMx7Pss*PT3q&)t{kbu7g-sz0I_bdP}^KDYx$yqCgy zq{kvA#45~s;y27~@3Dn`MtC#!u$#+E4~SYopCo4Tz;q*=wX>7Q`>e;%S1nwww076= zG>#?@srrlJ&w-vT(bU{&lEJN85x99xj_B!)MJ+CMw_^6iBevGyxwB)8M>;eoKA3O@bLeZ; zH6PmN!Wu4~mY)`4)9&r2h!m0+U*3*~{T_U_MmgGer%#)tZbQ>n&ma3r#*Gjgm9oSz z$OpDF+t#`bKjN3{qwyx?;_$|)cj3#xPs*_wp!X&Q`GWhX0M`D-U&{XDHC8>&GUgav zh@Kp6KX?rANCzIg_7&+f{4~~e8&<#Yb?uLe?4$e7r%(I!U-qq^4!?$Jw3g#l6Fh(W zD#4^+Av_lO)0NzDj05>#R{YlUSWEUxlrg#7JgH`HzuuW|m@E$U91uR$_R_9S8n}Nw$}|RIGWIi9cmu1^CX{ z?(e)I_E{e#Z*4!(WncyX4x2+BK$wGpjZ zY|Pvs`X8L0IQ@z>-8Od~57y(nVT@YpOXW@12NOm%@$RJ8=xHV08sX$oBYgSBTii2$ z7i{$Pu9|rI$$QS?l^Cs$#6K5&Eb-@onZMyH(5|%jmp{HS`L^UQ@d;wwp?iY+c>HVj zy8dfd%n`>V#iIbTs)RV|02#`Tg|GqbT` z>-Ko~UE>2S$M#%WP3z^2-k<)DAjltm2gv;N4i0@QrDjm-gp`lSHi(M)>&5>7vX74R zb(;6Xazn3KMsff?Ma~aEQNBiRTr&LyZyZ%8)T_S7$R%)bT~@#0-AluF&8O?ulWErq zakAz%i=RNv^0)j1n%XkH%zXAaF^uN4B(-Jj??Z`dLn6Pshlu4MdUX{MiM={ha*Z=u z4R%FfWsO*l`f2zeJTk0zd;Nx@eABYvbI#GMd#5UH*>}`G`{3@`!VJ*}bQaKxpvMVtl`t`}~ zdXZnAo+S8J@ebPJN3pSMyJhKc$X9XivM;=G*tP-fn(e26r1qJNu`}wA)2mM#cvkqC zp7OxkNTqhYvhvJ0B21PnJ7T{qG_4onE{w}Cu)d71SzbNTf52Cwk6;B4EnOrfiSCcq z+gI@Zpoh4}9R7{NC`t#V-Dg3EZSFCmiOeL$B$#wt%JNb4QLN zLyy4LN{UYQ2UVEu;C~5Du$is33H+xc_p?0UD;_}kxZ{(KYnt&Euc%rh9W~twjORZp z=ijAlsU{^-OJ-cS_+{awj#$%8zer?KQ*F>^^;IZT01t1QBfWgfto%&)jwkYTZDC?X zf12X$F&JM+l>PJDCkj33`vkh7`&m8Dd$#y(ruja8t8;T<6nR+VWDMs#1r9jJbDn=1 z@EE=*_|`cUh3&o7z~spCDeKhrz&}7MC5M&nSG7mKTl_t;xI~e)S*@i^aU@%p1F7BE z52jD0a~>1;nc}=oMMi#R{+IBaS>0Zg*LTHo{(MlhK}2UCF%6sq zBxK}gn)J&JLG=VzaCaWVALQ3GAsDvTUCruaDosW*du(`yqv5#Wym$_^VR?V%Wni^e z3mj*z=5w^*^*#FzE9fb%NRV1WqUv_Z*h1}$dbz>C{c4+5vLo9|sp7VO4g6BGTisp7 zY=MX_pJ-U6Xxoe{Z6G&spT`yL@!6%UOt%gsmQTFfa0jxkI##i1J+(5GSsp)i@Y}@( z>Tj@VlEr3?hsl(UbTZQSKb4;nT1}zdK8dKoHI4PA5$&#`dwYZ=WNwO8R~a1i zujx0(-w8Y?@haBp$HnGb%S*V#Zh>P)+B$8ON0^KcLaYd_>0_F9anMrO{#{yWB69Gv zM*Rs!WjuP3iv5-Nf&Tym;Inw=yZBw=$4SQKOC4$-+;z#XP8hmU z(oD)yX#9=R{vddZN`Tp!mh#yD0Lwdt-;Z1{A%Q;R{VU%7BL2=l7d{+b%Vpxv2wp64 zuJdRAEpzBNCGHM*xsu4K@?rT07%>%_V)qD+^1Sc*?r-@nx0)1lC; zC0Cs`+#n!siGwQV*974E92(X%y9v!1(Mf5k+(oBY#U}fyBZLCL^CN}h-<~VD@F#|C z?)-0}#TfF!T`cUxoCUWAT%2=)R1d^fj&&lv)@w$ZoRoW?Wq9`RYdY1+*ab^V4Uvgi zHo0X%yaU&-ABPyOKK539va*D0lF}#{#6gKTKJDq&t`M>J^ywy&{=ctNkeDuag={F{ zG0L3rySFFatzF4=b2x$s+DYX`3=|eG&$lc{9C2CAb3L>QvQg9e{=EzzwW>~GXp%xs zS_U5{r%pK@hNXK{p50`35(F)f&6fwWVB?R+wR9?U(Z?xEs~nDZEuF+KDMG+N0R}KS zGoGg%go_0Q>36?1=RqMglvt8ZYCTz>Ol3l`JDXZ!uh_cZwK~zBy6(4th>-y?XX(5^?g84l4h~_MP#mDhAPj4pZ z-trP5ja=^zerZ|qSo-sxIOegORqSsXv6{PIujXv&$Su~}%wsqyvE&|^;C)AGZk{8O z1t)YCa$#_O_D{cHeLGh(dYx3NB^7V$=5B>y2rN9&YWs;_`DllNX@TZ~Gqh${feJem zJy`pX=fz8Uof4?+{eMjP_x6Z}IQ1V6s(^OdTrUSF<&iPRVb315FUOlZ>zVvJKbBAH-Rikn9LB}G z=O;WGuM~iJG%X=0>fKf5M?@L4(Txj`dL{M2RzFgYx&Ps*NLTj22<&Ijhi{ z8Ve(kZ~*tJ>P|MW=d~+bcd>ri=C`K%DM+pMty}YvVhJ ztZlqcr|EjOPnTzYvO;CXNHE+62u=n{;II|vE+LXFjyI{sQHDoe52X`>Ye)DHrhcb< z3Gmm&UxRwZzlkUDm5!#E5DPi&q>=V8$yJs|EElKGxcdtD8^Qko8U7&n7)K73sC}x) zGLvh2x_I`E$i(lA6n_<0pcAV{W>4#9aqw?hL{) zbpQvtzY``0Q`@zEXZR=fobYb9<=S|`CGh3+#4h9e>nCD4#LL8LIuA8dap_(~Os=GR z{Ep>_gFjBE_(9-57YQ{l0_x{{qahMcwMI6a;G<$xua>NCqn;>lY%g}J;gaIsMhBkj zA|9uVW3_QggqPYjjH9wgsd$6nx5OO|;NHHQ{igur%VxS4YpBz2DJ95R#FECnxVXQ~z3-1x$4b+?7!g<3hC zN_k2M2Pc(W_6P9qUtoBr;0}$c>7!QgZn+HBvQrX*2RZB94ukTh`!S}O z&1?qQqLb* z<2?uWU3p%F4tXAxtZ|iN_JPw!WUP zK9%~Tx+E`l1In=hBa|gdi8xW;spp=ex|U}=MOw|(?BY{{ zO6i}8KVzG+AIiT=wBLaK18q9hwGW5dJ6#x)%`x2HG8s0F#ZQ_tGn^a@j@7HpDO}3z z|JL=-5`NK^T6Dfug_X3i$H5AVjC)tYugJYI}aIo>2C6A=z)(w%-Q_Kb6zphCAzYZ zTHVPL0CEtVjC&fYF;@v_eBt7KBUabtdwosT01g8O4WGn{<8|FeE4w)6Z=^x#*f zDrnQ(H}+-K%0{ReRw>2?4hR9W&2rAF>}d#_+EPBb0>h|6ib%%Ypp1SZt>~vwNmt3# zu2Fk=vBf4o@zGq)W{wQ6eeid~_vz*cVvM(Ni{J3CMnSLnW@Xjl*ak_G$#4MVpOEDJ z0LkrCs>qI0$sza~r$XD5uEpmh00+OddKQTM?M9>}AuMP<@GF%in{@ zJmBVsH`IK#>Ur{fIMgSVMvr|ZrIaTRJZ#yNkKjz40qQ`>^{;goI+FyBOB;2K1_DPp z3O%q_f$dVK9;395M@rDO?JX7|ahS)aQ}yeb>mt3D;fY%nF{#GooM4|pkTdUFD?N;) zrPYs$Th#$>kdyS!6sF=)*4AZF-5neB%~~@q#S+UW&Iu=`M?Xrey1a1=y`AAKZ~=`@ zN%Y#(HY+n$R%pz2$jT*lAG~w5BONkG2Z8jic5PlLiFs`AQda)}r&s>~8ckoRX?I1> zIY#A;x8ay~#zDb6<2mhE&8_KYEjQX!o~7-j823C;W!#xZT@6F#$}o%w>T}aQvTBNI zT0tNs&)(E?o>=?q3d5w{@1rJHm%{?E2$xkZR~F)-?s)f znnYHSMstNwK=dZPtiH787tYUHC&I^?Q|qz$<*7`nBxycq*#7qo)?WC>V0aZZ=%4K@ z!XhP7R7L;*pdEgsSFamAKd-p@{5(1@>*R9xLS((Ro6T!w37yfOpCdcAk;r_09qUR_ z5MPocA>COVugs?#cIW%K(j_6~j3Bl(_0K$Xb6ks8soLUVOB5?F?-;r;8wt);hCXJ`VbZrA zBxGxu=6i*Zki~!;XN-0LchAZy!<=*J5fz!@0(rnu&sHKgylI!!iZjfA5U{i7kr@FNk}cBpN( zDGV2E6L41?mi0Bx*K^dT9=$&UK*n7{hCAUPo=#;??vG>AritVds$?0X=-sxSx%V|p z-lJ+i*WulL$DI6BmLoQqV;+3SqiG2Tf~~>~dvwpWL*w|2+Tuwb84L-u58)X+eQVsx z>A}ItpN{cXI#a~*$7FeO+S{uVV+Gr(KsZrdKlk~+?QRqsPnTZ2i zTnxRGc}6PVC?UTJ*R`>dASBLKM>!qT`+HYCIA2z8R#txL6KT3U*2V*-D1J7`q(;E| zk;{D#71G*Thrk)9Ix%_?8trOR(!49+>2@vkwe-#h&XLFzGj<`kBOaam*JW*Hpbz11 z=Tr4)l(8}}^*;c3KHlXT&2MCh`7Xtf`{79-dtm$5eyKIZui2Dsk$&mLIMRaApp|Jo zZh36}2k@(m+Fo5mbN;SC3Bc=yE7$64vHK0gQa9Q&yU~Yn?^o=j*q?1K=bK-6G8Bzv z(r;SmF~N>Vc1WYSB~H>lg1tFy`Qwr~82(ifpo&u(h?GHbVuTK>1gA+;fb#P6tl4=hes2i`?p^PNP?6!%}=4 z@%^lgF61gPuQ)_3ve$&C=&jVYc2>dr-o>>KP zZl5U~2t1Ma8se{+!e3|aHH9}F+4E1DP*tzjvfb|Uf3-NhP}Q~&^qdk;ZgJxSf@taZktM}tM9c#VKJ8db~%f4dVgRs-9U z?s{Uq`akDsjdjc>xYrt?I*pG=rzEpE>dB;am!BN+f-<#21< zt}IsN*5X-AuD^BI0>kK7AIhal-3F%-njh^c;!8jy@l~{XwXyjmct_bM01ipODEaDg zI{H)ISAt0$=EGb77=%nXQ;@7Nj01uP1J^YZpODjwM^~s^d;{^iKB)`;0B344u3O9h z070HGMnEZzvJ>5aBDv+&?i4z`v~uN{m`IBtJGzWuV15)rDeoo@ZETMu@t1;pBV@48 zqWGda$3-B=aHxfdB%C@04mtoaob@&7(O*SrA(&~FjL(s~$&9eiUZWWGtDUfGD))<@3+VFdYj~PHyl@8d8Zj)WrN`aC zT!1}u$F*fC>TaA`)bK=(-ttjx97Mz!S#?mUiix$9mPeBN~b0Es8}{{X`t)jXY4 z{u=xbS@E}m?S2znM{DBSaL&Us2o=&ucYst6Fqq~DGS175qZt_HyeGsm_|xJYly_IR z8}z30tgZLP@LPSA@)bXN>{*D$PjjB5it?wMy0(Lyqvv=301V2nlY1!hRJFZzJ)Xnj z{{V)3F$C{#b9H9w0Z3=J;re5r;0!s*8%M2tbK<`T>J}E(MqANu0F3QP=Vxh%IC8FX zcASn0z{UnEw+~isUhP)>PB%iDS|jbNpW2g0aQ60*o0+!n54U7;an1b*zd+vOQ zBvgjk2p@Tvk`xZ-4At`u){xLjf>+-pd>NB;i;^-hx$W!FS7b8>YbhB;LA#%0czefQ z6}Pr5v$Q~k03V!|>A@I43FtxNxy^igV{vz;pE<~rkC9#Q8P5TT9P!)UspIPOl46xA zeHD+;4HLzl5Y<>U)}i)dHt&qE4!Jnw4CDjGM-}mYiL9lztqsDV7b}G_ugXt+Dz-xZ z0A96(vl^>$dnj&?)sG7JgTNYa4%*}sc}mUlxRKRI40b6jdFy~{<*xyF?maJ1^RMn~ zG|d{=D>Qc6N7`>?xCi%f$gnd8bIV`=#1Od2=ZE`7b7>{N^|9AV6xzFy`f;FL=$FeB zw))MnS>s2FB6iM0{oj?2es0+UzcDW@JRfiICVz-`YBfkTDI%HfZWiM~Lj>51$X_wM z$eSuT2jwR?8~{rbXKmTlPYp_OzcXLg$o+7+iS$^6msjfuKXnUyw2?d;wy<|N8!%TH2d#I~%x@IxE?3y(#pU$r&8l*8k9%AE&#Qbr;#(gZUFsTb zubmTVuJO-rJZ)`xtCfOao@)BNerQO+q6;z2R>)|$yW`q50OFVHI644S}iJY zzpJyA+mZUwuH9%iwwD?vvnsq-3$de(*iHWO7*CjvI;$xJ9z}ec;m_KOLOOi1 z+uK~qZQ|d$R*EU4MNmsV-L2P*Dua{vG*gYy10@NLiS zf#Pyw*CgzB+s|9x}Ahv~707ThldrhK4z`=XMq#0vGKv zNW?zr7t1AnZR|c=WQ&a8<*ZX{y3`>$oUf8iC%R9k>UvhC<4rq7RwS~oRwOyk;@Ph+ z)I3Gu4SjDd;F?4h&hbd)hQLtm;e#F+=NZRCn&?@CFJ{ro=v#AD{{U!@hw$8sO-ECP z-b~{XI9z^suarI@d@j;1by$3<C2+f`=-O< z2Zc3wJnc66E3AyF$08DY4oL&^HTkPF(R@6#GHKw?s7 zwwKo_sZJ%-B9Ivw5lDGtLA-#tRSFLPb6(B{ov9_<`i!P3F!el|Q12r&&Z1tq=Qzho zb-+{%k{0BgHyO$4`2PS(^x8@~{{UYz=;$SFf3KNBS%Bg^qcCC6oMdv``gcE-K3j7w z$}P$22bci>5s#Pf9+iTsmWi2S(wo!z`E??&@|rfc5Ap>-Uk5S|Y>q$K8KiYX2~FY{ zHw~C$$@KL;gr1e1NlEnzbS$p-{chGA>U&l$b~>oW z-uLzM?sKnwad~kmW8|tzS$2%OsNfUdpyNDawKGYzB4xGR=G;cP^AAEg;ZJ@#F{kEZ zT1mUEzjx!Q*lC6CB#c{z^Ox@Bj?g`ir}C?|s>v^u?92VopmX^8`qw2_a_Q5z_4=I@ zGM7Gc9mH`b952v+N~sOcB3Uq-TgxN+pmZnH)+*<%OHEt;yv<}_k|bz%08!AMxF;Qb zeJZV%T@b0k+TD7Mt;s(A)IE1OF>`#D{;rNk#n)DeaTvY5x3!eQzw0JM>P`p@bM0TJ zKLAdHtayJ$(&zA{o#Me~G_z_m+Q%>Ne9);2GYpqy$;UVXx^S7bI!V)UyY>G7fPPh+ z;HNsSuT782C~h>_EtV)|id(R9?Htj#+n&Hae-q7q^H1O}0$znnuM6nxxgBg{ zOErUcv-|%5;2umUNol9u{{RI1F48nzd&Dg7M9c8fJX-}8WbG%Bvuw?*)UK zm!3dM#(~@rI+8GYXZhAL&#NWN8(g=5rrq7_escKl;m^PyhHGb{L#@80dJBE7{{U3J zODrhsGTRM_oZ%ydNEkbuvv7Y!=J2kEdkaB5oKwcxa+1LmicdHsGS>x6UNWMzskUoa(o*$5F!X=f-{O~uApXjJI8^B!aW z0FD8JU!cPft1Of6E(Sh8HlO$j=B8QQX8!=-3+gcHe2ruKCd%X;Z^Y5cx%s?{Tru{{ zjC1v`tg&+;!r$ys*`W{kbrlaat=y%Af#F(z?7#4;(`(*Ecc`-Pe93&JC+LE`)ug&n zm73m9UC79PLTedj6`$H=tTcNZ-h=SV;rE7|OQPsjmTm_QV-h&@!#6e2o163)Ww#?A z^U;U%2CUOiQ@-%V}+9DX)lzhC7{>R#;Iv$dz%vf;U0p9r^PH{tkbE2KhbIlQs zDOx7v2T&Ua-CH2z(9~0Sb4z=GW|HM%ljR#O9YPV%V0OnRxvZv=+_-D8Qr6#60oL~7 zIneTqkeJvU_xYJf?Z6dq_)f+jGAnD_*qOSwo4o!7vx;hqwUB#N+>UW1vH9_{Cf*oe z9C6KQTIhdei1~L?PR*Q|TX!V&zyM~m_EJw{Uuzb()aQ~BBW6c-$UG6ZujlVH)<{hJ@POluN&%{W-=VTfyZ zNF5KK{wYh@zQC$3;~4-P@(;hQRh}(sBeZGHDLn*o#KiqF%UMdBG*d#AO!xl)zKvxY zon#z;Vc&n>F#iDY+K*zxoge?u{L23Tg>B=n4ZzxS`4OxxWNS!vJgUI&0#FVHLEZPT zy$)*jr{e2PRYbQ|qTcpx!tO+K`{#3km?uEhGsA%ablx#=vy<_w=h#i&u(A znTe@d50*!0{yOnR%uz$C>Jn-QkfZ@_*;phg!lLa_g#esj0(<7X?XDEtk2P4hI4#C7 zI}YUiX!b5!v&o%y-sfYeYDZPLmg`ZFOqnEnp>v#It~wm?fyHsbCE$(^1OwOq0If&W z5?hrh&PPdVF0OZ_jDiSmoI@k?uw>;`xXJb76^jdBJ;ErJ)%O?AUzlf4d`mg|qxb9-obNC*fy_Ed`p(qu<)CW*k$?=icHhv^;_m&} zoPUITWq0A97TarA*AJ{++oLFpREXR{?;zMzrbZYKyCCI3C%Mk$*TNqfT4@k#mtG&4 zZLWY2yDNRxI9HNCnU{Z69Z2L>6&h;VqiUGx#ryGJ*O~S8zs1jndMvM|d_~gqKORRM zYbC^1&x^=zB~O`Qxh>_ifE6BF;XuY%3ixjFJsU%g*TbLKlcO0@OPxo{Xv8K@lWVB} z1VQsC>zw?gk)A3zDCsIvc3m{L<)_Hnh9>^{Uvczhy!sB8tm+q@8-Eq4u#DCvV8C7a2ToDl1Y@ zmG9TCr%dq@lC!z%cEe1u(~@ggrPiL=p^`u(8_mqhVlu`hJOHb=hByNQtyqsr*UpHs zPp3%@t-QoY9EbiK1dR0oLBKsZ=QWJ+Ds!{dTe&KaGchRE@-7QQd2D+8BYyBoUZ9c_#RoC~d(=nILlsm#cw1z(r1On+Z~wbMK-W@hTM+3$QYrFe%?xz>{U-YGQ!z0{JMCSFXW@xNmuU-~xET45QgdGz+~4@xQ5Gki10hftkgbEs2N*c-f$h5HEjq+NA&m5x{BQLW!89jR%_@e9n5}5YGJ>J#jf`=*rZb>5@&1)VS zs@0_}3|)FBuDS6n7jd?oarT&7aUclF@$&E5M^E>;^shS7ZTvvrB;XwUrT38A8>r8r z?^{QQr5$6rlc`Uk@A`j=FLYa5r?|CQP2SSpV{997fKf=nzyyGL^yywJB-(^KE$(b$ zx{5dX8RTXF^aDBIcH@e|uMMh_QmL)BG=?EXG@Cw|Pl-3pq~0xnw_8huDH~n3V+`O1 z{n_frpuyS)Y}dG>y{=K4N}BO=av>PbNa_+kxKS zE)P(9eR!^gmZ&Q!omiaB)oYq5dL+Li-ZgDoQq)y-Z9$| zucxkm3XNq{Mh6>a0ZGq2bBy*GHU4>6pnX8 z9PPH|A%VGm_s#PR_Qz6BUV^Mm8pQ#7iGw?S@tIq6nO=p@sp`Jm*Ch1N^zhMbCo6xS zr(*}ibE&%i&(s(}7JRwg*efig0*VK&4nXwlSoc?tEyG>D8s6eIjnQ{D@<==!WC4J8 zcOwAR)1sQY+|Ln;YecNq-~Rv%X=!Z)4IC-}gE&&no!Rs~@_Td!w;TH%tRO^!R-RZL zPSA?4w*i>-KYttnoxb&rXsGo$=vQ*Pwx)|qZxl}?Tec_;h0iAk9AmKLb5!ReZq2zX z7E9wBeSq)y*Oxc0hpR?6SMU9Lng!I!HtZ^^M&KTB4|D2jgS3X-Olr6>qhZcE9PV#n zQk!RN9rf?|qo%QTRWYVV%>8go}CZ1=E?vo2LG30eBL9dS%4Px-z%Vnh9-rZ^BNpCVk zEyQuI%*X&xLc3&wMmY89UL9;zSX62&L*Vh0s7g)BZ=13EUeEoFYXz>St3|0?1W?y8 zBJGjL2azAko_~jfo-6Y4elL7={{Vz`+V4!#8&80tirV4XX8rViRtWcHiX3pt0LTHl zuZ!*MZKtX^3 z$B#5`0@&H>UMFbv)^_skrH+U?NnPu$Y)%zb}Xcrl?d8FP4n#CKo)6?{?o;*3>j~HlLrPBWZ!qK|dw6knV`a9c4C7tB)PIj4L zws~WW`ngP%UfYQ0wNcizKM*#H=lHVKEd0>;OAFaxyI6oMvJyHLTxD~F>B+1kn!;D) zcd@*(*Bf)K&WWw0`{QX|-0{tM4v(tqJ`(V%YiXz7UU+KZ8sgUa>f`Mfjj_tgB!z%w zi2)3Z2`oB+#$WcLQRZvzyZsF7WzvgDf0O%F3{J1+BLUch+PP%$zLO=ik80q}2_i^N1c0wp z!Tv6o`ihxn7-a3vuj(h4*1eor(MlLHvL*=i@AdpEgI|c&R~{!G6aLG*hUu*y_+ldQ zEHRQK5=uT$P=S(23`q-|8nu|ylF^z^EUOjFc)N)$0Nk=*l~^}rR)R~HoT7HLZjIj0u2I~Z@Tuz%5_ za8FS_4^FsYTy~A(&kuNp-qq}G{JC!8S(e^lW1K4k)f)$~=cPFp({6U(YazG$mnXKI@g@|kH_8>@O^;PHH*wd(!~hcuPHK2aG)$;w;u~Wh%nic?3^UMV{NL~&&bxlK1+C5{LElkY>>%4AVtMK@STpJjp&<|~lMYFO zVInZbK~#ONJ;2}XQw&sO%_oOzsnSC@a4-oT{Y7F$fAWkG^&I|Vt#5Q>;LT_GiNWTx zlKwdh07geq(DUqQ=0x(AWmx1~T*tc|Il&)FsP{_UQ>T2?yA38z41e?1Py6V9@im-o zAkQVgI-LvUXaCXp7vTQ@?3=0lR*nn1ZxIb16anvH)b#6%TdPRA??-IMIbf~j?OYxS zCj=k2JbUp+#u`6>TFb*)WLid~l5AUxc3GrLP`GFWPDk&MACTYy#zDpp#nH~_R91&h z>8JRk>wmJb5qFca^-X@K%zBsXeXV>*OMPeI+l?Q<7MAKPZ?y-?ba-*lpI`p>3>XD7-InaBekZ

{gC8mDN@(pD-c~ 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 Zyo&4oKBoTweZQS^eO90Hqeosx|Jf*Ngm3@= literal 0 HcmV?d00001 diff --git a/index.html b/index.html index 2672546..0630f29 100644 --- a/index.html +++ b/index.html @@ -1,257 +1,9 @@ - - - - - + Segmentation + + -

-
-
-

Labels

-
-

- - -

-

Views

-
-
Image

-
Boundary

-
Fill
-
-

Data

-
-
Import

-
Export
-
-
-
- diff --git a/js/app/edit.js b/js/app/edit.js new file mode 100644 index 0000000..9d0197c --- /dev/null +++ b/js/app/edit.js @@ -0,0 +1,404 @@ +/** Editor page renderer. + */ +define(['../image/layer', + '../helper/segment-annotator', + '../helper/util'], +function(Layer, Annotator, util) { + // Create the navigation menu. + function createNavigationMenu(params, data, annotator) { + var navigationMenu = document.createElement("p"), + navigation = createNavigation(params, data), + idBlock = document.createElement("div"); + idBlock.className = "edit-top-menu-block"; + idBlock.appendChild( + document.createTextNode(" ID = " + params.id)); + navigationMenu.appendChild(navigation); + navigationMenu.appendChild(idBlock); + return navigationMenu; + } + + // Create the page navigation. + function createNavigation(params, data) { + var id = parseInt(params.id, 10), + container = document.createElement("div"), + indexAnchor = document.createElement("a"), + indexAnchorText = document.createTextNode("Index"), + prevAnchorText = document.createTextNode("Prev"), + nextAnchorText = document.createTextNode("Next"), + prevAnchor, nextAnchor; + indexAnchor.href = util.makeQueryParams({ view: "index" }); + indexAnchor.appendChild(document.createTextNode("Index")); + if (id > 0) { + prevAnchor = document.createElement("a"); + prevAnchor.appendChild(prevAnchorText); + prevAnchor.href = util.makeQueryParams(params, { + id: id - 1 + }); + } + else + prevAnchor = prevAnchorText; + if (id < data.imageURLs.length - 1) { + nextAnchor = document.createElement("a"); + nextAnchor.appendChild(nextAnchorText); + nextAnchor.href = util.makeQueryParams(params, { + id: id + 1 + }); + } + else + nextAnchor = nextAnchorText; + container.appendChild(prevAnchor); + container.appendChild(document.createTextNode(" ")); + container.appendChild(indexAnchor); + container.appendChild(document.createTextNode(" ")); + container.appendChild(nextAnchor); + container.classList.add("edit-top-menu-block"); + return container; + } + + // Create the main content block. + function createMainDisplay(params, data, annotator, imageLayer) { + var container = document.createElement("div"), + imageContainerSpacer = document.createElement("div"), + imageContainer = document.createElement("div"), + annotatorTopMenu = createImageTopMenu(params, data, annotator), + annotatorContainer = document.createElement("div"), + sidebarSpacer = document.createElement("div"), + sidebarContainer = document.createElement("div"), + sidebar = createSidebar(params, data, annotator); + imageContainerSpacer.className = "edit-image-top-menu"; + imageContainer.className = "edit-image-display"; + imageContainer.appendChild(imageContainerSpacer); + imageContainer.appendChild(imageLayer.canvas); + annotatorContainer.className = "edit-image-display"; + annotatorContainer.appendChild(annotatorTopMenu); + annotatorContainer.appendChild(annotator.container); + sidebarSpacer.className = "edit-image-top-menu"; + sidebarContainer.className = "edit-image-display"; + sidebarContainer.appendChild(sidebarSpacer); + sidebarContainer.appendChild(sidebar); + container.className = "edit-main-container"; + container.appendChild(imageContainer); + container.appendChild(annotatorContainer); + container.appendChild(sidebarContainer); + return container; + } + + // Create the menu above the editor. + function createImageTopMenu(params, data, annotator) { + var container = document.createElement("div"), + zoomOutButton = document.createElement("div"), + zoomInButton = document.createElement("div"), + spacer1 = document.createElement("span"), + finerButton = document.createElement("div"), + boundaryButton = document.createElement("div"), + coarserButton = document.createElement("div"), + spacer2 = document.createElement("span"), + alphaMinusButton = document.createElement("div"), + imageButton = document.createElement("div"), + alphaPlusButton = document.createElement("div"); + zoomOutButton.appendChild(document.createTextNode("-")); + zoomOutButton.classList.add("edit-image-top-button"); + zoomOutButton.addEventListener("click", function (event) { + annotator.zoomOut(); + }); + zoomInButton.appendChild(document.createTextNode("zoom +")); + zoomInButton.classList.add("edit-image-top-button"); + zoomInButton.addEventListener("click", function (event) { + annotator.zoomIn(); + }); + spacer1.className = "edit-image-top-spacer"; + boundaryButton.id = "boundary-button"; + boundaryButton.className = "edit-image-top-button"; + boundaryButton.appendChild(document.createTextNode("boundary")); + boundaryButton.addEventListener("click", function (event) { + if (boundaryFlashTimeoutID) + window.clearTimeout(boundaryFlashTimeoutID); + if (boundaryButton.classList.contains("edit-image-top-button-enabled")) + annotator.hide("boundary"); + else + annotator.show("boundary"); + boundaryButton.classList.toggle("edit-image-top-button-enabled"); + }); + finerButton.appendChild(document.createTextNode("-")); + finerButton.className = "edit-image-top-button"; + finerButton.addEventListener("click", function () { + annotator.finer(); + boundaryFlash(); + }); + coarserButton.appendChild(document.createTextNode("+")); + coarserButton.className = "edit-image-top-button"; + coarserButton.addEventListener("click", function () { + annotator.coarser(); + boundaryFlash(); + }); + spacer2.className = "edit-image-top-spacer"; + alphaMinusButton.className = "edit-image-top-button"; + alphaMinusButton.appendChild(document.createTextNode("-")); + alphaMinusButton.addEventListener("click", function () { + annotator.moreAlpha(); + }); + imageButton.className = "edit-image-top-button " + + "edit-image-top-button-enabled"; + imageButton.appendChild(document.createTextNode("image")); + imageButton.addEventListener("click", function () { + if (imageButton.classList.contains("edit-image-top-button-enabled")) + annotator.hide("image"); + else + annotator.show("image"); + imageButton.classList.toggle("edit-image-top-button-enabled"); + }); + alphaPlusButton.className = "edit-image-top-button"; + alphaPlusButton.appendChild(document.createTextNode("+")); + alphaPlusButton.addEventListener("click", function () { + annotator.lessAlpha(); + }); + // + container.className = "edit-image-top-menu"; + container.appendChild(zoomOutButton); + container.appendChild(zoomInButton); + container.appendChild(spacer1); + container.appendChild(finerButton); + container.appendChild(boundaryButton); + container.appendChild(coarserButton); + container.appendChild(spacer2); + container.appendChild(alphaMinusButton); + container.appendChild(imageButton); + container.appendChild(alphaPlusButton); + return container; + } + + // Set up the automatic flash of boundary. + var boundaryFlashTimeoutID = null; + function boundaryFlash() { + var boundaryButton = document.getElementById("boundary-button"); + if (boundaryFlashTimeoutID) { + window.clearTimeout(boundaryFlashTimeoutID); + boundaryFlashTimeoutID = window.setTimeout(function() { + boundaryButton.click(); + boundaryFlashTimeoutID = null; + }, 1000); + } + else if (!boundaryButton.classList.contains( + "edit-image-top-button-enabled")) { + boundaryButton.click(); + boundaryFlashTimeoutID = window.setTimeout(function() { + boundaryButton.click(); + boundaryFlashTimeoutID = null; + }, 1000); + } + } + + // Create the sidebar. + function createSidebar(params, data, annotator) { + var container = document.createElement("div"), + labelPicker = createLabelPicker(params, data, annotator), + spacer1 = document.createElement("div"), + undoButton = document.createElement("div"), + redoButton = document.createElement("div"), + spacer2 = document.createElement("div"), + denoiseButton = document.createElement("div"), + spacer3 = document.createElement("div"), + exportButton = document.createElement("input"); + exportButton.type = "submit"; + exportButton.value = "export"; + exportButton.className = "edit-sidebar-submit"; + exportButton.addEventListener("click", function () { + var filename = (data.annotationURLs) ? + data.annotationURLs[params.id].split(/[\\/]/).pop() : + params.id + ".png"; + downloadURI(annotator.export(), filename); + }); + spacer1.className = "edit-sidebar-spacer"; + undoButton.className = "edit-sidebar-button"; + undoButton.appendChild(document.createTextNode("undo")); + undoButton.addEventListener("click", function () { annotator.undo(); }); + redoButton.className = "edit-sidebar-button"; + redoButton.appendChild(document.createTextNode("redo")); + redoButton.addEventListener("click", function () { annotator.redo(); }); + spacer2.className = "edit-sidebar-spacer"; + denoiseButton.className = "edit-sidebar-button"; + denoiseButton.appendChild(document.createTextNode("denoise")); + denoiseButton.addEventListener("click", function () { + annotator.denoise(); + }); + spacer3.className = "edit-sidebar-spacer"; + container.className = "edit-sidebar"; + container.appendChild(labelPicker); + container.appendChild(spacer1); + container.appendChild(undoButton); + container.appendChild(redoButton); + container.appendChild(spacer2); + container.appendChild(denoiseButton); + container.appendChild(spacer3); + container.appendChild(exportButton); + return container; + } + + function createLabelButton(data, value, index, annotator) { + var colorBox = document.createElement("span"), + labelText = document.createElement("span"), + pickButton = document.createElement("div"), + popupButton = document.createElement("div"), + popupContainer = document.createElement("div"); + colorBox.className = "edit-sidebar-legend-colorbox"; + colorBox.style.backgroundColor = + "rgb(" + data.colormap[index].join(",") + ")"; + labelText.appendChild(document.createTextNode(value)); + labelText.className = "edit-sidebar-legend-label"; + popupButton.appendChild(document.createTextNode("+")); + popupButton.className = "edit-sidebar-popup-trigger"; + popupButton.addEventListener("click", function (event) { + popupContainer.classList.toggle("edit-sidebar-popup-active"); + }); + popupContainer.className = "edit-sidebar-popup"; + popupContainer.appendChild( + createRelabelSelector(data, index, annotator, popupContainer) + ); + popupContainer.addEventListener("click", function (event) { + event.preventDefault(); + }); + pickButton.appendChild(colorBox); + pickButton.appendChild(labelText); + pickButton.appendChild(popupButton); + pickButton.appendChild(popupContainer); + pickButton.id = "label-" + index + "-button"; + pickButton.className = "edit-sidebar-button"; + pickButton.addEventListener("click", function () { + var className = "edit-sidebar-button-selected"; + annotator.currentLabel = index; + var selectedElements = document.getElementsByClassName(className); + for (var i = 0; i < selectedElements.length; ++i) + selectedElements[i].classList.remove(className); + pickButton.classList.add(className); + }); + pickButton.addEventListener('mouseenter', function () { + if (!document.getElementsByClassName("edit-sidebar-popup-active").length) + annotator.highlightLabel(index); + }); + pickButton.addEventListener('mouseleave', function () { + if (!document.getElementsByClassName("edit-sidebar-popup-active").length) + annotator.unhighlightLabel(); + }); + return pickButton; + } + + // Create the label picker button. + function createLabelPicker(params, data, annotator) { + var container = document.createElement("div"); + container.className = "edit-sidebar-label-picker"; + for (var i = 0; i < data.labels.length; ++i) { + var labelButton = createLabelButton(data, data.labels[i], i, annotator); + if (i === 0) { + annotator.currentLabel = 0; + labelButton.classList.add("edit-sidebar-button-selected"); + } + container.appendChild(labelButton); + } + window.addEventListener("click", cancelPopup, true); + return container; + } + + // Cancel popup. + function cancelPopup(event) { + var isOutsidePopup = true, + target = event.target; + while (target.parentNode) { + isOutsidePopup = isOutsidePopup && + !target.classList.contains("edit-sidebar-popup"); + target = target.parentNode; + } + if (isOutsidePopup) { + var popups = document.getElementsByClassName( + "edit-sidebar-popup-active"); + if (popups.length) + for (var i = 0; i < popups.length; ++i) + popups[i].classList.remove("edit-sidebar-popup-active"); + } + } + + // Create the relabel selector. + function createRelabelSelector(data, index, annotator, popupContainer) { + var select = document.createElement("select"), + firstOption = document.createElement("option"); + firstOption.appendChild(document.createTextNode("Change to")); + select.appendChild(firstOption); + for (i = 0; i < data.labels.length; ++i) { + if (i !== index) { + var option = document.createElement("option"); + option.value = i; + option.appendChild(document.createTextNode(data.labels[i])); + select.appendChild(option); + } + } + select.addEventListener("change", function (event) { + var sourceLabel = index; + var targetLabel = parseInt(event.target.value, 10); + if (sourceLabel !== targetLabel) { + var currentLabel = annotator.currentLabel; + annotator.currentLabel = targetLabel; + annotator.fill(sourceLabel); + annotator.currentLabel = currentLabel; + } + popupContainer.classList.remove("edit-sidebar-popup-active"); + firstOption.selected = true; + event.preventDefault(); + }); + return select; + } + + // Download trick. + function downloadURI(uri, filename) { + var anchor = document.createElement("a"); + anchor.style.display = "none"; + anchor.target = "_blank"; // Safari doesn't work. + anchor.download = filename; + anchor.href = uri; + document.body.appendChild(anchor); + anchor.click(); + document.body.removeChild(anchor); + } + + // Entry point. + function render(data, params) { + var id = parseInt(params.id, 10); + if (isNaN(id)) + throw("Invalid id"); + var annotator = new Annotator(data.imageURLs[id], { + width: params.width, + height: params.height, + colormap: data.colormap, + superpixelOptions: { method: "slic", regionSize: 25 }, + onload: function () { + if (data.annotationURLs) + annotator.import(data.annotationURLs[id]); + annotator.hide("boundary"); + boundaryFlash(); + }, + onchange: function () { + var activeLabels = this.getUniqueLabels(), + legendClass = "edit-sidebar-legend-label", + legendActiveClass = "edit-sidebar-legend-label-active", + elements = document.getElementsByClassName(legendClass), + i; + for (i = 0; i < elements.length; ++i) + elements[i].classList.remove(legendActiveClass); + for (i = 0; i < activeLabels.length; ++i) + elements[activeLabels[i]].classList.add(legendActiveClass); + }, + onrightclick: function (label) { + document.getElementById("label-" + label + "-button").click(); + } + }), + imageLayer = new Layer(data.imageURLs[id], { + width: params.width, + height: params.height + }); + document.body.appendChild(createNavigationMenu(params, data, annotator)); + document.body.appendChild(createMainDisplay(params, + data, + annotator, + imageLayer)); + } + + return render; +}); diff --git a/js/app/index.js b/js/app/index.js new file mode 100644 index 0000000..88cc08e --- /dev/null +++ b/js/app/index.js @@ -0,0 +1,56 @@ +/** Index page renderer. + */ +define(["../helper/pagination", + "../helper/segment-viewer", + "../helper/util"], +function(Pagination, Viewer, util) { + function createLabelOptions(params, labels) { + var container = document.createElement("p"), + select = document.createElement("select"), + option; + { + option = document.createElement("option"); + option.appendChild(document.createTextNode("all")); + select.appendChild(option); + } + for (var i = 0; i < labels.length; ++i) { + option = document.createElement("option"); + option.appendChild(document.createTextNode(labels[i])); + if (labels[i] === params.label) { + option.selected = true; + } + select.appendChild(option); + } + select.onchange = function(event) { + if (event.target.value === "all") + delete params.label; + else + params.label = event.target.value; + window.location = util.makeQueryParams(params); + }; + container.appendChild(select); + return container; + } + + function render(data, params) { + var pagination = new Pagination(data.imageURLs.length, params); + document.body.appendChild(pagination.render()); + document.body.appendChild(createLabelOptions(params, data.labels)); + for (var i = pagination.begin(); i < pagination.end(); ++i) { + var viewer = new Viewer(data.imageURLs[i], data.annotationURLs[i], { + width: (params.width || 240), + height: (params.height || 320), + colormap: data.colormap, + labels: data.labels, + excludedLegends: [0], + overlay: i.toString() + }), + anchor = document.createElement("a"); + anchor.appendChild(viewer.container); + anchor.href = util.makeQueryParams({ view: "edit", id: i }); + document.body.appendChild(anchor); + } + } + + return render; +}); diff --git a/js/helper/colormap.js b/js/helper/colormap.js new file mode 100644 index 0000000..04ad8a3 --- /dev/null +++ b/js/helper/colormap.js @@ -0,0 +1,104 @@ +/** Colormap generator. + * + * Example: + * + * define(["./colormap"], function (colormap) { + * var randomColor = colormap.create("random", { size: 16 }); + * var grayColor = colormap.create("gray", { size: 16 }); + * var hsvColor = colormap.create("hsv", { size: 256 }); + * // ... + * }); + * + * Copyright 2015 Kota Yamaguchi + */ +define(function() { + var registry = { + random: function (options) { + var colormap = []; + for (var i = 0; i < options.size; ++i) + colormap.push([Math.floor(256 * Math.random()), + Math.floor(256 * Math.random()), + Math.floor(256 * Math.random())]); + return colormap; + }, + gray: function (options) { + var colormap = []; + for (var i = 0; i < options.size; ++i) { + var intensity = Math.round(255 * i / options.size); + colormap.push([intensity, intensity, intensity]); + } + return colormap; + }, + hsv: function (options) { + var colormap = [], + saturation = (options.saturation === undefined) ? + 1 : options.saturation; + for (var i = 0; i < options.size; ++i) + colormap.push(hsv2rgb(i / options.size, saturation, 1)); + return colormap; + }, + hhsv: function (options) { + var colormap = [], + depth = options.depth || 2, + saturationBlocks = [], + i; + for (i = 0; i < depth; ++i) + saturationBlocks[i] = 0; + for (i = 0; i < options.size; ++i) + saturationBlocks[Math.floor(depth * i / options.size)] += 1; + for (i = 0; i < depth; ++i) { + colormap = colormap.concat(registry.hsv({ + size: saturationBlocks[i], + saturation: 1 - (i / depth) + })); + } + return colormap; + }, + single: function (options) { + var colormap = []; + for (var i = 0; i < options.size; ++i) { + if (i === options.index) + colormap.push(options.foreground || [255, 0, 0]); + else + colormap.push(options.background || [255, 255, 255]); + } + return colormap; + } + }; + + /** Compute RGB value from HSV. + */ + function hsv2rgb(h, s, v) { + var i = Math.floor(h * 6), + f = h * 6 - i, + p = v * (1 - s), + q = v * (1 - f * s), + t = v * (1 - (1 - f) * s), + r, g, b; + switch(i % 6) { + case 0: r = v; g = t; b = p; break; + case 1: r = q; g = v; b = p; break; + case 2: r = p; g = v; b = t; break; + case 3: r = p; g = q; b = v; break; + case 4: r = t; g = p; b = v; break; + case 5: r = v; g = p; b = q; break; + } + return [r, g, b].map(function (x) { return Math.round(x * 255); }); + } + + function create(name, options) { + if (typeof name === "undefined") name = "random"; + if (typeof options === "undefined") options = {}; + options.size = options.size || 8; + return registry[name](options); + } + + function register(name, callback) { + register[name] = callback; + } + + return { + create: create, + register: register + }; +}); diff --git a/js/helper/pagination.js b/js/helper/pagination.js new file mode 100644 index 0000000..61c1ace --- /dev/null +++ b/js/helper/pagination.js @@ -0,0 +1,64 @@ +/** Pagination helper. + * + * Example: + * + * var pagination = new Pagination(100, params); + * document.body.appendChild(pagination.render()); + * + * Copyright 2015 Kota Yamaguchi + */ +define(function () { + function Pagination(count, params) { + var i, anchor, + page = parseInt(params.page || 0, 10), + perPage = parseInt(params.per_page || 30, 10), + neighbors = parseInt(params.page_neighbors || 2, 10), + pages = Math.ceil(count / perPage), + startIndex = Math.min(Math.max(page * perPage, 0), count), + endIndex = Math.min(Math.max((page + 1) * perPage, 0), count); + this.begin = function () { return startIndex; }; + this.end = function () { return endIndex; }; + this.render = function (options) { + options = options || {}; + var index = []; + for (i = 0; i < pages; ++i) { + if (i <= ((page <= neighbors) ? 2 : 1) * neighbors || + (page - neighbors <= i && i <= page + neighbors) || + pages - ((page >= pages - neighbors - 1) ? 2 : 1) * neighbors <= i) + index.push(i); + } + var container = document.createElement(options.nodeType || "p"); + { + anchor = document.createElement("a"); + params.page = page - 1; + if (page > 0) + anchor.href = makeQueryParams(params); + anchor.appendChild(document.createTextNode("Prev")); + container.appendChild(anchor); + container.appendChild(document.createTextNode(" ")); + } + for (i = 0; i < index.length; ++i) { + anchor = document.createElement("a"); + params.page = index[i]; + if (index[i] !== page) + anchor.href = makeQueryParams(params); + anchor.appendChild(document.createTextNode(index[i])); + container.appendChild(anchor); + container.appendChild(document.createTextNode(" ")); + if (i < index.length - 1 && index[i] + 1 != index[i+1]) + container.appendChild(document.createTextNode("... ")); + } + { + anchor = document.createElement("a"); + params.page = page + 1; + if (page < pages - 1) + anchor.href = makeQueryParams(params); + anchor.appendChild(document.createTextNode("Next")); + container.appendChild(anchor); + } + return container; + }; + } + + return Pagination; +}); diff --git a/js/helper/segment-annotator.js b/js/helper/segment-annotator.js new file mode 100644 index 0000000..e5d9b0c --- /dev/null +++ b/js/helper/segment-annotator.js @@ -0,0 +1,538 @@ +/** + * Segment annotation widget. + * + * var annotator = new SegmentAnnotator("/path/to/image.jpg", { + * onload: function () {}, + * onerror: function () {}, + * onchange: function () {}, + * onrightclick: function () {}, + * onleftclick: function () {} + * }); + * document.body.appendChild(annotator.container); + * + * Copyright 2015 Kota Yamaguchi + */ +define(['../image/layer', + '../image/segmentation', + '../image/morph'], +function(Layer, segmentation, morph) { + // Segment annotator. + function Annotator(imageURL, options) { + options = options || {}; + if (typeof imageURL !== "string") + throw "Invalid imageURL"; + this.colormap = options.colormap || [[255, 255, 255], [255, 0, 0]]; + this.boundaryAlpha = options.boundaryAlpha || 127; + this.visualizationAlpha = options.visualizationAlpha || 144; + this.highlightAlpha = options.highlightAlpha || + Math.min(255, this.visualizationAlpha + 128); + this.currentZoom = 1.0; + this.defaultLabel = options.defaultLabel || 0; + this.maxHistoryRecord = options.maxHistoryRecord || 10; + this.onchange = options.onchange || null; + this.onrightclick = options.onrightclick || null; + this.onleftclick = options.onleftclick || null; + this._createLayers(options); + this._initializeHistory(options); + var annotator = this; + this.layers.image.load(imageURL, { + width: options.width, + height: options.height, + onload: function () { annotator._initialize(options); }, + onerror: options.onerror + }); + } + + // Run superpixel segmentation. + Annotator.prototype.resetSuperpixels = function (options) { + options = options || {}; + this.layers.superpixel.copy(this.layers.image); + this.segmentation = segmentation.create(this.layers.image.imageData, + options); + this._updateSuperpixels(options); + return this; + }; + + // Adjust the superpixel resolution. + Annotator.prototype.finer = function (options) { + this.segmentation.finer(); + this._updateSuperpixels(options); + return this; + }; + + // Adjust the superpixel resolution. + Annotator.prototype.coarser = function (options) { + this.segmentation.coarser(); + this._updateSuperpixels(options); + return this; + }; + + // Undo the edit. + Annotator.prototype.undo = function () { + if (this.currentHistoryRecord < 0) + return false; + var record = this.history[this.currentHistoryRecord--]; + this._fillPixels(record.pixels, record.prev); + this.layers.visualization.render(); + if (typeof this.onchange === "function") + this.onchange.call(this); + return this.currentHistoryRecord < 0; + }; + + // Redo the edit. + Annotator.prototype.redo = function () { + if (this.currentHistoryRecord >= this.history.length - 1) + return false; + var record = this.history[++this.currentHistoryRecord]; + this._fillPixels(record.pixels, record.next); + this.layers.visualization.render(); + if (typeof this.onchange === "function") + this.onchange.call(this); + return this.currentHistoryRecord >= this.history.length; + }; + + // Get unique labels in the current annotation. + Annotator.prototype.getUniqueLabels = function () { + var uniqueIndex = [], + data = this.layers.annotation.imageData.data; + for (var i = 0; i < data.length; i += 4) { + var label = _getEncodedLabel(data, i); + if (uniqueIndex.indexOf(label) < 0) { + uniqueIndex.push(label); + } + } + return uniqueIndex.sort(function (a, b) { return a - b; }); + }; + + // Fill all the pixels assigned the target label or all. + Annotator.prototype.fill = function (targetLabel) { + var pixels = [], + annotationData = this.layers.annotation.imageData.data; + for (var i = 0; i < annotationData.length; i += 4) { + var label = _getEncodedLabel(annotationData, i); + if (label === targetLabel || targetLabel === undefined) + pixels.push(i); + } + if (pixels.length > 0) + this._updateAnnotation(pixels, this.currentLabel); + return this; + }; + + Annotator.prototype.setAlpha = function (alpha) { + this.visualizationAlpha = Math.max(Math.min(alpha, 255), 0); + this.layers.visualization.setAlpha(this.visualizationAlpha).render(); + return this; + }; + + Annotator.prototype.lessAlpha = function (scale) { + return this.setAlpha(this.visualizationAlpha - (scale || 1) * 20); + }; + + Annotator.prototype.moreAlpha = function (scale) { + return this.setAlpha(this.visualizationAlpha + (scale || 1) * 20); + }; + + // Import an existing annotation. + Annotator.prototype.import = function (annotationURL, options) { + options = options || {}; + var annotator = this; + this.layers.annotation.load(annotationURL, { + onload: function () { + if (options.grayscale) + this.gray2index(); + annotator.layers + .visualization + .copy(this) + .applyColormap(annotator.colormap) + .setAlpha(annotator.visualizationAlpha) + .render(); + this.setAlpha(0).render(); + this.history = []; + this.currentHistoryRecord = -1; + if (typeof options.onload === "function") + options.onload.call(annotator); + if (typeof annotator.onchange === "function") + annotator.onchange.call(annotator); + }, + onerror: options.onerror + }); + return this; + }; + + // Export the annotation in data URL. + Annotator.prototype.export = function () { + this.layers.annotation.setAlpha(255); + this.layers.annotation.render(); + var data = this.layers.annotation.canvas.toDataURL(); + this.layers.annotation.setAlpha(0); + this.layers.annotation.render(); + return data; + }; + + // Show a specified layer. + Annotator.prototype.show = function (layer) { + this.layers[layer].canvas.style.display = "inline-block"; + return this; + }; + + // Hide a specified layer. + Annotator.prototype.hide = function (layer) { + this.layers[layer].canvas.style.display = "none"; + return this; + }; + + // Highlight a specified label. + Annotator.prototype.highlightLabel = function (label) { + var pixels = [], + annotationData = this.layers.annotation.imageData.data; + for (var i = 0; i < annotationData.length; i += 4) { + var currentLabel = _getEncodedLabel(annotationData, i); + if (currentLabel === label) + pixels.push(i); + } + this._updateHighlight(pixels); + return this; + }; + + // Disable highlight. + Annotator.prototype.unhighlightLabel = function () { + this._updateHighlight(null); + return this; + }; + + // Zoom to specific resolution. + Annotator.prototype.zoom = function (scale) { + this.currentZoom = Math.max(Math.min(scale || 1.0, 10.0), 1.0); + this.innerContainer.style.zoom = this.currentZoom; + this.innerContainer.style.MozTransform = + "scale(" + this.currentZoom + ")"; + return this; + }; + + // Zoom in. + Annotator.prototype.zoomIn = function (scale) { + return this.zoom(this.currentZoom + (scale || 0.25)); + }; + + // Zoom out. + Annotator.prototype.zoomOut = function (scale) { + return this.zoom(this.currentZoom - (scale || 0.25)); + }; + + // // Align the current annotation to the boundary of superpixels. + // Annotator.prototype.alignBoundary = function () { + // var annotationData = this.layers.annotation.imageData.data; + // for (var i = 0; i < this.pixelIndex.length; ++i) { + // var pixels = this.pixelIndex[i], + // label = _findMostFrequent(annotationData, pixels); + // this._fillPixels(pixels, label); + // } + // this.layers.visualization.render(); + // this.history = []; + // this.currentHistoryRecord = 0; + // }; + + Annotator.prototype.denoise = function () { + var indexImage = morph.decodeIndexImage(this.layers.annotation.imageData), + result = morph.maxFilter(indexImage); + var pixels = new Int32Array(result.data.length); + for (var i = 0; i < pixels.length; ++i) + pixels[i] = 4 * i; + this._updateAnnotation(pixels, result.data); + return this; + }; + + // Private methods. + + Annotator.prototype._createLayers = function (options) { + var onload = options.onload; + delete options.onload; + this.container = document.createElement("div"); + this.container.classList.add("segment-annotator-outer-container"); + this.innerContainer = document.createElement("div"); + this.innerContainer.classList.add("segment-annotator-inner-container"); + this.layers = { + image: new Layer(options), + superpixel: new Layer(options), + visualization: new Layer(options), + boundary: new Layer(options), + annotation: new Layer(options) + }; + options.onload = onload; + for (var key in this.layers) { + var canvas = this.layers[key].canvas; + canvas.classList.add("segment-annotator-layer"); + this.innerContainer.appendChild(canvas); + } + this.container.appendChild(this.innerContainer); + this._resizeLayers(options); + }; + + Annotator.prototype._resizeLayers = function (options) { + this.width = options.width || this.layers.image.canvas.width; + this.height = options.height || this.layers.image.canvas.height; + for (var key in this.layers) { + if (key !== "image") { + var canvas = this.layers[key].canvas; + canvas.width = this.width; + canvas.height = this.height; + } + } + this.innerContainer.style.width = this.width; + this.innerContainer.style.height = this.height; + this.container.style.width = this.width; + this.container.style.height = this.height; + }; + + Annotator.prototype._initializeHistory = function (options) { + this.history = []; + this.currentHistoryRecord = -1; + }; + + Annotator.prototype._initialize = function (options) { + options = options || {}; + if (!options.width) + this._resizeLayers(options); + this._initializeAnnotationLayer(); + this._initializeVisualizationLayer(); + this._initializeEvents(); + this.resetSuperpixels(options.superpixelOptions); + if (typeof options.onload === "function") + options.onload.call(this); + if (typeof this.onchange === "function") + this.onchange.call(this); + }; + + Annotator.prototype._initializeEvents = function () { + var canvas = this.layers.annotation.canvas, + mousestate = { down: false, button: 0 }, + annotator = this; + canvas.oncontextmenu = function() { return false; }; + function updateIfActive(event) { + var offset = annotator._getClickOffset(event), + superpixelData = annotator.layers.superpixel.imageData.data, + superpixelIndex = _getEncodedLabel(superpixelData, offset), + pixels = annotator.pixelIndex[superpixelIndex]; + annotator._updateHighlight(pixels); + if (mousestate.down) { + if (mousestate.button == 2 && + typeof annotator.onrightclick === "function") { + var annotationData = annotator.layers.annotation.imageData.data; + annotator.onrightclick.call(annotator, + _getEncodedLabel(annotationData, offset)); + } + else { + annotator._updateAnnotation(pixels, annotator.currentLabel); + if (typeof annotator.onleftclick === "function") + annotator.onleftclick.call(annotator, annotator.currentLabel); + } + } + } + canvas.addEventListener('mousemove', updateIfActive); + canvas.addEventListener('mouseup', updateIfActive); + canvas.addEventListener('mouseleave', function (event) { + annotator._updateHighlight(null); + }); + canvas.addEventListener('mousedown', function (event) { + mousestate.down = true; + mousestate.button = event.button; + }); + window.addEventListener('mouseup', function (event) { + mousestate.down = false; + }); + }; + + Annotator.prototype._updateBoundaryLayer = function () { + var boundaryLayer = this.layers.boundary; + boundaryLayer.copy(this.layers.superpixel); + boundaryLayer.computeEdgemap({ + foreground: [255, 255, 255, this.boundaryAlpha], + background: [255, 255, 255, 0] + }); + boundaryLayer.render(); + }; + + Annotator.prototype._initializeAnnotationLayer = function () { + var layer = this.layers.annotation; + layer.resize(this.width, this.height); + this.currentLabel = this.defaultLabel; + layer.fill([this.defaultLabel, 0, 0, 0]); + layer.render(); + }; + + Annotator.prototype._initializeVisualizationLayer = function () { + var layer = this.layers.visualization; + layer.resize(this.width, this.height); + var initialColor = this.colormap[this.defaultLabel] + .concat([this.visualizationAlpha]); + layer.fill(initialColor); + layer.render(); + }; + + Annotator.prototype._updateSuperpixels = function () { + var annotator = this; + this.layers.superpixel.process(function (imageData) { + imageData.data.set(annotator.segmentation.result.data); + annotator._createPixelIndex(annotator.segmentation.result.numSegments); + annotator._updateBoundaryLayer(); + this.setAlpha(0).render(); + }); + }; + + Annotator.prototype._createPixelIndex = function (numSegments) { + var pixelIndex = new Array(numSegments), + data = this.layers.superpixel.imageData.data, + i; + for (i = 0; i < numSegments; ++i) + pixelIndex[i] = []; + for (i = 0; i < data.length; i += 4) { + var index = data[i] | (data[i + 1] << 8) | (data[i + 2] << 16); + pixelIndex[index].push(i); + } + this.currentPixels = null; + this.pixelIndex = pixelIndex; + }; + + Annotator.prototype._getClickOffset = function (event) { + var container = this.container, + x = Math.round( + (event.pageX - container.offsetLeft + container.scrollLeft) * + (container.offsetWidth / container.scrollWidth) + ), + y = Math.round( + (event.pageY - container.offsetTop + container.scrollTop) * + (container.offsetHeight / container.scrollHeight) + ), + data = this.layers.superpixel.imageData.data, + offset; + var canvas = this.layers.image.canvas; + x = Math.max(Math.min(x, this.layers.visualization.canvas.width - 1), 0); + y = Math.max(Math.min(y, this.layers.visualization.canvas.height - 1), 0); + offset = 4 * (y * this.layers.visualization.canvas.width + x); + return offset; + }; + + Annotator.prototype._updateHighlight = function (pixels) { + var visualizationData = this.layers.visualization.imageData.data, + boundaryData = this.layers.boundary.imageData.data, + i, + offset; + if (this.currentPixels !== null) { + for (i = 0; i < this.currentPixels.length; ++i) { + offset = this.currentPixels[i]; + visualizationData[offset + 3] = this.visualizationAlpha; + if (boundaryData[offset + 3]) + boundaryData[offset + 3] = this.boundaryAlpha; + } + } + this.currentPixels = pixels; + if (this.currentPixels !== null) { + for (i = 0; i < pixels.length; ++i) { + offset = pixels[i]; + visualizationData[offset + 3] = this.highlightAlpha; + if (boundaryData[offset + 3]) + boundaryData[offset + 3] = this.highlightAlpha; + } + } + this.layers.visualization.render(); + this.layers.boundary.render(); + }; + + Annotator.prototype._fillPixels = function (pixels, labels) { + if (pixels.length !== labels.length) + throw "Invalid fill: " + pixels.length + " !== " + labels.length; + var annotationData = this.layers.annotation.imageData.data, + visualizationData = this.layers.visualization.imageData.data; + for (i = 0; i < pixels.length; ++i) { + var offset = pixels[i], + label = labels[i], + color = this.colormap[label]; + _setEncodedLabel(annotationData, offset, label); + visualizationData[offset + 0] = color[0]; + visualizationData[offset + 1] = color[1]; + visualizationData[offset + 2] = color[2]; + } + }; + + // Update label. + Annotator.prototype._updateAnnotation = function (pixels, labels) { + var updates; + labels = (typeof labels === "object") ? + labels : _fillArray(new Int32Array(pixels.length), labels); + updates = this._getDifferentialUpdates(pixels, labels); + if (updates.pixels.length === 0) + return this; + this._updateHistory(updates); + this._fillPixels(updates.pixels, updates.next); + this.layers.visualization.render(); + if (typeof this.onchange === "function") + this.onchange.call(this); + return this; + }; + + // Get the differential update of labels. + Annotator.prototype._getDifferentialUpdates = function (pixels, labels) { + if (pixels.length !== labels.length) + throw "Invalid labels"; + var annotationData = this.layers.annotation.imageData.data, + updates = { pixels: [], prev: [], next: [] }; + for (var i = 0; i < pixels.length; ++i) { + var label = _getEncodedLabel(annotationData, pixels[i]); + if (label !== labels[i]) { + updates.pixels.push(pixels[i]); + updates.prev.push(label); + updates.next.push(labels[i]); + } + } + return updates; + }; + + Annotator.prototype._updateHistory = function (updates) { + this.history = this.history.slice(0, this.currentHistoryRecord + 1); + this.history.push(updates); + if (this.history.length > this.maxHistoryRecord) + this.history = this.history.slice(1, this.history.length); + else + ++this.currentHistoryRecord; + }; + + function _fillArray(array, value) { + for (var i = 0; i < array.length; ++i) + array[i] = value; + return array; + } + + function _findMostFrequent(annotationData, pixels) { + var histogram = {}, + j; + for (j = 0; j < pixels.length; ++j) { + var label = _getEncodedLabel(annotationData, pixels[j]); + histogram[label] = (histogram[label]) ? histogram[label] + 1 : 1; + } + var maxFrequency = 0, + majorLabel = 0; + for (j in histogram) { + var frequency = histogram[j]; + if (frequency > maxFrequency) { + maxFrequency = frequency; + majorLabel = j; + } + } + return majorLabel; + } + + function _getEncodedLabel(array, offset) { + return array[offset] | + (array[offset + 1] << 8) | + (array[offset + 2] << 16); + } + + function _setEncodedLabel(array, offset, label) { + array[offset + 0] = label & 255; + array[offset + 1] = (label >>> 8) & 255; + array[offset + 2] = (label >>> 16) & 255; + array[offset + 3] = 255; + } + + return Annotator; +}); diff --git a/js/helper/segment-viewer.js b/js/helper/segment-viewer.js new file mode 100644 index 0000000..aa860e6 --- /dev/null +++ b/js/helper/segment-viewer.js @@ -0,0 +1,134 @@ +/** Segmentation viewer. + * + * var viewer = new Viewer("/path/to/image.jpg", "/path/to/annotation.png", { + * colormap: [[255, 255, 255], [0, 255, 255]], + * labels: ["background", "foreground"], + * onload: function () { } + * }); + * document.body.appendChild(viewer.container); + * + * Copyright 2015 Kota Yamaguchi + */ +define(['../image/layer'], function(Layer) { + // Segment viewer. + function Viewer(imageURL, annotationURL, options) { + if (typeof options === "undefined") options = {}; + this.colormap = options.colormap || [[255, 255, 255], [255, 0, 0]]; + this.labels = options.labels; + this._createLayers(imageURL, annotationURL, options); + var viewer = this; + this.layers.image.load(imageURL, { + width: options.width, + height: options.height, + onload: function () { viewer._initializeIfReady(options); } + }); + this.layers.visualization.load(annotationURL, { + width: options.width, + height: options.height, + imageSmoothingEnabled: false, + onload: function () { viewer._initializeIfReady(options); }, + onerror: options.onerror + }); + if (options.overlay) + viewer.addOverlay(options.overlay); + } + + Viewer.prototype._createLayers = function (imageURL, + annotationURL, + options) { + var onload = options.onload; + delete options.onload; + this.container = document.createElement("div"); + this.container.classList.add("segment-viewer-container"); + this.layers = { + image: new Layer(options), + visualization: new Layer(options) + }; + options.onload = onload; + for (var key in this.layers) { + var canvas = this.layers[key].canvas; + canvas.classList.add("segment-viewer-layer"); + this.container.appendChild(canvas); + } + this._unloadedLayers = Object.keys(this.layers).length; + this._resizeLayers(options); + }; + + Viewer.prototype._resizeLayers = function (options) { + this.width = options.width || this.layers.image.canvas.width; + this.height = options.height || this.layers.image.canvas.height; + for (var key in this.layers) { + if (key !== "image") { + var canvas = this.layers[key].canvas; + canvas.width = this.width; + canvas.height = this.height; + } + } + this.container.style.width = this.width; + this.container.style.height = this.height; + }; + + Viewer.prototype._initializeIfReady = function (options) { + if (--this._unloadedLayers > 0) + return; + this._resizeLayers(options); + var viewer = this; + this.layers.visualization.process(function () { + var uniqueIndex = getUniqueIndex(this.imageData.data); + this.applyColormap(viewer.colormap); + this.setAlpha(192); + this.render(); + if (viewer.labels) + viewer.addLegend(uniqueIndex.filter(function (x) { + return (options.excludedLegends || []).indexOf(x) < 0; + })); + }); + }; + + Viewer.prototype.addOverlay = function (text) { + var overlayContainer = document.createElement("div"); + overlayContainer.classList.add("segment-viewer-overlay-container"); + if (text) + overlayContainer.appendChild(document.createTextNode(text)); + this.container.appendChild(overlayContainer); + }; + + Viewer.prototype.addLegend = function (index) { + var legendContainer = document.createElement("div"), + i; + if (typeof index === "undefined") { + index = []; + for (i = 0; i < labels.length; ++i) + index.push(i); + } + legendContainer.classList.add("segment-viewer-legend-container"); + for (i = 0; i < index.length; ++i) { + var label = this.labels[index[i]], + color = this.colormap[index[i]], + legendItem = document.createElement("div"), + colorbox = document.createElement("span"), + legendLabel = document.createElement("span"); + colorbox.classList.add("segment-viewer-legend-colorbox"); + colorbox.style.backgroundColor = "rgb(" + color.join(",") + ")"; + legendItem.classList.add("segment-viewer-legend-item"); + legendLabel.appendChild(document.createTextNode(" " + label)); + legendLabel.classList.add("segment-viewer-legend-label"); + legendItem.appendChild(colorbox); + legendItem.appendChild(legendLabel); + legendContainer.appendChild(legendItem); + } + this.container.appendChild(legendContainer); + }; + + var getUniqueIndex = function (data) { + var uniqueIndex = []; + for (var i = 0; i < data.length; i += 4) { + if (uniqueIndex.indexOf(data[i]) < 0) { + uniqueIndex.push(data[i]); + } + } + return uniqueIndex.sort(function (a, b) { return a - b; }); + }; + + return Viewer; +}); diff --git a/js/helper/util.js b/js/helper/util.js new file mode 100644 index 0000000..88e705e --- /dev/null +++ b/js/helper/util.js @@ -0,0 +1,51 @@ +/** Misc utilities regarding HTTP request. + */ +define(function () { + // Get JSON by AJAX request. + function requestJSON(url, callback) { + var xmlhttp = new XMLHttpRequest(); + xmlhttp.onreadystatechange = function() { + if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { + var data = xmlhttp.responseText; + callback(JSON.parse(data)); + } + }; + xmlhttp.open("GET", url, true); + xmlhttp.send(); + } + + // Parse query params. + function getQueryParams(queryString) { + var tokens, + params = {}, + re = /[?&]?([^=]+)=([^&]*)/g; + queryString = queryString || document.location.search; + while (tokens = re.exec(queryString.split("+").join(" "))) + params[decodeURIComponent(tokens[1])] = decodeURIComponent(tokens[2]); + return params; + } + + // Create query params from an object. + function makeQueryParams(params, updates) { + params = params || {}; + updates = updates || {}; + var queryString = "?"; + var keys = Object.keys(params); + for (var i = 0; i < keys.length; ++i) { + var value = updates[keys[i]]; + if (typeof value === "undefined") + value = params[keys[i]]; + queryString = queryString + + encodeURIComponent(keys[i]) + "=" + + encodeURIComponent(value) + + ((i < keys.length - 1) ? "&" : ""); + } + return queryString; + } + + return { + requestJSON: requestJSON, + getQueryParams: getQueryParams, + makeQueryParams: makeQueryParams + }; +}); diff --git a/js/image/canny.js b/js/image/canny.js new file mode 100644 index 0000000..a83018f --- /dev/null +++ b/js/image/canny.js @@ -0,0 +1,225 @@ +/** Canny edge detection. + * + * var edge = canny(imageData, {}); + * + * Copyright 2015 Kota Yamaguchi + */ +define(function () { + function createIntensityData(width, height) { + return { + width: width, + height: height, + data: new Float32Array(width * height) + }; + } + + function createGaussian1D(k, sigma) { + k = k || 1; + sigma = sigma || 1.3; + var size = 2 * k + 1, + kernel = new Float32Array(size), + coeff = 1 / (2 * Math.PI * Math.pow(sigma, 2)); + for (var i = 0; i < size; ++i) + kernel[i] = coeff * Math.exp(-Math.pow((i - k) / sigma, 2)); + return normalize(kernel); + } + + function normalize(array) { + var sum = 0, + i; + for (i = 0; i < array.length; ++i) + sum += array[i]; + for (i = 0; i < array.length; ++i) + array[i] /= sum; + return array; + } + + function rgb2intensity(imageData) { + var intensity = createIntensityData(imageData.width, imageData.height), + newData = intensity.data, + data = imageData.data; + for (var i = 0; i < imageData.width * imageData.height; ++i) { + newData[i] = (data[4 * i] + data[4 * i + 1] + data[4 * i + 2]) / + (3 * 255); + } + return intensity; + } + + function intensity2rgb(intensity) { + var newImageData = new ImageData(intensity.width, intensity.height), + data = intensity.data, + newData = newImageData.data; + for (var i = 0; i < data.length; ++i) { + var value = Math.max(Math.min(Math.round(255 * data[i]), 255), 0); + newData[4 * i] = value; + newData[4 * i + 1] = value; + newData[4 * i + 2] = value; + newData[4 * i + 3] = 255; + } + return newImageData; + } + + function padImage(intensity, size) { + size = size || [0, 0]; + if (typeof size === "number") + size = [size, size]; + var width = intensity.width, + height = intensity.height, + data = intensity.data, + newIntensity = createIntensityData(width + 2 * size[0], + height + 2 * size[1]), + newData = newIntensity.data, + i, j, k; + for (i = 0; i < newIntensity.height; ++i) { + var y = (i < size[1]) ? size[1] - i: + (i >= height + size[1]) ? 2 * height - size[1] + 1 - i : + i - size[1]; + for (j = 0; j < newIntensity.width; ++j) { + var x = (j < size[0]) ? size[0] - j: + (j >= width + size[0]) ? 2 * width - size[0] + 1 - j : + j - size[0], + newOffset = i * newIntensity.width + j, + oldOffset = y * width + x; + newData[newOffset] = data[oldOffset]; + } + } + return newIntensity; + } + + function filter1D(intensity, kernel, horizontal) { + var size = Math.round((kernel.length - 1) / 2), + paddedData = padImage(intensity, + (horizontal) ? [size, 0] : [0, size]), + data = paddedData.data, + width = paddedData.width, + height = paddedData.height, + temporaryData = new Float32Array(data.length), + i, j, k, offset, value; + if (horizontal) { + for (i = 0; i < height; ++i) { + for (j = size; j < width - size; ++j){ + offset = i * width + j; + value = kernel[size] * data[offset]; + for (k = 1; k <= size; ++k) { + value += kernel[size + k] * data[offset + k] + + kernel[size - k] * data[offset - k]; + } + temporaryData[offset] = value; + } + } + } + else { + for (i = size; i < height - size; ++i) { + for (j = 0; j < width; ++j) { + offset = i * width + j; + value = kernel[size] * data[offset]; + for (k = 1; k <= size; ++k) { + value += kernel[size + k] * data[offset + width * k] + + kernel[size - k] * data[offset - width * k]; + } + temporaryData[offset] = value; + } + } + } + paddedData.data.set(temporaryData); + return padImage(paddedData, (horizontal) ? [-size, 0] : [0, -size]); + } + + function filter1DTwice(intensity, kernel) { + return filter1D(filter1D(intensity, kernel, true), kernel, false); + } + + function detectEdges(intensity, options) { + var width = intensity.width, + height = intensity.height, + magnitude = new Float32Array(intensity.data.length), + orientation = new Float32Array(intensity.data.length), + suppressed = new Float32Array(intensity.data.length), + result = createIntensityData(width, height), + SobelKernel = [-1, 0, 1], + dx = filter1D(intensity, SobelKernel, true), + dy = filter1D(intensity, SobelKernel, false), + i, j, direction, offset, offset1, offset2; + for (i = 0; i < intensity.data.length; ++i) { + magnitude[i] = Math.sqrt(Math.pow(dx.data[i], 2) + + Math.pow(dy.data[i], 2)); + direction = Math.atan2(dy.data[i], dx.data[i]); + orientation[i] = (direction < 0) ? direction + Math.PI : + (direction > Math.PI) ? direction - Math.PI : direction; + } + // NMS. + for (i = 1; i < height - 1; ++i) { + for (j = 1; j < width - 1; ++j) { + offset = i * width + j; + direction = orientation[offset]; + if (direction < Math.PI / 8 || 7 * Math.PI / 8 <= direction) { + offset1 = offset - 1; + offset2 = offset + 1; + } + else if (Math.PI / 8 <= direction && direction < 3 * Math.PI / 8) { + offset1 = offset - width - 1; + offset2 = offset + width + 1; + } + else if (3 * Math.PI / 8 <= direction && direction < 5 * Math.PI / 8) { + offset1 = offset - width; + offset2 = offset + width; + } + else if (5 * Math.PI / 8 <= direction && direction < 7 * Math.PI / 8) { + offset1 = offset - width + 1; + offset2 = offset + width - 1; + } + suppressed[offset] = (magnitude[offset] > magnitude[offset1] && + magnitude[offset] > magnitude[offset2]) ? + magnitude[offset] : 0; + } + } + // Hysteresis. + for (i = 1; i < height - 1; ++i) { + for (j = 1; j < width - 1; ++j) { + offset = i * width + j; + direction = orientation[offset] - 0.5 * Math.PI; + direction = (direction < 0) ? direction + Math.PI : direction; + if (direction < Math.PI / 8 || 7 * Math.PI / 8 <= direction) { + offset1 = offset - 1; + offset2 = offset + 1; + } + else if (Math.PI / 8 <= direction && direction < 3 * Math.PI / 8) { + offset1 = offset - width - 1; + offset2 = offset + width + 1; + } + else if (3 * Math.PI / 8 <= direction && direction < 5 * Math.PI / 8) { + offset1 = offset - width; + offset2 = offset + width; + } + else if (5 * Math.PI / 8 <= direction && direction < 7 * Math.PI / 8) { + offset1 = offset - width + 1; + offset2 = offset + width - 1; + } + result.data[offset] = + (suppressed[offset] >= options.highThreshold || + (suppressed[offset] >= options.lowThreshold && + suppressed[offset1] >= options.highThreshold) || + (suppressed[offset] >= options.lowThreshold && + suppressed[offset2] >= options.highThreshold)) ? + suppressed[offset] : 0; + } + } + result.magnitude = magnitude; + return result; + } + + function canny(imageData, options) { + options = options || {}; + options.kernelTail = options.kernelTail || 4; + options.sigma = options.sigma || 1.6; + options.highThreshold = options.highThreshold || 0.04; + options.lowThreshold = options.lowThreshold || 0.3 * options.highThreshold; + var intensity = rgb2intensity(imageData); + var gaussianKernel = createGaussian1D(options.kernelTail, options.sigma); + var blurredData = filter1DTwice(intensity, gaussianKernel); + var edge = detectEdges(blurredData, options); + return edge; + } + + return canny; +}); diff --git a/js/image/distance-transform.js b/js/image/distance-transform.js new file mode 100644 index 0000000..80b8622 --- /dev/null +++ b/js/image/distance-transform.js @@ -0,0 +1,106 @@ +/** Distance transform implementation based on the following paper. + * + * Distance Transforms of Sampled Functions + * P. Felzenszwalb, D. Huttenlocher + * Theory of Computing, Vol. 8, No. 19, September 2012 + * + * Copyright 2015 Kota Yamaguchi + */ +define(function () { + var INF = 1e20; + + function distanceTransform1D(f, n, flag) { + var d = new Float32Array(n), + v = new Int32Array(n), + z = new Float32Array(n + 1), + k = 0, + square = function(x) { return x * x; }, + q; + v[0] = 0; + z[0] = -INF; + z[1] = INF; + for (q = 1; q <= n - 1; ++q) { + var s = ((f[q] + square(q)) - (f[v[k]] + square(v[k]))) / + (2 * q - 2 * v[k]); + if (isNaN(s)) + throw "NaN error"; + while (s <= z[k]) { + --k; + s = ((f[q] + square(q)) - (f[v[k]] + square(v[k]))) / + (2 * q - 2 * v[k]); + if (isNaN(s)) + throw "NaN error"; + } + ++k; + v[k] = q; + z[k] = s; + z[k + 1] = INF; + } + k = 0; + for (q = 0; q <= n - 1; ++q) { + while (z[k + 1] < q) + k++; + d[q] = square(q - v[k]) + f[v[k]]; + } + return d; + } + + function distanceTransform2D(distanceMap) { + var width = distanceMap.width, + height = distanceMap.height, + data = distanceMap.data, + f = new Float32Array(Math.max(width, height)), + x, y, offset; + // Column transform. + for (x = 0; x < width; ++x) { + for (y = 0; y < height; ++y) + f[y] = data[y * width + x]; + d = distanceTransform1D(f, height); + for (y = 0; y < height; ++y) + data[y * width + x] = d[y]; + } + // Row transform. + for (y = 0; y < height; ++y) { + for (x = 0; x < width; ++x) + f[x] = data[y * width + x]; + d = distanceTransform1D(f, width, true); + for (x = 0; x < width; ++x) + data[y * width + x] = d[x]; + } + // Sqrt. + for (x = 0; x < data.length; ++x) + data[x] = Math.sqrt(data[x]); + } + + function distanceTransform(intensity, options) { + options = options || {}; + var distanceMap = { + width: intensity.width, + height: intensity.height, + data: new Float32Array(intensity.data.length) + }; + for (var offset = 0; offset < distanceMap.data.length; ++offset) + distanceMap.data[offset] = (intensity.data[offset]) ? 0 : INF; + distanceTransform2D(distanceMap); + //if (options.outputRGB) + // distanceMap = intensity2rgb(distanceMap); + return distanceMap; + } + + // For debugging. + function intensity2rgb(intensity) { + var newImageData = new ImageData(intensity.width, intensity.height), + data = intensity.data, + newData = newImageData.data; + for (var i = 0; i < data.length; ++i) { + var value = Math.round(data[i]); + newData[4 * i] = 255 & value; + newData[4 * i + 1] = 255 & (value >> 8); + newData[4 * i + 2] = 255 & (value >> 16); + newData[4 * i + 3] = 255; + } + return newImageData; + } + + return distanceTransform; +}); diff --git a/js/image/layer.js b/js/image/layer.js new file mode 100644 index 0000000..e89b04d --- /dev/null +++ b/js/image/layer.js @@ -0,0 +1,216 @@ +/** Image canvas wrapper. + * + * Example: + * + * var layer = new Layer("/path/to/image.jpg", { + * onload: function () { + * this.resize(200, 300); + * document.body.appendChild(this.canvas); + * } + * }); + * + * Copyright 2015 Kota Yamaguchi + */ +define(function() { + // Canvas wrapper object. + function Layer(source, options) { + options = options || {}; + this.canvas = document.createElement("canvas"); + this.canvas.width = options.width || this.canvas.width; + this.canvas.height = options.height || this.canvas.height; + if (source) { + if (typeof source === "string" || + typeof source === "object" && source.nodeName === "IMG") + this.load(source, options); + else if (typeof source === "object" && + (source.nodeName === "CANVAS" || source instanceof ImageData)) + this.fromCanvas(source, options); + } + } + + Layer.prototype.load = function (source, options) { + options = options || {}; + if (typeof options === "function") options = { onload: options }; + var image, layer = this; + this.canvas.width = options.width || this.canvas.width; + this.canvas.height = options.height || this.canvas.height; + if (typeof source === "string") { + image = new Image(); + image.src = source; + } + else + image = source; + image.onload = function() { layer._onImageLoad(image, options); }; + if (typeof options.onerror === "function") + image.onerror = options.onerror.call(this); + return this; + }; + + Layer.prototype._onImageLoad = function (image, options) { + this.canvas.width = options.width || image.width; + this.canvas.height = options.height || image.height; + var context = this.canvas.getContext("2d"); + this._setImageSmoothing(context, options); + context.drawImage(image, 0, 0, image.width, image.height, + 0, 0, this.canvas.width, this.canvas.height); + this.imageData = context.getImageData(0, 0, + this.canvas.width, + this.canvas.height); + if (typeof options.onload === "function") + options.onload.call(this); + }; + + Layer.prototype.fromCanvas = function (source, options) { + options = options || {}; + if (typeof options === "function") options = { onload: options }; + this.canvas.width = source.width; + this.canvas.height = source.height; + var context = this.canvas.getContext("2d"); + this._setImageSmoothing(context, options); + if (source instanceof ImageData) + context.putImageData(source, 0, 0); + else + context.drawImage(source, 0, 0, this.canvas.width, this.canvas.height); + this.imageData = context.getImageData(0, 0, + this.canvas.width, + this.canvas.height); + if (typeof options.onload === "function") + options.onload.call(this); + return this; + }; + + Layer.prototype.fromImageData = function (imageData, options) { + options = options || {}; + if (typeof options === "function") options = { onload: options }; + this.canvas.width = imageData.width; + this.canvas.height = imageData.height; + var context = this.canvas.getContext("2d"); + this._setImageSmoothing(context, options); + context.drawImage(source, 0, 0, this.canvas.width, this.canvas.height); + this.imageData = context.getImageData(0, 0, + this.canvas.width, + this.canvas.height); + if (typeof options.onload === "function") + options.onload.call(this); + return this; + }; + + Layer.prototype._setImageSmoothing = function (context, options) { + if (typeof options.imageSmoothingEnabled === "undefined") + options.imageSmoothingEnabled = true; + context.mozImageSmoothingEnabled = options.imageSmoothingEnabled; + context.webkitImageSmoothingEnabled = options.imageSmoothingEnabled; + context.msImageSmoothingEnabled = options.imageSmoothingEnabled; + context.imageSmoothingEnabled = options.imageSmoothingEnabled; + }; + + Layer.prototype.copy = function (source) { + source.render(); + this.fromCanvas(source.canvas); + return this; + }; + + Layer.prototype.process = function (callback) { + if (typeof callback !== "function") + throw "Invalid callback"; + callback.call(this, this.imageData); + return this.render(); + }; + + Layer.prototype.render = function () { + if (this.imageData) + this.canvas.getContext("2d").putImageData(this.imageData, 0, 0); + return this; + }; + + Layer.prototype.setAlpha = function (alpha) { + var data = this.imageData.data; + for (var i = 3; i < data.length; i += 4) + data[i] = alpha; + return this; + }; + + Layer.prototype.fill = function (rgba) { + var data = this.imageData.data; + for (var i = 0; i < data.length; i += 4) + for (var j = 0; j < rgba.length; ++j) + data[i + j] = rgba[j]; + return this; + }; + + Layer.prototype.resize = function (width, height, options) { + options = options || {}; + var temporaryCanvas = document.createElement("canvas"), + tempoaryContext = temporaryCanvas.getContext("2d"); + temporaryCanvas.width = width; + temporaryCanvas.height = height; + tempoaryContext.drawImage(this.canvas, 0, 0, width, height); + this.canvas.width = width; + this.canvas.height = height; + var context = this.canvas.getContext("2d"); + this._setImageSmoothing(context, options); + context.drawImage(temporaryCanvas, 0, 0); + this.imageData = context.getImageData(0, 0, width, height); + return this; + }; + + Layer.prototype.applyColormap = function (colormap, grayscale) { + var data = this.imageData.data; + if (typeof grayscale === "undefined") grayscale = true; + for (var i = 0; i < data.length; i += 4) { + var index = data[i]; + if (!grayscale) + index |= (data[i + 1] << 8) | (data[i + 2] << 16); + data[i + 0] = colormap[index][0]; + data[i + 1] = colormap[index][1]; + data[i + 2] = colormap[index][2]; + } + return this; + }; + + Layer.prototype.computeEdgemap = function (options) { + if (typeof options === "undefined") options = {}; + var data = this.imageData.data, + width = this.imageData.width, + height = this.imageData.height, + edgeMap = new Uint8Array(this.imageData.data), + foreground = options.foreground || [255, 255, 255], + background = options.background || [0, 0, 0], + i, j, k, c = {}; + for (i = 0; i < height; ++i) { + for (j = 0; j < width; ++j) { + var offset = 4 * (i * width + j), + index = data[4 * (i * width + j)], + isBoundary = (i === 0 || + j === 0 || + i === (height - 1) || + j === (width - 1) || + index !== data[4 * (i * width + j - 1)] || + index !== data[4 * (i * width + j + 1)] || + index !== data[4 * ((i - 1) * width + j)] || + index !== data[4 * ((i + 1) * width + j)]); + if (isBoundary) { + for (k = 0; k < foreground.length; ++k) + edgeMap[offset + k] = foreground[k]; + } + else { + for (k = 0; k < background.length; ++k) + edgeMap[offset + k] = background[k]; + } + } + } + data.set(edgeMap); + return this; + }; + + Layer.prototype.gray2index = function (options) { + var data = this.imageData.data; + for (var i = 0; i < data.length; i += 4) { + data[i + 1] = 0; + data[i + 2] = 0; + } + return this; + }; + + return Layer; +}); diff --git a/js/image/morph.js b/js/image/morph.js new file mode 100644 index 0000000..18eb151 --- /dev/null +++ b/js/image/morph.js @@ -0,0 +1,40 @@ +/** Image morphology operations and index image I/O. + * + * Copyright 2015 Kota Yamaguchi + */ +define(["./morph/max-filter"], +function (maxFilter) { + function decodeIndexImage(imageData) { + var indexImage = { + width: imageData.width, + height: imageData.height, + data: new Int32Array(imageData.width * imageData.height) + }; + for (var i = 0; i < imageData.data.length; ++i) { + var offset = 4 * i; + indexImage.data[i] = (imageData.data[offset + 0]) | + (imageData.data[offset + 1] << 8) | + (imageData.data[offset + 2] << 16); + } + return indexImage; + } + + function encodeIndexImage(indexImage) { + var imageData = new ImageData(indexImage.width, indexImage.height); + for (var i = 0; i < indexImage.length; ++i) { + var offset = 4 * i, + value = indexImage.data[i]; + imageData.data[offset] = 255 & value; + imageData.data[offset + 1] = 255 & (value >>> 8); + imageData.data[offset + 2] = 255 & (value >>> 16); + imageData.data[offset + 3] = 255; + } + return imageData; + } + + return { + encodeIndexImage: encodeIndexImage, + decodeIndexImage: decodeIndexImage, + maxFilter: maxFilter + }; +}); diff --git a/js/image/morph/max-filter.js b/js/image/morph/max-filter.js new file mode 100644 index 0000000..fc1504f --- /dev/null +++ b/js/image/morph/max-filter.js @@ -0,0 +1,49 @@ +/** Max filter for an index image. + * + * Copyright 2015 Kota Yamaguchi + */ +define(["./neighbor-map"], function (NeighborMap) { + function findDominantLabel(data, neighbors) { + var histogram = {}, + i, label; + for (i = 0; i < neighbors.length; ++i) { + label = data[neighbors[i]]; + if (histogram[label]) + ++histogram[label]; + else + histogram[label] = 1; + } + var labels = Object.keys(histogram), + count = 0, + dominantLabel = null; + for (i = 0; i < labels.length; ++i) { + label = labels[i]; + if (histogram[label] > count) { + dominantLabel = parseInt(label, 10); + count = histogram[label]; + } + } + return dominantLabel; + } + + function maxFilter(indexImage, options) { + options = options || {}; + var neighbors = options.neighbors || [[-1, -1], [-1, 0], [-1, 1], + [ 0, -1], [ 0, 0], [ 0, 1], + [ 1, -1], [ 1, 0], [ 1, 1]], + result = new Int32Array(indexImage.data.length), + neighborMap = new NeighborMap(indexImage.width, + indexImage.height, + neighbors); + for (var i = 0; i < indexImage.data.length; ++i) + result[i] = findDominantLabel(indexImage.data, + neighborMap.get(i)); + return { + width: indexImage.width, + height: indexImage.height, + data: result + }; + } + + return maxFilter; +}); diff --git a/js/image/morph/neighbor-map.js b/js/image/morph/neighbor-map.js new file mode 100644 index 0000000..eaa5d64 --- /dev/null +++ b/js/image/morph/neighbor-map.js @@ -0,0 +1,47 @@ +/** Create a map of neighbor offsets. + * + * var neighborMap = new NeighborMap(width, height); + * for (var i = 0; i < data.length; ++i) { + * var neighbors = neighborMap.get(i); + * for (var j = 0; j < neighbors.length; ++j) { + * var pixel = data[neighbors[j]]; + * } + * } + * + * Copyright 2015 Kota Yamaguchi + */ +define(function () { + // Neighbor Map. + function NeighborMap(width, height, neighbors) { + this.neighbors = neighbors || [[-1, -1], [-1, 0], [-1, 1], + [ 0, -1], [ 0, 1], + [ 1, -1], [ 1, 0], [ 1, 1]]; + this.maps = []; + for (var k = 0; k < this.neighbors.length; ++k) { + var dy = this.neighbors[k][0], + dx = this.neighbors[k][1], + map = new Int32Array(width * height); + for (var y = 0; y < height; ++y) { + for (var x = 0; x < width; ++x) { + var Y = y + dy, + X = x + dx; + map[y * width + x] = (Y < 0 || height <= Y || X < 0 || width <= X) ? + -1 : Y * width + X; + } + } + this.maps.push(map); + } + } + + NeighborMap.prototype.get = function (offset) { + var neighborOffsets = []; + for (var k = 0; k < this.neighbors.length; ++k) { + var neighborOffset = this.maps[k][offset]; + if (neighborOffset >= 0) + neighborOffsets.push(neighborOffset); + } + return neighborOffsets; + }; + + return NeighborMap; +}); diff --git a/js/image/segmentation.js b/js/image/segmentation.js new file mode 100644 index 0000000..84d12c5 --- /dev/null +++ b/js/image/segmentation.js @@ -0,0 +1,32 @@ +/** Image segmentation factory. + * + * var segm = segmentation.create(imageData); + * var segmentData = segm.result; // imageData with numSegments. + * + * segm.finer(); + * segm.coarser(); + * + * Copyright 2015 Kota Yamaguchi + */ +define(["./segmentation/pff", + "./segmentation/slic", + "./segmentation/slico", + "./segmentation/watershed"], +function (pff, slic, slico, watershed) { + var methods = { + pff: pff, + slic: slic, + slico: slico, + watershed: watershed + }; + + methods.create = function (imageData, options) { + options = options || {}; + options.method = options.method || "slic"; + if (!methods[options.method]) + throw "Invalid method: " + options.method; + return new methods[options.method](imageData, options); + }; + + return methods; +}); diff --git a/js/image/segmentation/base.js b/js/image/segmentation/base.js new file mode 100644 index 0000000..5e2c42c --- /dev/null +++ b/js/image/segmentation/base.js @@ -0,0 +1,19 @@ +/** + * Base class for over-segmentation algorithms. + * + * Copyright 2015 Kota Yamaguchi + */ +define(function () { + function BaseSegmentation(imageData, options) { + if (!imageData instanceof ImageData) + throw "Invalid ImageData"; + this.imageData = new ImageData(imageData.width, imageData.height); + this.imageData.data.set(imageData.data); + } + + BaseSegmentation.prototype.finer = function () {}; + + BaseSegmentation.prototype.coarser = function () {}; + + return BaseSegmentation; +}); diff --git a/js/image/segmentation/binary-heap-priority-queue.js b/js/image/segmentation/binary-heap-priority-queue.js new file mode 100644 index 0000000..2075906 --- /dev/null +++ b/js/image/segmentation/binary-heap-priority-queue.js @@ -0,0 +1,90 @@ +/** Priority queue based on binary heap. + * + * Example: Basic usage. + * + * var queue = new PriorityQueue(); + * queue.push(1); + * queue.push(2); + * queue.push(0); + * var x = queue.shift(); // returns 0 + * + * Example: By descending order. + * + * var queue = new PriorityQueue({ + * comparator: function (a, b) { return b - a; } + * }); + * + * Copyright 2015 Kota Yamaguchi + */ +define([], function () { + function BinaryHeapPriorityQueue(options) { + options = options || {}; + this.comparator = options.comparator || function (a, b) { return a - b; }; + this.data = (options.initialValues) ? options.initialValues.slice(0) : []; + this.length = this.data.length; + if (this.data.length > 0) + for (var i = 1; i <= data.length; ++i) + this._bubbleUp(i); + } + + BinaryHeapPriorityQueue.prototype.push = function (value) { + this.data.push(value); + this.length = this.data.length; + this._bubbleUp(this.data.length - 1); + return this; + }; + + BinaryHeapPriorityQueue.prototype.shift = function () { + var value = this.data[0], + last = this.data.pop(); + this.length = this.data.length; + if (this.length > 0) { + this.data[0] = last; + this._bubbleDown(0); + } + return value; + }; + + BinaryHeapPriorityQueue.prototype.peek = function () { + return this.data[0]; + }; + + BinaryHeapPriorityQueue.prototype._bubbleUp = function (i) { + while (i > 0) { + var parent = (i - 1) >>> 1; + if (this.comparator(this.data[i], this.data[parent]) < 0) { + var value = this.data[parent]; + this.data[parent] = this.data[i]; + this.data[i] = value; + i = parent; + } + else + break; + } + }; + + BinaryHeapPriorityQueue.prototype._bubbleDown = function (i) { + var last = this.data.length - 1; + while (true) { + var left = (i << 1) + 1, + right = left + 1, + minIndex = i; + if (left <= last && + this.comparator(this.data[left], this.data[minIndex]) < 0) + minIndex = left; + if (right <= last && + this.comparator(this.data[right], this.data[minIndex]) < 0) + minIndex = right; + if (minIndex !== i) { + var value = this.data[minIndex]; + this.data[minIndex] = this.data[i]; + this.data[i] = value; + i = minIndex; + } + else + break; + } + }; + + return BinaryHeapPriorityQueue; +}); diff --git a/pf-segmentation.js b/js/image/segmentation/pff.js similarity index 61% rename from pf-segmentation.js rename to js/image/segmentation/pff.js index 7637ecc..e0c6a29 100644 --- a/pf-segmentation.js +++ b/js/image/segmentation/pff.js @@ -8,69 +8,55 @@ * API * --- * - * PFSegmentation(imageURL, options) + * new PFF(imageData, options) * * The function takes the following options. * * `sigma` - Parameter for Gaussian pre-smoothing. Default 0.5. * * `threshold` - Threshold value of the algorithm. Default 500. * * `minSize` - Minimum segment size in pixels. Default 20. - * * `toDataURL` - callback function to receive the result as a data URL. - * * `callback` - function to be called on finish. The function takes a single - * argument of result object that contains following fields. - * * `width` - Width of the image in pixels. - * * `height` - Height of the image in pixels. - * * `size` - Number of segments. - * * `indexMap` - Int32Array of `width * height` elements containing - * segment index for each pixel location. The segment index - * at pixel `(i, j)` is `indexMap(i * width + j)`, where - * `i` is the y coordinate of the pixel and `j` is the x - * coordinate. * - * Example - * ------- - * - * Drawing the result to a canvas. - * - * function colorRandomRGB(size, indexMap, imageData) { - * var width = imageData.width; - * var height = imageData.height; - * var rgbData = imageData.data; - * var colormap = new Uint8Array(size * 3); - * for (var i = 0; i < colormap.length; ++i) - * colormap[i] = Math.round(255 * Math.random()); - * for (var i = 0; i < height; ++i) { - * for (var j = 0; j < width; ++j) { - * var index = indexMap[i * width + j]; - * rgbData[4 * (i * width + j) + 0] = colormap[3 * index + 0]; - * rgbData[4 * (i * width + j) + 1] = colormap[3 * index + 1]; - * rgbData[4 * (i * width + j) + 2] = colormap[3 * index + 2]; - * rgbData[4 * (i * width + j) + 3] = 255; - * } - * } - * } - * - * PFSegmentation('/path/to/image.jpg', { - * sigma: 1.0, - * threshold: 500, - * minSize: 100, - * callback: function(result) { - * var canvas = document.createElement('canvas'); - * canvas.width = result.width; - * canvas.height = result.height; - * var context = canvas.getContext('2d'); - * var imageData = context.getImageData(0, - * 0, - * canvas.width, - * canvas.height); - * colorRandomRGB(result.size, result.indexMap, imageData); - * context.putImageData(imageData, 0, 0); - * document.body.appendChild(canvas); - * } - * }); - * - * Kota Yamaguchi 2013. + * Copyright 2015 Kota Yamaguchi */ -(function() { +define(["./base"], function(BaseSegmentation) { + function PFF(imageData, options) { + BaseSegmentation.call(this, imageData, options); + options = options || {}; + this.sigma = options.sigma || Math.sqrt(2.0); + this.threshold = options.threshold || 500; + this.minSize = options.minSize || 20; + this.result = this._compute(); + } + + PFF.prototype = Object.create(BaseSegmentation.prototype); + + // Compute segmentation. + PFF.prototype._compute = function () { + var smoothedImage = new ImageData(this.imageData.width, + this.imageData.height); + smoothedImage.data.set(this.imageData.data); + smoothImage(smoothedImage, this.sigma); + var universe = segmentGraph(smoothedImage, this.threshold, this.minSize), + indexMap = createIndexMap(universe, smoothedImage), + result = new ImageData(smoothedImage.width, smoothedImage.height); + encodeLabels(indexMap, result.data); + result.numSegments = universe.nodes; + return result; + }; + + // Finer. + PFF.prototype.finer = function (scale) { + this.sigma /= (scale || Math.sqrt(2)); + this.threshold /= (scale || Math.sqrt(2)); + this.result = this._compute(); + }; + + // Coarser. + PFF.prototype.coarser = function (scale) { + this.sigma *= (scale || Math.sqrt(2.0)); + this.threshold *= (scale || Math.sqrt(2.0)); + this.result = this._compute(); + }; + // Create a normalized Gaussian filter. function createGaussian(sigma) { sigma = Math.max(sigma, 0.01); @@ -118,7 +104,7 @@ // Vertical filter. for (i = 0; i < height; ++i) { for (j = 0; j < width; ++j) { - for (k = 0; k < Math.min(4, 3); ++k) { + for (k = 0; k < 3; ++k) { sum = filter[0] * temporary[4 * (i * width + j) + k]; for (l = 1; l < filter.length; ++l) { sum += filter[l] * ( @@ -139,19 +125,19 @@ } // Create an edge structure. - function createEdges(imageData, options) { + function createEdges(imageData) { var width = imageData.width, height = imageData.height, rgbData = imageData.data, edgeSize = 4 * width * height - 3 * width - 3 * height + 2, index = 0, edges = { - a: new Int32Array(edgeSize), - b: new Int32Array(edgeSize), - w: new Float32Array(edgeSize) - }, - x1, - x2; + a: new Int32Array(edgeSize), + b: new Int32Array(edgeSize), + w: new Float32Array(edgeSize) + }, + x1, + x2; for (var i = 0; i < height; ++i) { for (var j = 0; j < width; ++j) { if (j < width - 1) { @@ -280,13 +266,9 @@ } // Segment a graph. - function segmentGraph(imageData, options) { - var c = options.threshold, - minSize = options.minSize, - edges = createEdges(imageData, options), - a, - b, - i; + function segmentGraph(imageData, c, minSize) { + var edges = createEdges(imageData), + a, b, i; sortEdgesByWeights(edges); var universe = createUniverse(imageData.width * imageData.height, c); // Bottom-up merge. @@ -313,7 +295,7 @@ } // Create an index map. - function createIndexMap(universe, imageData, options) { + function createIndexMap(universe, imageData) { var width = imageData.width, height = imageData.height, indexMap = new Int32Array(width * height), @@ -333,69 +315,15 @@ return indexMap; } - // Compute segmentation. - function computeSegmentation(imageData, options) { - smoothImage(imageData, options.sigma); - var universe = segmentGraph(imageData, options), - indexMap = createIndexMap(universe, imageData, options); - if (options.callback) { - var rgbData = new Uint8Array(imageData.data); - options.callback({ - width: imageData.width, - height: imageData.height, - size: universe.nodes, - indexMap: indexMap, - rgbData: rgbData - }); - } - if (options.toDataURL) - getDataURL(imageData.width, imageData.height, indexMap, options); - } - - // Convert to Data URL. - function getDataURL(width, height, indexMap, options) { - var canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - var context = canvas.getContext('2d'), - imageData = context.createImageData(width, height), - data = imageData.data; - for (var i = 0; i < indexMap.length; ++i) { + function encodeLabels(indexMap, data) { + for (i = 0; i < indexMap.length; ++i) { var value = indexMap[i]; data[4 * i + 0] = value & 255; data[4 * i + 1] = (value >>> 8) & 255; data[4 * i + 2] = (value >>> 16) & 255; + data[4 * i + 3] = 255; } - context.putImageData(imageData, 0, 0); - options.toDataURL(canvas.toDataURL()); - } - - // When image is loaded. - function onSuccessImageLoad(image, options) { - var canvas = document.createElement('canvas'); - canvas.width = image.width; - canvas.height = image.height; - var context = canvas.getContext('2d'); - context.drawImage(image, 0, 0); - var imageData = context.getImageData(0, 0, image.width, image.height), - segmentation = computeSegmentation(imageData, options); } - // When image is invalid. - function onErrorImageLoad() { - alert('Failed to load an image: ' + image.src); - } - - // Public API. - window.PFSegmentation = function(imageURL, options) { - if (typeof options === 'undefined') options = {}; - if (options.sigma === undefined) options.sigma = 0.5; - if (options.threshold === undefined) options.threshold = 500; - if (options.minSize === undefined) options.minSize = 20; - var image = new Image(); - image.src = imageURL; - image.crossOrigin = null; - image.onerror = function() { onErrorImageLoad(image); }; - image.onload = function() { onSuccessImageLoad(image, options); }; - }; -}).call(this); + return PFF; +}); diff --git a/slic-segmentation.js b/js/image/segmentation/slic.js similarity index 72% rename from slic-segmentation.js rename to js/image/segmentation/slic.js index 5f95604..1f87e77 100644 --- a/slic-segmentation.js +++ b/js/image/segmentation/slic.js @@ -12,39 +12,65 @@ * API * --- * - * SLICSegmentation(imageURL, options) + * SLIC(imageURL, options) * * The function takes the following options. * * `regionSize` - Parameter of superpixel size - * * `regularization` - Regularization parameter. See paper. * * `minRegionSize` - Minimum segment size in pixels. - * * `toDataURL` - Callback function to receive the result as a data URL. - * * `callback` - Function to be called on finish. The function takes a single - * argument of result object that contains following fields. - * * `width` - Width of the image in pixels. - * * `height` - Height of the image in pixels. - * * `size` - Number of segments. - * * `indexMap` - Int32Array of `width * height` elements containing - * segment index for each pixel location. The segment index - * at pixel `(i, j)` is `indexMap(i * width + j)`, where - * `i` is the y coordinate of the pixel and `j` is the x - * coordinate. * - * LongLong Yu 2014. + * Copyright 2014 LongLong Yu. */ -(function() { +define(["./base"], function(BaseSegmentation) { + // SLIC segmentation. + function SLIC(imageData, options) { + BaseSegmentation.call(this, imageData, options); + options = options || {}; + this.regionSize = options.regionSize || 16; + this.minRegionSize = options.minRegionSize || + Math.round(this.regionSize * 0.8); + this.maxIterations = options.maxIterations || 10; + this._compute(); + } + + SLIC.prototype = Object.create(BaseSegmentation.prototype); + + SLIC.prototype.finer = function () { + var newSize = Math.max(5, Math.round(this.regionSize / Math.sqrt(2.0))); + if (newSize !== this.regionSize) { + this.regionSize = newSize; + this.minRegionSize = Math.round(newSize * 0.8); + this._compute(); + } + }; + + SLIC.prototype.coarser = function () { + var newSize = Math.min(640, Math.round(this.regionSize * Math.sqrt(2.0))); + if (newSize !== this.regionSize) { + this.regionSize = newSize; + this.minRegionSize = Math.round(newSize * 0.8); + this._compute(); + } + }; + + SLIC.prototype._compute = function () { + this.result = computeSLICSegmentation(this.imageData, + this.regionSize, + this.minRegionSize, + this.maxIterations); + }; + // Convert RGBA into XYZ color space. rgba: Red Green Blue Alpha. function rgb2xyz(rgba, w, h) { var xyz = new Float32Array(3*w*h), gamma = 2.2; for (var i = 0; i>> 8) & 255; + data[4 * i + 2] = (value >>> 16) & 255; + data[4 * i + 3] = 255; + } + } + // Compute SLIC Segmentation. - function computeSLICSegmentation(imageData, options) { - var imWidth = imageData.width, + function computeSLICSegmentation(imageData, + regionSize, + minRegionSize, + maxIterations) { + var i, + imWidth = imageData.width, imHeight = imageData.height, - numRegionsX = parseInt(imWidth / options.regionSize, 10), - numRegionsY = parseInt(imHeight / options.regionSize, 10), - numRegions = parseInt(numRegionsX * numRegionsY, 10), - numPixels = parseInt(imWidth * imHeight, 10), - regionSize = options.regionSize, + numRegionsX = Math.floor(imWidth / regionSize), + numRegionsY = Math.floor(imHeight / regionSize), + numRegions = Math.floor(numRegionsX * numRegionsY), + numPixels = Math.floor(imWidth * imHeight), edgeMap = new Float32Array(numPixels), masses = new Array(numPixels), // 2 (geometric: x & y) and 3 (RGB or Lab) @@ -343,13 +396,10 @@ mcMap = new Float32Array(numPixels), msMap = new Float32Array(numPixels), distanceMap = new Float32Array(numPixels), - labData = xyz2lab(rgb2xyz(imageData.data, - imageData.width, - imageData.height), - imageData.width, - imageData.height); + xyzData = rgb2xyz(imageData.data, imWidth, imHeight), + labData = xyz2lab(xyzData, imWidth, imHeight); // Compute edge. - computeEdge(labData, edgeMap, imageData.width, imageData.height); + computeEdge(labData, edgeMap, imWidth, imHeight); // Initialize K-Means Centers. initializeKmeansCenters(labData, edgeMap, @@ -358,14 +408,13 @@ numRegionsX, numRegionsY, regionSize, - imageData.width, - imageData.height); - var maxNumIterations = 10, - segmentation = new Int32Array(numPixels); + imWidth, + imHeight); + var segmentation = new Int32Array(numPixels); /** SLICO implementation: "SLIC Superpixels Compared to State-of-the-art * Superpixel Methods" */ - for (var iter =0; iter < maxNumIterations; ++iter) { + for (var iter = 0; iter < maxIterations; ++iter) { // Do assignment. assignSuperpixelLabel(labData, segmentation, @@ -377,12 +426,11 @@ numRegionsX, numRegionsY, regionSize, - imageData.width, - imageData.height); + imWidth, + imHeight); // Update maximum spatial and color distances [1]. updateClusterParams(segmentation, mcMap, msMap, clusterParams); // Compute new centers. - var i; for (i = 0; i < masses.length; ++i) masses[i] = 0; for (i = 0; i < newCenters.length; ++i) @@ -392,8 +440,8 @@ masses, newCenters, numRegions, - imageData.width, - imageData.height); + imWidth, + imHeight); // Compute residual error of assignment. var error = computeResidualError(currentCenters, newCenters); if (error < 1e-5) @@ -402,90 +450,23 @@ currentCenters[i] = newCenters[i]; } eliminateSmallRegions(segmentation, - options.minRegionSize, + minRegionSize, numPixels, - imageData.width, - imageData.height); - return segmentation; + imWidth, + imHeight); + // Refresh the canvas. + var result = new ImageData(imWidth, imHeight); + result.numSegments = remapLabels(segmentation); + encodeLabels(segmentation, result.data); + return result; } - // Remap label indices. - function remapLabels(segmentation) { - var map = {}, - index = 0; - for (var i = 0; i < segmentation.length; ++i) { - var label = segmentation[i]; - if (map[label] === undefined) - map[label] = index++; - segmentation[i] = map[label]; - } - return index; - } + // function max(array) { + // var value = array[0]; + // for (var i = 0; i < array.length; ++i) + // value = Math.max(array[i], value); + // return value; + // } - // Compute segmentation. - function computeSegmentation(imageData, options) { - var segmentation = computeSLICSegmentation(imageData, options), - numSegments = remapLabels(segmentation); - if (options.callback) { - var rgbData = new Uint8Array(imageData.data); - options.callback({ - width: imageData.width, - height: imageData.height, - size: numSegments, - indexMap: segmentation, - rgbData: rgbData - }); - } - if (options.toDataURL) - getDataURL(imageData.width, imageData.height, indexMap, options); - } - - // Convert to Data URL. - function getDataURL(width, height, indexMap, options) { - var canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - var context = canvas.getContext('2d'), - imageData = context.createImageData(width, height), - data = imageData.data; - for (var i = 0; i < indexMap.length; ++i) { - var value = indexMap[i]; - data[4 * i + 0] = value & 255; - data[4 * i + 1] = (value >>> 8) & 255; - data[4 * i + 2] = (value >>> 16) & 255; - } - context.putImageData(imageData, 0, 0); - options.toDataURL(canvas.toDataURL()); - } - - // When image is loaded. - function onSuccessImageLoad(image, options) { - var canvas = document.createElement('canvas'); - canvas.width = image.width; - canvas.height = image.height; - var context = canvas.getContext('2d'); - context.drawImage(image, 0, 0); - var imageData = context.getImageData(0, 0, image.width, image.height), - segmentation = computeSegmentation(imageData, options); - } - - // When image is invalid. - function onErrorImageLoad() { - alert('Failed to load an image: ' + image.src); - } - - // Public API. - window.SLICSegmentation = function(imageURL, options) { - if (typeof options === 'undefined') options = {}; - // the lateral side of a rectangle superpixel in pixels. - if (options.regionSize === undefined) options.regionSize = 40; - // width or high should be larger than 20 pixels - if (options.minRegionSize === undefined) - options.minRegionSize = options.regionSize * options.regionSize / 4; - var image = new Image(); - image.src = imageURL; - image.crossOrigin = null; - image.onerror = function() { onErrorImageLoad(image); }; - image.onload = function() { onSuccessImageLoad(image, options); }; - }; -}).call(this); + return SLIC; +}); diff --git a/js/image/segmentation/slico.js b/js/image/segmentation/slico.js new file mode 100644 index 0000000..fbe9b85 --- /dev/null +++ b/js/image/segmentation/slico.js @@ -0,0 +1,507 @@ +/** SLICO segmentation implementation. + * + * SLIC Superpixels + * Radhakrishna Achanta, Appu Shaji, Kevin Smith, Aurelien Lucchi, Pascal + * Fua, and Sabine Süsstrunk + * IEEE Transactions on Pattern Analysis and Machine Intelligence, vol. 34, + * num. 11, p. 2274 - 2282, May 2012. + * + * http://ivrl.epfl.ch/research/superpixels + * + * Copyright 2015 Kota Yamaguchi + */ +define(["./base"], function(BaseSegmentation) { + function SLICO(imageData, options) { + BaseSegmentation.call(this, imageData, options); + this.width = this.imageData.width; + this.height = this.imageData.height; + options = options || {}; + this.method = options.method || "FixedK"; + this.perturb = (typeof options.perturb === "undefined") ? + true : options.perturb; + this.maxIterations = options.maxIterations || 10; + this.K = options.K || 1024; + this.step = options.step || 200; + this.enforceConnectivity = (options.enforceConnectivity === false) ? + false : true; + this._compute(); + } + + SLICO.prototype = Object.create(BaseSegmentation.prototype); + + SLICO.prototype.finer = function () { + var newK = Math.min(8962, Math.round(this.K * (2.0))); + if (newK !== this.K) { + this.K = newK; + this._compute(); + } + }; + + SLICO.prototype.coarser = function () { + var newK = Math.max(16, Math.round(this.K / (2.0))); + if (newK !== this.K) { + this.K = newK; + this._compute(); + } + }; + + SLICO.prototype._compute = function () { + var labels = (this.method === "FixedK") ? + this.performSLICOForGivenK() : this.performSLICOForGivenStepSize(); + var result = new ImageData(this.width, this.height); + result.numSegments = remapLabels(labels); + encodeLabels(labels, result.data); + this.result = result; + }; + + // sRGB (D65 illuninant assumption) to XYZ conversion. + SLICO.prototype.rgb2xyz = function (sRGB) { + var R = parseInt(sRGB[0], 10) / 255.0, + G = parseInt(sRGB[1], 10) / 255.0, + B = parseInt(sRGB[2], 10) / 255.0, + r = (R <= 0.04045) ? R / 12.92 : Math.pow((R + 0.055) / 1.055, 2.4), + g = (G <= 0.04045) ? G / 12.92 : Math.pow((R + 0.055) / 1.055, 2.4), + b = (B <= 0.04045) ? B / 12.92 : Math.pow((R + 0.055) / 1.055, 2.4); + return [ + r * 0.4124564 + g * 0.3575761 + b * 0.1804375, + r * 0.2126729 + g * 0.7151522 + b * 0.0721750, + r * 0.0193339 + g * 0.1191920 + b * 0.9503041 + ]; + }; + + // sRGB to Lab conversion. + SLICO.prototype.rgb2lab = function (sRGB) { + var epsilon = 0.008856, //actual CIE standard + kappa = 903.3, //actual CIE standard + Xr = 0.950456, //reference white + Yr = 1.0, //reference white + Zr = 1.088754, //reference white + xyz = this.rgb2xyz(sRGB), + xr = xyz[0] / Xr, + yr = xyz[1] / Yr, + zr = xyz[2] / Zr, + fx = (xr > epsilon) ? + Math.pow(xr, 1.0/3.0) : (kappa * xr + 16.0) / 116.0, + fy = (yr > epsilon) ? + Math.pow(yr, 1.0/3.0) : (kappa * yr + 16.0) / 116.0, + fz = (zr > epsilon) ? + Math.pow(zr, 1.0/3.0) : (kappa * zr + 16.0) / 116.0; + return [ + 116.0 * fy - 16.0, + 500.0 * (fx - fy), + 200.0 * (fy - fz) + ]; + }; + + SLICO.prototype.doRGBtoLABConversion = function (imageData) { + var size = this.width * this.height, + data = imageData.data; + this.lvec = new Float64Array(size); + this.avec = new Float64Array(size); + this.bvec = new Float64Array(size); + for (var j = 0; j < size; ++j) { + var r = data[4 * j + 0], + g = data[4 * j + 1], + b = data[4 * j + 2]; + var lab = this.rgb2lab([r, g, b]); + this.lvec[j] = lab[0]; + this.avec[j] = lab[1]; + this.bvec[j] = lab[2]; + } + }; + + SLICO.prototype.detectLabEdges = function () { + var w = this.width; + this.edges = fillArray(new Float64Array(this.width * this.height), 0); + for (var j = 1; j < this.height - 1; ++j) { + for (var k = 1; k < this.width - 1; ++k) { + var i = parseInt(j * this.width + k, 10), + dx = Math.pow(this.lvec[i - 1] - this.lvec[i + 1], 2) + + Math.pow(this.avec[i - 1] - this.avec[i + 1], 2) + + Math.pow(this.bvec[i - 1] - this.bvec[i + 1], 2), + dy = Math.pow(this.lvec[i - w] - this.lvec[i + w], 2) + + Math.pow(this.avec[i - w] - this.avec[i + w], 2) + + Math.pow(this.bvec[i - w] - this.bvec[i + w], 2); + this.edges[i] = dx + dy; + } + } + }; + + SLICO.prototype.perturbSeeds = function () { + var dx8 = [-1, -1, 0, 1, 1, 1, 0, -1], + dy8 = [ 0, -1, -1, -1, 0, 1, 1, 1], + numSeeds = this.kSeedsL.length; + for (var n = 0; n < numSeeds; ++n) { + var ox = parseInt(this.kSeedsX[n], 10), //original x + oy = parseInt(this.kSeedsY[n], 10), //original y + oind = parseInt(oy * this.width + ox, 10), + storeind = parseInt(oind, 10); + for (var i = 0; i < 8; ++i) { + var nx = parseInt(ox + dx8[i], 10); //new x + var ny = parseInt(oy + dy8[i], 10); //new y + if (nx >= 0 && nx < this.width && ny >= 0 && ny < this.height) { + var nind = parseInt(ny * this.width + nx, 10); + if (this.edges[nind] < this.edges[storeind]) + storeind = nind; + } + } + if (storeind != oind) { + this.kSeedsX[n] = Math.floor(storeind % this.width); + this.kSeedsY[n] = Math.floor(storeind / this.width); + this.kSeedsL[n] = this.lvec[storeind]; + this.kSeedsA[n] = this.avec[storeind]; + this.kSeedsB[n] = this.bvec[storeind]; + } + } + }; + + SLICO.prototype.getLABXYSeedsForGivenStepSize = function(step, perturb) { + var n = 0, + xstrips = Math.round(0.5 + parseFloat(this.width) / parseFloat(step)), + ystrips = Math.round(0.5 + parseFloat(this.height) / parseFloat(step)), + xerr = Math.round(this.width - step * xstrips), + yerr = Math.round(this.height - step * ystrips), + xerrperstrip = parseFloat(xerr) / parseFloat(xstrips), + yerrperstrip = parseFloat(yerr) / parseFloat(ystrips), + xoff = Math.floor(step / 2), + yoff = Math.floor(step / 2), + numSeeds = xstrips * ystrips; + this.kSeedsL = new Float64Array(numSeeds); + this.kSeedsA = new Float64Array(numSeeds); + this.kSeedsB = new Float64Array(numSeeds); + this.kSeedsX = new Float64Array(numSeeds); + this.kSeedsY = new Float64Array(numSeeds); + for (var y = 0; y < ystrips; ++y) { + var ye = Math.floor(y * yerrperstrip); + for (var x = 0; x < xstrips; ++x) { + var xe = Math.floor(x * xerrperstrip); + var i = Math.floor((y * step + yoff + ye) * this.width + + (x * step + xoff + xe)); + this.kSeedsL[n] = this.lvec[i]; + this.kSeedsA[n] = this.avec[i]; + this.kSeedsB[n] = this.bvec[i]; + this.kSeedsX[n] = (x * step + xoff + xe); + this.kSeedsY[n] = (y * step + yoff + ye); + ++n; + } + } + if (perturb) + this.perturbSeeds(); + }; + + SLICO.prototype.getLABXYSeedsForGivenK = function(K, perturb) { + var size = Math.floor(this.width * this.height); + var step = Math.sqrt(parseFloat(size) / parseFloat(K)); + var T = Math.round(step); + var xoff = Math.round(step / 2); + var yoff = Math.round(step / 2); + var n = 0; + var r = 0; + this.kSeedsL = []; + this.kSeedsA = []; + this.kSeedsB = []; + this.kSeedsX = []; + this.kSeedsY = []; + for (var y = 0; y < this.height; ++y) { + var Y = Math.floor(y * step + yoff); + if (Y > this.height - 1) + break; + for (var x = 0; x < this.width; ++x) { + //var X = x*step + xoff; //square grid + var X = Math.floor(x * step + (xoff << (r & 0x1))); //hex grid + if (X > this.width - 1) + break; + var i = Math.floor(Y * this.width + X); + this.kSeedsL.push(this.lvec[i]); + this.kSeedsA.push(this.avec[i]); + this.kSeedsB.push(this.bvec[i]); + this.kSeedsX.push(X); + this.kSeedsY.push(Y); + ++n; + } + ++r; + } + if (perturb) + this.perturbSeeds(); + }; + + function fillArray(array, value) { + for (var i = 0; i < array.length; ++i) + array[i] = value; + return array; + } + + function findMinMax(data) { + var min = Infinity, max = -Infinity; + for (var i = 0; i < data.length; ++i) { + min = Math.min(min, data[i]); + max = Math.max(max, data[i]); + } + return [min, max]; + } + + function sum(data) { + var value = 0; + for (var i = 0; i < data.length; ++i) + value += data[i]; + return value; + } + + SLICO.prototype.performSuperpixelSegmentationVariableSandM = function ( + kLabels, + step, + maxIterations + ) { + var size = Math.floor(this.width * this.height), + numK = this.kSeedsL.length, + numIter = 0, + offset = Math.floor((step < 10) ? step * 1.5 : step), + sigmal = fillArray(new Float64Array(numK), 0), + sigmaa = fillArray(new Float64Array(numK), 0), + sigmab = fillArray(new Float64Array(numK), 0), + sigmax = fillArray(new Float64Array(numK), 0), + sigmay = fillArray(new Float64Array(numK), 0), + clusterSize = fillArray(new Int32Array(numK), 0), + distxy = fillArray(new Float64Array(size), Infinity), + distlab = fillArray(new Float64Array(size), Infinity), + distvec = fillArray(new Float64Array(size), Infinity), + maxlab = fillArray(new Float64Array(numK), Math.pow(10, 2)), + maxxy = fillArray(new Float64Array(numK), Math.pow(step, 2)), + invxywt = 1.0 / Math.pow(step, 2), + i, j, k, n, x, y; + while (numIter < maxIterations) { + ++numIter; + // Assign the closest cluster. + fillArray(distvec, Infinity); + for (n = 0; n < numK; ++n) { + var y1 = Math.floor(Math.max(0, this.kSeedsY[n] - offset)), + y2 = Math.floor(Math.min(this.height, this.kSeedsY[n] + offset)), + x1 = Math.floor(Math.max(0, this.kSeedsX[n] - offset)), + x2 = Math.floor(Math.min(this.width, this.kSeedsX[n] + offset)); + for (y = y1; y < y2; ++y) { + for (x = x1; x < x2; ++x) { + i = Math.floor(y * this.width + x); + if (!(y < this.height && x < this.width && y >= 0 && x >= 0)) + throw "Assertion error"; + var l = this.lvec[i], + a = this.avec[i], + b = this.bvec[i]; + distlab[i] = Math.pow(l - this.kSeedsL[n], 2) + + Math.pow(a - this.kSeedsA[n], 2) + + Math.pow(b - this.kSeedsB[n], 2); + distxy[i] = Math.pow(x - this.kSeedsX[n], 2) + + Math.pow(y - this.kSeedsY[n], 2); + // var dist = distlab[i] / maxlab[n] + distxy[i] * invxywt; + var dist = distlab[i] / maxlab[n] + distxy[i] / maxxy[n]; + if (dist < distvec[i]) { + distvec[i] = dist; + kLabels[i] = n; + } + } + } + } + //console.log("iter = " + numIter + ", sum_dist = " + sum(distvec)); + // Assign the max color distance for a cluster. + if (numIter === 0) { + fillArray(maxlab, 1); + fillArray(maxxy, 1); + } + for (i = 0; i < size; ++i) { + if (maxlab[kLabels[i]] < distlab[i]) + maxlab[kLabels[i]] = distlab[i]; + if (maxxy[kLabels[i]] < distxy[i]) + maxxy[kLabels[i]] = distxy[i]; + } + // Recalculate the centroid and store in the seed values. + fillArray(sigmal, 0); + fillArray(sigmaa, 0); + fillArray(sigmab, 0); + fillArray(sigmax, 0); + fillArray(sigmay, 0); + fillArray(clusterSize, 0); + for (j = 0; j < size; ++j) { + var temp = kLabels[j]; + if (kLabels[j] < 0) + throw "Assertion error"; + sigmal[kLabels[j]] += this.lvec[j]; + sigmaa[kLabels[j]] += this.avec[j]; + sigmab[kLabels[j]] += this.bvec[j]; + sigmax[kLabels[j]] += (j % this.width); + sigmay[kLabels[j]] += (j / this.width); + clusterSize[kLabels[j]]++; + } + for (k = 0; k < numK; ++k) { + if (clusterSize[k] <= 0) + clusterSize[k] = 1; + //computing inverse now to multiply, than divide later. + var inv = 1.0 / clusterSize[k]; + this.kSeedsL[k] = sigmal[k] * inv; + this.kSeedsA[k] = sigmaa[k] * inv; + this.kSeedsB[k] = sigmab[k] * inv; + this.kSeedsX[k] = sigmax[k] * inv; + this.kSeedsY[k] = sigmay[k] * inv; + } + } + }; + + SLICO.prototype.enforceLabelConnectivity = function (labels, nlabels, K) { + var dx4 = [-1, 0, 1, 0], + dy4 = [ 0, -1, 0, 1], + size = this.width * this.height, + SUPSZ = Math.floor(size / K), + c, n, x, y, nindex; + var label = 0, + xvec = new Int32Array(size), + yvec = new Int32Array(size), + oindex = 0, + adjlabel = 0; // adjacent label + for (var j = 0; j < this.height; ++j) { + for (var k = 0; k < this.width; ++k) { + if (nlabels[oindex] < 0) { + nlabels[oindex] = label; + // Start a new segment. + xvec[0] = k; + yvec[0] = j; + // Quickly find an adjacent label for use later if needed. + for (n = 0; n < 4; ++n) { + x = Math.floor(xvec[0] + dx4[n]); + y = Math.floor(yvec[0] + dy4[n]); + if ((x >= 0 && x < this.width) && (y >= 0 && y < this.height)) { + nindex = Math.floor(y * this.width + x); + if (nlabels[nindex] >= 0) + adjlabel = nlabels[nindex]; + } + } + var count = 1; + for (c = 0; c < count; ++c) { + for (n = 0; n < 4; ++n) { + x = Math.floor(xvec[c] + dx4[n]); + y = Math.floor(yvec[c] + dy4[n]); + if ((x >= 0 && x < this.width) && (y >= 0 && y < this.height)) { + nindex = Math.floor(y * this.width + x); + if (nlabels[nindex] < 0 && labels[oindex] == labels[nindex]) { + xvec[count] = x; + yvec[count] = y; + nlabels[nindex] = label; + ++count; + } + } + } + } + // If segment size is less then a limit, assign an + // adjacent label found before, and decrement label count. + if (count <= SUPSZ >> 2) { + for (c = 0; c < count; c++ ) { + var ind = Math.floor(yvec[c] * this.width + xvec[c]); + nlabels[ind] = adjlabel; + } + --label; + } + ++label; + } + ++oindex; + } + } + return label; + }; + + SLICO.prototype.performSLICOForGivenStepSize = function() { + var size = this.width * this.height, + kLabels = fillArray(new Int32Array(size), -1); + this.doRGBtoLABConversion(this.imageData); + if (this.perturb) + this.detectLabEdges(); + this.getLABXYSeedsForGivenStepSize(step, this.perturb); + this.performSuperpixelSegmentationVariableSandM(kLabels, + this.step, + this.maxIterations); + var numlabels = kLabels.length; + if (this.enforceConnectivity) { + var nlabels = fillArray(new Int32Array(size), -1); + numlabels = this.enforceLabelConnectivity(kLabels, + nlabels, + size / (step * step)); + for (var i = 0; i < size; ++i) + kLabels[i] = nlabels[i]; + } + return kLabels; + }; + + SLICO.prototype.performSLICOForGivenK = function() { + var size = this.width * this.height, + kLabels = fillArray(new Int32Array(size), -1); + this.doRGBtoLABConversion(this.imageData); + if (this.perturb) + this.detectLabEdges(); + this.getLABXYSeedsForGivenK(this.K, this.perturb); + var step = Math.sqrt(size / this.K) + 2.0; + this.performSuperpixelSegmentationVariableSandM(kLabels, + step, + this.maxIterations); + var numlabels = kLabels.length; + if (this.enforceConnectivity) { + var nlabels = fillArray(new Int32Array(size), -1); + numlabels = this.enforceLabelConnectivity(kLabels, nlabels, this.K); + for (var i = 0; i < size; ++i) + kLabels[i] = nlabels[i]; + } + return kLabels; + }; + + SLICO.prototype.drawContoursAroundSegments = function (result) { + var imageData = new ImageData(this.width, this.height), + data = fillArray(imageData.data, 255), + color = [255, 0, 0], + dx8 = [-1, -1, 0, 1, 1, 1, 0, -1], + dy8 = [ 0, -1, -1, -1, 0, 1, 1, 1]; + istaken = fillArray(new Uint8Array(this.width * this.height), 0); + var mainindex = 0; + for (var j = 0; j < this.height; ++j) { + for (var k = 0; k < this.width; ++k) { + var np = 0; + for (var i = 0; i < 8; ++i) { + var x = k + dx8[i], + y = j + dy8[i]; + if ((x >= 0 && x < this.width) && (y >= 0 && y < this.height)) { + var index = y * this.width + x; + if (istaken[index] === 0 && + result.labels[mainindex] !== result.labels[index]) + ++np; + } + } + if (np > 1) { + data[4 * mainindex + 0] = color[0]; + data[4 * mainindex + 1] = color[1]; + data[4 * mainindex + 2] = color[2]; + } + ++mainindex; + } + } + return imageData; + }; + + // Remap label indices. + function remapLabels(labels) { + var map = {}, + index = 0; + for (var i = 0; i < labels.length; ++i) { + var label = labels[i]; + if (map[label] === undefined) + map[label] = index++; + labels[i] = map[label]; + } + return index; + } + + function encodeLabels(labels, data) { + for (var i = 0; i < labels.length; ++i) { + var label = labels[i]; + data[4 * i + 0] = 255 & label; + data[4 * i + 1] = 255 & (label >> 8); + data[4 * i + 2] = 255 & (label >> 16); + data[4 * i + 3] = 255; + } + } + + return SLICO; +}); diff --git a/js/image/segmentation/watershed.js b/js/image/segmentation/watershed.js new file mode 100644 index 0000000..2312afe --- /dev/null +++ b/js/image/segmentation/watershed.js @@ -0,0 +1,322 @@ +/** + * Canny + Watershed segmentation algorithm. + * + * var segmentation = new WatershedSegmentation(imageData); + * var result = segmentation.result; + * var result = segmentation.finer(); + * var result = segmentation.coarser(); + * + * TODO: + * * Edge options other than canny. + * * Create a graph-structure for coarse/fine adjustment. + * + */ +define(["./base", + "./binary-heap-priority-queue", + "../canny", + "../distance-transform"], +function (BaseSegmentation, PriorityQueue, canny, distanceTransform) { + // Constructor for the segmentation configuration. + function WatershedSegmentation(imageData, options) { + BaseSegmentation.call(this, imageData, options); + options = options || {}; + this.sigmaRange = options.sigmaRange || + [-2, -1, 0, 0.5, 1, 2, 3].map(function(n){ + return Math.pow(2, n); + }); + this.kernelRange = options.kernelRange || [2, 3, 4, 4, 4, 5, 6]; + this.currentConfig = options.currentConfig || + Math.floor((this.sigmaRange.length - 1) / 2); + this.minRegionSize = options.minRegionSize || 16; + this.highThreshold = options.highThreshold || 0.04; + this.lowThreshold = options.lowThreshold || 0.3 * options.highThreshold; + if (this.sigmaRange.length <= 0) + throw "Invalid sigma range"; + this.neighborMap8 = new NeighborMap(this.imageData.width, + this.imageData.height); + this.neighborMap4 = new NeighborMap(this.imageData.width, + this.imageData.height, + [[-1, -1], + [-1, 0], + [-1, 1], + [ 0, -1]]); + this._compute(); + } + + WatershedSegmentation.prototype = Object.create(BaseSegmentation.prototype); + + // Change the segmentation resolution. + WatershedSegmentation.prototype.finer = function (scale) { + if (this.currentConfig > 0) { + --this.currentConfig; + if (this.imageData) + this._compute(); + } + }; + + // Change the segmentation resolution. + WatershedSegmentation.prototype.coarser = function (scale) { + if (this.currentConfig < this.sigmaRange.length - 1) { + ++this.currentConfig; + if (this.imageData) + this._compute(); + } + }; + + // Compute canny-watershed segmentation. + WatershedSegmentation.prototype._compute = function () { + var queue = new PriorityQueue({ + comparator: function(a, b) { return a[0] - b[0]; } + }); + var edge = canny(this.imageData, { + kernelTail: this.kernelRange[this.currentConfig], + sigma: this.sigmaRange[this.currentConfig], + lowThreshold: this.lowThreshold, + highThreshold: this.highThreshold, + }); + var seeds = this._findLocalMaxima(distanceTransform(edge)); + var labels = new Int32Array(edge.data.length); + var i, j, offset, neighbors, neighborOffset; + // Initialize. + for (i = 0; i < labels.length; ++i) + labels[i] = -1; + for (i = 0; i < seeds.length; ++i) + labels[seeds[i]] = i + 1; + for (i = 0; i < seeds.length; ++i) { + neighbors = this.neighborMap8.get(seeds[i]); + for (j = 0; j < neighbors.length; ++j) { + neighborOffset = neighbors[j]; + if (labels[neighborOffset] === -1) { + queue.push([edge.magnitude[neighborOffset], neighborOffset]); + labels[neighborOffset] = -2; + } + } + } + // Iterate until we label all pixels by non-border dilation. + var iter = 0; + while (queue.length > 0) { + offset = queue.shift()[1]; + neighbors = this.neighborMap8.get(offset); + var uniqueLabel = this._findUniqueRegionLabel(neighbors, labels); + if (uniqueLabel) { // Dilate when there is a unique region label. + labels[offset] = uniqueLabel; + for (i = 0; i < neighbors.length; ++i) { + neighborOffset = neighbors[i]; + if (labels[neighborOffset] === -1) { + labels[neighborOffset] = -2; + queue.push([edge.magnitude[neighborOffset], neighborOffset]); + } + } + } + else + labels[offset] = 0; // Boundary. + if (++iter > labels.length) + throw "Too many iterations"; + } + // Remove boundaries and small regions. + this.erode(0, labels); + this._removeSmallRegions(labels); + var numSegments = this._relabel(labels); + this.result = this._encodeLabels(labels); + this.result.numSegments = numSegments; + }; + + // Find the local maxima. + WatershedSegmentation.prototype._findLocalMaxima = function (intensity) { + var data = intensity.data, + maximaMap = new Uint8Array(data.length), + offsets = [], + i, k, offset, neighbors, flag; + for (offset = 0; offset < data.length; ++offset) { + neighbors = this.neighborMap8.get(offset); + flag = true; + for (k = 0; k < neighbors.length; ++k) + flag = flag && data[offset] >= data[neighbors[k]]; + maximaMap[offset] = flag; + } + // Erase connected seeds. + var suppressed = new Uint8Array(maximaMap.length); + for (offset = 0; offset < data.length; ++offset) { + neighbors = this.neighborMap4.get(offset); + flag = true; + for (k = 0; k < neighbors.length; ++k) + flag = flag && maximaMap[offset] > maximaMap[neighbors[k]]; + suppressed[offset] = flag; + } + for (offset = 0; offset < suppressed.length; ++offset) + if (suppressed[offset]) + offsets.push(offset); + return offsets; + }; + + WatershedSegmentation.prototype._findUniqueRegionLabel = + function (neighbors, labels) { + var uniqueLabels = []; + for (var i = 0; i < neighbors.length; ++i) { + var label = labels[neighbors[i]]; + if (label > 0 && uniqueLabels.indexOf(label) < 0) + uniqueLabels.push(label); + } + return (uniqueLabels.length === 1) ? uniqueLabels[0] : null; + }; + + WatershedSegmentation.prototype._findDominantLabel = + function (neighbors, labels, target) { + var histogram = {}, + label; + for (var i = 0; i < neighbors.length; ++i) { + label = labels[neighbors[i]]; + if (label !== target) { + if (histogram[label]) + ++histogram[label]; + else + histogram[label] = 1; + } + } + var count = 0, + dominantLabel = null; + for (label in histogram) { + if (histogram[label] > count) { + dominantLabel = label; + count = histogram[label]; + } + } + return dominantLabel; + }; + + // Greedy erode. + WatershedSegmentation.prototype.erode = function (target, labels) { + var offsets = [], + updates = {}, + i, j, offset; + for (offset = 0; offset < labels.length; ++offset) + if (labels[offset] === target) + offsets.push(offset); + if (target !== 0 && offsets.length === 0) + throw "No pixels for label " + target; + updates[target] = 0; + var iter = 0; + while (offsets.length > 0) { + offset = offsets.shift(); + var neighbors = this.neighborMap8.get(offset), + dominantLabel = this._findDominantLabel(neighbors, labels, target); + if (dominantLabel !== null) { + labels[offset] = dominantLabel; + if (updates[dominantLabel]) + ++updates[dominantLabel]; + else + updates[dominantLabel] = 1; + --updates[target]; + } + else + offsets.push(offset); + if (++iter > labels.length) + throw "Too many iterations for label " + target; + } + return updates; + }; + + // Find small item. + WatershedSegmentation.prototype._findSmallLabel = + function (histogram) { + var smallLabel = null; + for (var label in histogram) { + var count = histogram[label]; + if (0 < count && count < this.minRegionSize) { + smallLabel = parseInt(label, 10); + break; + } + } + return smallLabel; + }; + + // Remove small regions. + WatershedSegmentation.prototype._removeSmallRegions = + function (labels) { + var histogram = {}, + i, offset, label, updates; + for (offset = 0; offset < labels.length; ++offset) { + label = labels[offset]; + if (histogram[label]) + ++histogram[label]; + else + histogram[label] = 1; + } + var iter = 0; + while (true) { + var smallLabel = this._findSmallLabel(histogram); + if (smallLabel !== null) { + updates = this.erode(smallLabel, labels); + for (label in updates) + histogram[label] += updates[label]; + } + else + break; + if (++iter >= Object.keys(histogram).length) + throw "Too many iterations"; + } + }; + + WatershedSegmentation.prototype._relabel = function (labels) { + var uniqueArray = []; + for (var i = 0; i < labels.length; ++i) { + var index = uniqueArray.indexOf(labels[i]); + if (index < 0) { + index = uniqueArray.length; + uniqueArray.push(labels[i]); + } + labels[i] = index; + } + return uniqueArray.length; + }; + + // Encode segmentation. + WatershedSegmentation.prototype._encodeLabels = function (labels) { + var imageData = new ImageData(this.imageData.width, + this.imageData.height), + data = imageData.data; + for (var i = 0; i < labels.length; ++i) { + var value = labels[i]; + data[4 * i] = 255 & value; + data[4 * i + 1] = 255 & (value >> 8); + data[4 * i + 2] = 255 & (value >> 16); + data[4 * i + 3] = 255; + } + return imageData; + }; + + // Neighbor Map. + function NeighborMap(width, height, neighbors) { + this.neighbors = neighbors || [[-1, -1], [-1, 0], [-1, 1], + [ 0, -1], [ 0, 1], + [ 1, -1], [ 1, 0], [ 1, 1]]; + this.maps = []; + for (var k = 0; k < this.neighbors.length; ++k) { + var dy = this.neighbors[k][0], + dx = this.neighbors[k][1], + map = new Int32Array(width * height); + for (var y = 0; y < height; ++y) { + for (var x = 0; x < width; ++x) { + var Y = y + dy, + X = x + dx; + map[y * width + x] = (Y < 0 || height <= Y || X < 0 || width <= X) ? + -1 : Y * width + X; + } + } + this.maps.push(map); + } + } + + NeighborMap.prototype.get = function (offset) { + var neighborOffsets = []; + for (var k = 0; k < this.neighbors.length; ++k) { + var neighborOffset = this.maps[k][offset]; + if (neighborOffset >= 0) + neighborOffsets.push(neighborOffset); + } + return neighborOffsets; + }; + + + return WatershedSegmentation; +}); diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..8afa117 --- /dev/null +++ b/js/main.js @@ -0,0 +1,42 @@ +/* Main page dispatcher. +*/ +requirejs(['app/index', + 'app/edit', + 'helper/colormap', + 'helper/util'], +function(indexPage, editPage, colormap, util) { + var params = util.getQueryParams(); + + function createColormap(label, labels) { + return (label) ? + colormap.create("single", { + size: labels.length, + index: labels.indexOf(label) + }) : + [[255, 255, 255], + [226, 196, 196], + [64, 32, 32]].concat(colormap.create("hsv", { + size: labels.length - 3 + })); + } + + function renderPage(renderer) { + util.requestJSON('data/example.json', function(data) { + data.colormap = createColormap(params.label, data.labels); + renderer(data, params); + }); + } + + switch(params.view) { + case "index": + renderPage(indexPage); + break; + case "edit": + renderPage(editPage); + break; + default: + params.view = "index"; + window.location = util.makeQueryParams(params); + break; + } +}); diff --git a/js/require.js b/js/require.js new file mode 100644 index 0000000..3628047 --- /dev/null +++ b/js/require.js @@ -0,0 +1,2087 @@ +/** vim: et:ts=4:sw=4:sts=4 + * @license RequireJS 2.1.18 Copyright (c) 2010-2015, The Dojo Foundation All Rights Reserved. + * Available via the MIT or new BSD license. + * see: http://github.com/jrburke/requirejs for details + */ +//Not using strict: uneven strict support in browsers, #392, and causes +//problems with requirejs.exec()/transpiler plugins that may not be strict. +/*jslint regexp: true, nomen: true, sloppy: true */ +/*global window, navigator, document, importScripts, setTimeout, opera */ + +var requirejs, require, define; +(function (global) { + var req, s, head, baseElement, dataMain, src, + interactiveScript, currentlyAddingScript, mainScript, subPath, + version = '2.1.18', + commentRegExp = /(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg, + cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g, + jsSuffixRegExp = /\.js$/, + currDirRegExp = /^\.\//, + op = Object.prototype, + ostring = op.toString, + hasOwn = op.hasOwnProperty, + ap = Array.prototype, + apsp = ap.splice, + isBrowser = !!(typeof window !== 'undefined' && typeof navigator !== 'undefined' && window.document), + isWebWorker = !isBrowser && typeof importScripts !== 'undefined', + //PS3 indicates loaded and complete, but need to wait for complete + //specifically. Sequence is 'loading', 'loaded', execution, + // then 'complete'. The UA check is unfortunate, but not sure how + //to feature test w/o causing perf issues. + readyRegExp = isBrowser && navigator.platform === 'PLAYSTATION 3' ? + /^complete$/ : /^(complete|loaded)$/, + defContextName = '_', + //Oh the tragedy, detecting opera. See the usage of isOpera for reason. + isOpera = typeof opera !== 'undefined' && opera.toString() === '[object Opera]', + contexts = {}, + cfg = {}, + globalDefQueue = [], + useInteractive = false; + + function isFunction(it) { + return ostring.call(it) === '[object Function]'; + } + + function isArray(it) { + return ostring.call(it) === '[object Array]'; + } + + /** + * Helper function for iterating over an array. If the func returns + * a true value, it will break out of the loop. + */ + function each(ary, func) { + if (ary) { + var i; + for (i = 0; i < ary.length; i += 1) { + if (ary[i] && func(ary[i], i, ary)) { + break; + } + } + } + } + + /** + * Helper function for iterating over an array backwards. If the func + * returns a true value, it will break out of the loop. + */ + function eachReverse(ary, func) { + if (ary) { + var i; + for (i = ary.length - 1; i > -1; i -= 1) { + if (ary[i] && func(ary[i], i, ary)) { + break; + } + } + } + } + + function hasProp(obj, prop) { + return hasOwn.call(obj, prop); + } + + function getOwn(obj, prop) { + return hasProp(obj, prop) && obj[prop]; + } + + /** + * Cycles over properties in an object and calls a function for each + * property value. If the function returns a truthy value, then the + * iteration is stopped. + */ + function eachProp(obj, func) { + var prop; + for (prop in obj) { + if (hasProp(obj, prop)) { + if (func(obj[prop], prop)) { + break; + } + } + } + } + + /** + * Simple function to mix in properties from source into target, + * but only if target does not already have a property of the same name. + */ + function mixin(target, source, force, deepStringMixin) { + if (source) { + eachProp(source, function (value, prop) { + if (force || !hasProp(target, prop)) { + if (deepStringMixin && typeof value === 'object' && value && + !isArray(value) && !isFunction(value) && + !(value instanceof RegExp)) { + + if (!target[prop]) { + target[prop] = {}; + } + mixin(target[prop], value, force, deepStringMixin); + } else { + target[prop] = value; + } + } + }); + } + return target; + } + + //Similar to Function.prototype.bind, but the 'this' object is specified + //first, since it is easier to read/figure out what 'this' will be. + function bind(obj, fn) { + return function () { + return fn.apply(obj, arguments); + }; + } + + function scripts() { + return document.getElementsByTagName('script'); + } + + function defaultOnError(err) { + throw err; + } + + //Allow getting a global that is expressed in + //dot notation, like 'a.b.c'. + function getGlobal(value) { + if (!value) { + return value; + } + var g = global; + each(value.split('.'), function (part) { + g = g[part]; + }); + return g; + } + + /** + * Constructs an error with a pointer to an URL with more information. + * @param {String} id the error ID that maps to an ID on a web page. + * @param {String} message human readable error. + * @param {Error} [err] the original error, if there is one. + * + * @returns {Error} + */ + function makeError(id, msg, err, requireModules) { + var e = new Error(msg + '\nhttp://requirejs.org/docs/errors.html#' + id); + e.requireType = id; + e.requireModules = requireModules; + if (err) { + e.originalError = err; + } + return e; + } + + if (typeof define !== 'undefined') { + //If a define is already in play via another AMD loader, + //do not overwrite. + return; + } + + if (typeof requirejs !== 'undefined') { + if (isFunction(requirejs)) { + //Do not overwrite an existing requirejs instance. + return; + } + cfg = requirejs; + requirejs = undefined; + } + + //Allow for a require config object + if (typeof require !== 'undefined' && !isFunction(require)) { + //assume it is a config object. + cfg = require; + require = undefined; + } + + function newContext(contextName) { + var inCheckLoaded, Module, context, handlers, + checkLoadedTimeoutId, + config = { + //Defaults. Do not set a default for map + //config to speed up normalize(), which + //will run faster if there is no default. + waitSeconds: 7, + baseUrl: './', + paths: {}, + bundles: {}, + pkgs: {}, + shim: {}, + config: {} + }, + registry = {}, + //registry of just enabled modules, to speed + //cycle breaking code when lots of modules + //are registered, but not activated. + enabledRegistry = {}, + undefEvents = {}, + defQueue = [], + defined = {}, + urlFetched = {}, + bundlesMap = {}, + requireCounter = 1, + unnormalizedCounter = 1; + + /** + * Trims the . and .. from an array of path segments. + * It will keep a leading path segment if a .. will become + * the first path segment, to help with module name lookups, + * which act like paths, but can be remapped. But the end result, + * all paths that use this function should look normalized. + * NOTE: this method MODIFIES the input array. + * @param {Array} ary the array of path segments. + */ + function trimDots(ary) { + var i, part; + for (i = 0; i < ary.length; i++) { + part = ary[i]; + if (part === '.') { + ary.splice(i, 1); + i -= 1; + } else if (part === '..') { + // If at the start, or previous value is still .., + // keep them so that when converted to a path it may + // still work when converted to a path, even though + // as an ID it is less than ideal. In larger point + // releases, may be better to just kick out an error. + if (i === 0 || (i === 1 && ary[2] === '..') || ary[i - 1] === '..') { + continue; + } else if (i > 0) { + ary.splice(i - 1, 2); + i -= 2; + } + } + } + } + + /** + * Given a relative module name, like ./something, normalize it to + * a real name that can be mapped to a path. + * @param {String} name the relative name + * @param {String} baseName a real name that the name arg is relative + * to. + * @param {Boolean} applyMap apply the map config to the value. Should + * only be done if this normalization is for a dependency ID. + * @returns {String} normalized name + */ + function normalize(name, baseName, applyMap) { + var pkgMain, mapValue, nameParts, i, j, nameSegment, lastIndex, + foundMap, foundI, foundStarMap, starI, normalizedBaseParts, + baseParts = (baseName && baseName.split('/')), + map = config.map, + starMap = map && map['*']; + + //Adjust any relative paths. + if (name) { + name = name.split('/'); + lastIndex = name.length - 1; + + // If wanting node ID compatibility, strip .js from end + // of IDs. Have to do this here, and not in nameToUrl + // because node allows either .js or non .js to map + // to same file. + if (config.nodeIdCompat && jsSuffixRegExp.test(name[lastIndex])) { + name[lastIndex] = name[lastIndex].replace(jsSuffixRegExp, ''); + } + + // Starts with a '.' so need the baseName + if (name[0].charAt(0) === '.' && baseParts) { + //Convert baseName to array, and lop off the last part, + //so that . matches that 'directory' and not name of the baseName's + //module. For instance, baseName of 'one/two/three', maps to + //'one/two/three.js', but we want the directory, 'one/two' for + //this normalization. + normalizedBaseParts = baseParts.slice(0, baseParts.length - 1); + name = normalizedBaseParts.concat(name); + } + + trimDots(name); + name = name.join('/'); + } + + //Apply map config if available. + if (applyMap && map && (baseParts || starMap)) { + nameParts = name.split('/'); + + outerLoop: for (i = nameParts.length; i > 0; i -= 1) { + nameSegment = nameParts.slice(0, i).join('/'); + + if (baseParts) { + //Find the longest baseName segment match in the config. + //So, do joins on the biggest to smallest lengths of baseParts. + for (j = baseParts.length; j > 0; j -= 1) { + mapValue = getOwn(map, baseParts.slice(0, j).join('/')); + + //baseName segment has config, find if it has one for + //this name. + if (mapValue) { + mapValue = getOwn(mapValue, nameSegment); + if (mapValue) { + //Match, update name to the new value. + foundMap = mapValue; + foundI = i; + break outerLoop; + } + } + } + } + + //Check for a star map match, but just hold on to it, + //if there is a shorter segment match later in a matching + //config, then favor over this star map. + if (!foundStarMap && starMap && getOwn(starMap, nameSegment)) { + foundStarMap = getOwn(starMap, nameSegment); + starI = i; + } + } + + if (!foundMap && foundStarMap) { + foundMap = foundStarMap; + foundI = starI; + } + + if (foundMap) { + nameParts.splice(0, foundI, foundMap); + name = nameParts.join('/'); + } + } + + // If the name points to a package's name, use + // the package main instead. + pkgMain = getOwn(config.pkgs, name); + + return pkgMain ? pkgMain : name; + } + + function removeScript(name) { + if (isBrowser) { + each(scripts(), function (scriptNode) { + if (scriptNode.getAttribute('data-requiremodule') === name && + scriptNode.getAttribute('data-requirecontext') === context.contextName) { + scriptNode.parentNode.removeChild(scriptNode); + return true; + } + }); + } + } + + function hasPathFallback(id) { + var pathConfig = getOwn(config.paths, id); + if (pathConfig && isArray(pathConfig) && pathConfig.length > 1) { + //Pop off the first array value, since it failed, and + //retry + pathConfig.shift(); + context.require.undef(id); + + //Custom require that does not do map translation, since + //ID is "absolute", already mapped/resolved. + context.makeRequire(null, { + skipMap: true + })([id]); + + return true; + } + } + + //Turns a plugin!resource to [plugin, resource] + //with the plugin being undefined if the name + //did not have a plugin prefix. + function splitPrefix(name) { + var prefix, + index = name ? name.indexOf('!') : -1; + if (index > -1) { + prefix = name.substring(0, index); + name = name.substring(index + 1, name.length); + } + return [prefix, name]; + } + + /** + * Creates a module mapping that includes plugin prefix, module + * name, and path. If parentModuleMap is provided it will + * also normalize the name via require.normalize() + * + * @param {String} name the module name + * @param {String} [parentModuleMap] parent module map + * for the module name, used to resolve relative names. + * @param {Boolean} isNormalized: is the ID already normalized. + * This is true if this call is done for a define() module ID. + * @param {Boolean} applyMap: apply the map config to the ID. + * Should only be true if this map is for a dependency. + * + * @returns {Object} + */ + function makeModuleMap(name, parentModuleMap, isNormalized, applyMap) { + var url, pluginModule, suffix, nameParts, + prefix = null, + parentName = parentModuleMap ? parentModuleMap.name : null, + originalName = name, + isDefine = true, + normalizedName = ''; + + //If no name, then it means it is a require call, generate an + //internal name. + if (!name) { + isDefine = false; + name = '_@r' + (requireCounter += 1); + } + + nameParts = splitPrefix(name); + prefix = nameParts[0]; + name = nameParts[1]; + + if (prefix) { + prefix = normalize(prefix, parentName, applyMap); + pluginModule = getOwn(defined, prefix); + } + + //Account for relative paths if there is a base name. + if (name) { + if (prefix) { + if (pluginModule && pluginModule.normalize) { + //Plugin is loaded, use its normalize method. + normalizedName = pluginModule.normalize(name, function (name) { + return normalize(name, parentName, applyMap); + }); + } else { + // If nested plugin references, then do not try to + // normalize, as it will not normalize correctly. This + // places a restriction on resourceIds, and the longer + // term solution is not to normalize until plugins are + // loaded and all normalizations to allow for async + // loading of a loader plugin. But for now, fixes the + // common uses. Details in #1131 + normalizedName = name.indexOf('!') === -1 ? + normalize(name, parentName, applyMap) : + name; + } + } else { + //A regular module. + normalizedName = normalize(name, parentName, applyMap); + + //Normalized name may be a plugin ID due to map config + //application in normalize. The map config values must + //already be normalized, so do not need to redo that part. + nameParts = splitPrefix(normalizedName); + prefix = nameParts[0]; + normalizedName = nameParts[1]; + isNormalized = true; + + url = context.nameToUrl(normalizedName); + } + } + + //If the id is a plugin id that cannot be determined if it needs + //normalization, stamp it with a unique ID so two matching relative + //ids that may conflict can be separate. + suffix = prefix && !pluginModule && !isNormalized ? + '_unnormalized' + (unnormalizedCounter += 1) : + ''; + + return { + prefix: prefix, + name: normalizedName, + parentMap: parentModuleMap, + unnormalized: !!suffix, + url: url, + originalName: originalName, + isDefine: isDefine, + id: (prefix ? + prefix + '!' + normalizedName : + normalizedName) + suffix + }; + } + + function getModule(depMap) { + var id = depMap.id, + mod = getOwn(registry, id); + + if (!mod) { + mod = registry[id] = new context.Module(depMap); + } + + return mod; + } + + function on(depMap, name, fn) { + var id = depMap.id, + mod = getOwn(registry, id); + + if (hasProp(defined, id) && + (!mod || mod.defineEmitComplete)) { + if (name === 'defined') { + fn(defined[id]); + } + } else { + mod = getModule(depMap); + if (mod.error && name === 'error') { + fn(mod.error); + } else { + mod.on(name, fn); + } + } + } + + function onError(err, errback) { + var ids = err.requireModules, + notified = false; + + if (errback) { + errback(err); + } else { + each(ids, function (id) { + var mod = getOwn(registry, id); + if (mod) { + //Set error on module, so it skips timeout checks. + mod.error = err; + if (mod.events.error) { + notified = true; + mod.emit('error', err); + } + } + }); + + if (!notified) { + req.onError(err); + } + } + } + + /** + * Internal method to transfer globalQueue items to this context's + * defQueue. + */ + function takeGlobalQueue() { + //Push all the globalDefQueue items into the context's defQueue + if (globalDefQueue.length) { + //Array splice in the values since the context code has a + //local var ref to defQueue, so cannot just reassign the one + //on context. + apsp.apply(defQueue, + [defQueue.length, 0].concat(globalDefQueue)); + globalDefQueue = []; + } + } + + handlers = { + 'require': function (mod) { + if (mod.require) { + return mod.require; + } else { + return (mod.require = context.makeRequire(mod.map)); + } + }, + 'exports': function (mod) { + mod.usingExports = true; + if (mod.map.isDefine) { + if (mod.exports) { + return (defined[mod.map.id] = mod.exports); + } else { + return (mod.exports = defined[mod.map.id] = {}); + } + } + }, + 'module': function (mod) { + if (mod.module) { + return mod.module; + } else { + return (mod.module = { + id: mod.map.id, + uri: mod.map.url, + config: function () { + return getOwn(config.config, mod.map.id) || {}; + }, + exports: mod.exports || (mod.exports = {}) + }); + } + } + }; + + function cleanRegistry(id) { + //Clean up machinery used for waiting modules. + delete registry[id]; + delete enabledRegistry[id]; + } + + function breakCycle(mod, traced, processed) { + var id = mod.map.id; + + if (mod.error) { + mod.emit('error', mod.error); + } else { + traced[id] = true; + each(mod.depMaps, function (depMap, i) { + var depId = depMap.id, + dep = getOwn(registry, depId); + + //Only force things that have not completed + //being defined, so still in the registry, + //and only if it has not been matched up + //in the module already. + if (dep && !mod.depMatched[i] && !processed[depId]) { + if (getOwn(traced, depId)) { + mod.defineDep(i, defined[depId]); + mod.check(); //pass false? + } else { + breakCycle(dep, traced, processed); + } + } + }); + processed[id] = true; + } + } + + function checkLoaded() { + var err, usingPathFallback, + waitInterval = config.waitSeconds * 1000, + //It is possible to disable the wait interval by using waitSeconds of 0. + expired = waitInterval && (context.startTime + waitInterval) < new Date().getTime(), + noLoads = [], + reqCalls = [], + stillLoading = false, + needCycleCheck = true; + + //Do not bother if this call was a result of a cycle break. + if (inCheckLoaded) { + return; + } + + inCheckLoaded = true; + + //Figure out the state of all the modules. + eachProp(enabledRegistry, function (mod) { + var map = mod.map, + modId = map.id; + + //Skip things that are not enabled or in error state. + if (!mod.enabled) { + return; + } + + if (!map.isDefine) { + reqCalls.push(mod); + } + + if (!mod.error) { + //If the module should be executed, and it has not + //been inited and time is up, remember it. + if (!mod.inited && expired) { + if (hasPathFallback(modId)) { + usingPathFallback = true; + stillLoading = true; + } else { + noLoads.push(modId); + removeScript(modId); + } + } else if (!mod.inited && mod.fetched && map.isDefine) { + stillLoading = true; + if (!map.prefix) { + //No reason to keep looking for unfinished + //loading. If the only stillLoading is a + //plugin resource though, keep going, + //because it may be that a plugin resource + //is waiting on a non-plugin cycle. + return (needCycleCheck = false); + } + } + } + }); + + if (expired && noLoads.length) { + //If wait time expired, throw error of unloaded modules. + err = makeError('timeout', 'Load timeout for modules: ' + noLoads, null, noLoads); + err.contextName = context.contextName; + return onError(err); + } + + //Not expired, check for a cycle. + if (needCycleCheck) { + each(reqCalls, function (mod) { + breakCycle(mod, {}, {}); + }); + } + + //If still waiting on loads, and the waiting load is something + //other than a plugin resource, or there are still outstanding + //scripts, then just try back later. + if ((!expired || usingPathFallback) && stillLoading) { + //Something is still waiting to load. Wait for it, but only + //if a timeout is not already in effect. + if ((isBrowser || isWebWorker) && !checkLoadedTimeoutId) { + checkLoadedTimeoutId = setTimeout(function () { + checkLoadedTimeoutId = 0; + checkLoaded(); + }, 50); + } + } + + inCheckLoaded = false; + } + + Module = function (map) { + this.events = getOwn(undefEvents, map.id) || {}; + this.map = map; + this.shim = getOwn(config.shim, map.id); + this.depExports = []; + this.depMaps = []; + this.depMatched = []; + this.pluginMaps = {}; + this.depCount = 0; + + /* this.exports this.factory + this.depMaps = [], + this.enabled, this.fetched + */ + }; + + Module.prototype = { + init: function (depMaps, factory, errback, options) { + options = options || {}; + + //Do not do more inits if already done. Can happen if there + //are multiple define calls for the same module. That is not + //a normal, common case, but it is also not unexpected. + if (this.inited) { + return; + } + + this.factory = factory; + + if (errback) { + //Register for errors on this module. + this.on('error', errback); + } else if (this.events.error) { + //If no errback already, but there are error listeners + //on this module, set up an errback to pass to the deps. + errback = bind(this, function (err) { + this.emit('error', err); + }); + } + + //Do a copy of the dependency array, so that + //source inputs are not modified. For example + //"shim" deps are passed in here directly, and + //doing a direct modification of the depMaps array + //would affect that config. + this.depMaps = depMaps && depMaps.slice(0); + + this.errback = errback; + + //Indicate this module has be initialized + this.inited = true; + + this.ignore = options.ignore; + + //Could have option to init this module in enabled mode, + //or could have been previously marked as enabled. However, + //the dependencies are not known until init is called. So + //if enabled previously, now trigger dependencies as enabled. + if (options.enabled || this.enabled) { + //Enable this module and dependencies. + //Will call this.check() + this.enable(); + } else { + this.check(); + } + }, + + defineDep: function (i, depExports) { + //Because of cycles, defined callback for a given + //export can be called more than once. + if (!this.depMatched[i]) { + this.depMatched[i] = true; + this.depCount -= 1; + this.depExports[i] = depExports; + } + }, + + fetch: function () { + if (this.fetched) { + return; + } + this.fetched = true; + + context.startTime = (new Date()).getTime(); + + var map = this.map; + + //If the manager is for a plugin managed resource, + //ask the plugin to load it now. + if (this.shim) { + context.makeRequire(this.map, { + enableBuildCallback: true + })(this.shim.deps || [], bind(this, function () { + return map.prefix ? this.callPlugin() : this.load(); + })); + } else { + //Regular dependency. + return map.prefix ? this.callPlugin() : this.load(); + } + }, + + load: function () { + var url = this.map.url; + + //Regular dependency. + if (!urlFetched[url]) { + urlFetched[url] = true; + context.load(this.map.id, url); + } + }, + + /** + * Checks if the module is ready to define itself, and if so, + * define it. + */ + check: function () { + if (!this.enabled || this.enabling) { + return; + } + + var err, cjsModule, + id = this.map.id, + depExports = this.depExports, + exports = this.exports, + factory = this.factory; + + if (!this.inited) { + this.fetch(); + } else if (this.error) { + this.emit('error', this.error); + } else if (!this.defining) { + //The factory could trigger another require call + //that would result in checking this module to + //define itself again. If already in the process + //of doing that, skip this work. + this.defining = true; + + if (this.depCount < 1 && !this.defined) { + if (isFunction(factory)) { + //If there is an error listener, favor passing + //to that instead of throwing an error. However, + //only do it for define()'d modules. require + //errbacks should not be called for failures in + //their callbacks (#699). However if a global + //onError is set, use that. + if ((this.events.error && this.map.isDefine) || + req.onError !== defaultOnError) { + try { + exports = context.execCb(id, factory, depExports, exports); + } catch (e) { + err = e; + } + } else { + exports = context.execCb(id, factory, depExports, exports); + } + + // Favor return value over exports. If node/cjs in play, + // then will not have a return value anyway. Favor + // module.exports assignment over exports object. + if (this.map.isDefine && exports === undefined) { + cjsModule = this.module; + if (cjsModule) { + exports = cjsModule.exports; + } else if (this.usingExports) { + //exports already set the defined value. + exports = this.exports; + } + } + + if (err) { + err.requireMap = this.map; + err.requireModules = this.map.isDefine ? [this.map.id] : null; + err.requireType = this.map.isDefine ? 'define' : 'require'; + return onError((this.error = err)); + } + + } else { + //Just a literal value + exports = factory; + } + + this.exports = exports; + + if (this.map.isDefine && !this.ignore) { + defined[id] = exports; + + if (req.onResourceLoad) { + req.onResourceLoad(context, this.map, this.depMaps); + } + } + + //Clean up + cleanRegistry(id); + + this.defined = true; + } + + //Finished the define stage. Allow calling check again + //to allow define notifications below in the case of a + //cycle. + this.defining = false; + + if (this.defined && !this.defineEmitted) { + this.defineEmitted = true; + this.emit('defined', this.exports); + this.defineEmitComplete = true; + } + + } + }, + + callPlugin: function () { + var map = this.map, + id = map.id, + //Map already normalized the prefix. + pluginMap = makeModuleMap(map.prefix); + + //Mark this as a dependency for this plugin, so it + //can be traced for cycles. + this.depMaps.push(pluginMap); + + on(pluginMap, 'defined', bind(this, function (plugin) { + var load, normalizedMap, normalizedMod, + bundleId = getOwn(bundlesMap, this.map.id), + name = this.map.name, + parentName = this.map.parentMap ? this.map.parentMap.name : null, + localRequire = context.makeRequire(map.parentMap, { + enableBuildCallback: true + }); + + //If current map is not normalized, wait for that + //normalized name to load instead of continuing. + if (this.map.unnormalized) { + //Normalize the ID if the plugin allows it. + if (plugin.normalize) { + name = plugin.normalize(name, function (name) { + return normalize(name, parentName, true); + }) || ''; + } + + //prefix and name should already be normalized, no need + //for applying map config again either. + normalizedMap = makeModuleMap(map.prefix + '!' + name, + this.map.parentMap); + on(normalizedMap, + 'defined', bind(this, function (value) { + this.init([], function () { return value; }, null, { + enabled: true, + ignore: true + }); + })); + + normalizedMod = getOwn(registry, normalizedMap.id); + if (normalizedMod) { + //Mark this as a dependency for this plugin, so it + //can be traced for cycles. + this.depMaps.push(normalizedMap); + + if (this.events.error) { + normalizedMod.on('error', bind(this, function (err) { + this.emit('error', err); + })); + } + normalizedMod.enable(); + } + + return; + } + + //If a paths config, then just load that file instead to + //resolve the plugin, as it is built into that paths layer. + if (bundleId) { + this.map.url = context.nameToUrl(bundleId); + this.load(); + return; + } + + load = bind(this, function (value) { + this.init([], function () { return value; }, null, { + enabled: true + }); + }); + + load.error = bind(this, function (err) { + this.inited = true; + this.error = err; + err.requireModules = [id]; + + //Remove temp unnormalized modules for this module, + //since they will never be resolved otherwise now. + eachProp(registry, function (mod) { + if (mod.map.id.indexOf(id + '_unnormalized') === 0) { + cleanRegistry(mod.map.id); + } + }); + + onError(err); + }); + + //Allow plugins to load other code without having to know the + //context or how to 'complete' the load. + load.fromText = bind(this, function (text, textAlt) { + /*jslint evil: true */ + var moduleName = map.name, + moduleMap = makeModuleMap(moduleName), + hasInteractive = useInteractive; + + //As of 2.1.0, support just passing the text, to reinforce + //fromText only being called once per resource. Still + //support old style of passing moduleName but discard + //that moduleName in favor of the internal ref. + if (textAlt) { + text = textAlt; + } + + //Turn off interactive script matching for IE for any define + //calls in the text, then turn it back on at the end. + if (hasInteractive) { + useInteractive = false; + } + + //Prime the system by creating a module instance for + //it. + getModule(moduleMap); + + //Transfer any config to this other module. + if (hasProp(config.config, id)) { + config.config[moduleName] = config.config[id]; + } + + try { + req.exec(text); + } catch (e) { + return onError(makeError('fromtexteval', + 'fromText eval for ' + id + + ' failed: ' + e, + e, + [id])); + } + + if (hasInteractive) { + useInteractive = true; + } + + //Mark this as a dependency for the plugin + //resource + this.depMaps.push(moduleMap); + + //Support anonymous modules. + context.completeLoad(moduleName); + + //Bind the value of that module to the value for this + //resource ID. + localRequire([moduleName], load); + }); + + //Use parentName here since the plugin's name is not reliable, + //could be some weird string with no path that actually wants to + //reference the parentName's path. + plugin.load(map.name, localRequire, load, config); + })); + + context.enable(pluginMap, this); + this.pluginMaps[pluginMap.id] = pluginMap; + }, + + enable: function () { + enabledRegistry[this.map.id] = this; + this.enabled = true; + + //Set flag mentioning that the module is enabling, + //so that immediate calls to the defined callbacks + //for dependencies do not trigger inadvertent load + //with the depCount still being zero. + this.enabling = true; + + //Enable each dependency + each(this.depMaps, bind(this, function (depMap, i) { + var id, mod, handler; + + if (typeof depMap === 'string') { + //Dependency needs to be converted to a depMap + //and wired up to this module. + depMap = makeModuleMap(depMap, + (this.map.isDefine ? this.map : this.map.parentMap), + false, + !this.skipMap); + this.depMaps[i] = depMap; + + handler = getOwn(handlers, depMap.id); + + if (handler) { + this.depExports[i] = handler(this); + return; + } + + this.depCount += 1; + + on(depMap, 'defined', bind(this, function (depExports) { + if (this.undefed) { + return; + } + this.defineDep(i, depExports); + this.check(); + })); + + if (this.errback) { + on(depMap, 'error', bind(this, this.errback)); + } else if (this.events.error) { + // No direct errback on this module, but something + // else is listening for errors, so be sure to + // propagate the error correctly. + on(depMap, 'error', bind(this, function(err) { + this.emit('error', err); + })); + } + } + + id = depMap.id; + mod = registry[id]; + + //Skip special modules like 'require', 'exports', 'module' + //Also, don't call enable if it is already enabled, + //important in circular dependency cases. + if (!hasProp(handlers, id) && mod && !mod.enabled) { + context.enable(depMap, this); + } + })); + + //Enable each plugin that is used in + //a dependency + eachProp(this.pluginMaps, bind(this, function (pluginMap) { + var mod = getOwn(registry, pluginMap.id); + if (mod && !mod.enabled) { + context.enable(pluginMap, this); + } + })); + + this.enabling = false; + + this.check(); + }, + + on: function (name, cb) { + var cbs = this.events[name]; + if (!cbs) { + cbs = this.events[name] = []; + } + cbs.push(cb); + }, + + emit: function (name, evt) { + each(this.events[name], function (cb) { + cb(evt); + }); + if (name === 'error') { + //Now that the error handler was triggered, remove + //the listeners, since this broken Module instance + //can stay around for a while in the registry. + delete this.events[name]; + } + } + }; + + function callGetModule(args) { + //Skip modules already defined. + if (!hasProp(defined, args[0])) { + getModule(makeModuleMap(args[0], null, true)).init(args[1], args[2]); + } + } + + function removeListener(node, func, name, ieName) { + //Favor detachEvent because of IE9 + //issue, see attachEvent/addEventListener comment elsewhere + //in this file. + if (node.detachEvent && !isOpera) { + //Probably IE. If not it will throw an error, which will be + //useful to know. + if (ieName) { + node.detachEvent(ieName, func); + } + } else { + node.removeEventListener(name, func, false); + } + } + + /** + * Given an event from a script node, get the requirejs info from it, + * and then removes the event listeners on the node. + * @param {Event} evt + * @returns {Object} + */ + function getScriptData(evt) { + //Using currentTarget instead of target for Firefox 2.0's sake. Not + //all old browsers will be supported, but this one was easy enough + //to support and still makes sense. + var node = evt.currentTarget || evt.srcElement; + + //Remove the listeners once here. + removeListener(node, context.onScriptLoad, 'load', 'onreadystatechange'); + removeListener(node, context.onScriptError, 'error'); + + return { + node: node, + id: node && node.getAttribute('data-requiremodule') + }; + } + + function intakeDefines() { + var args; + + //Any defined modules in the global queue, intake them now. + takeGlobalQueue(); + + //Make sure any remaining defQueue items get properly processed. + while (defQueue.length) { + args = defQueue.shift(); + if (args[0] === null) { + return onError(makeError('mismatch', 'Mismatched anonymous define() module: ' + + args[args.length - 1])); + } else { + //args are id, deps, factory. Should be normalized by the + //define() function. + callGetModule(args); + } + } + } + + context = { + config: config, + contextName: contextName, + registry: registry, + defined: defined, + urlFetched: urlFetched, + defQueue: defQueue, + Module: Module, + makeModuleMap: makeModuleMap, + nextTick: req.nextTick, + onError: onError, + + /** + * Set a configuration for the context. + * @param {Object} cfg config object to integrate. + */ + configure: function (cfg) { + //Make sure the baseUrl ends in a slash. + if (cfg.baseUrl) { + if (cfg.baseUrl.charAt(cfg.baseUrl.length - 1) !== '/') { + cfg.baseUrl += '/'; + } + } + + //Save off the paths since they require special processing, + //they are additive. + var shim = config.shim, + objs = { + paths: true, + bundles: true, + config: true, + map: true + }; + + eachProp(cfg, function (value, prop) { + if (objs[prop]) { + if (!config[prop]) { + config[prop] = {}; + } + mixin(config[prop], value, true, true); + } else { + config[prop] = value; + } + }); + + //Reverse map the bundles + if (cfg.bundles) { + eachProp(cfg.bundles, function (value, prop) { + each(value, function (v) { + if (v !== prop) { + bundlesMap[v] = prop; + } + }); + }); + } + + //Merge shim + if (cfg.shim) { + eachProp(cfg.shim, function (value, id) { + //Normalize the structure + if (isArray(value)) { + value = { + deps: value + }; + } + if ((value.exports || value.init) && !value.exportsFn) { + value.exportsFn = context.makeShimExports(value); + } + shim[id] = value; + }); + config.shim = shim; + } + + //Adjust packages if necessary. + if (cfg.packages) { + each(cfg.packages, function (pkgObj) { + var location, name; + + pkgObj = typeof pkgObj === 'string' ? {name: pkgObj} : pkgObj; + + name = pkgObj.name; + location = pkgObj.location; + if (location) { + config.paths[name] = pkgObj.location; + } + + //Save pointer to main module ID for pkg name. + //Remove leading dot in main, so main paths are normalized, + //and remove any trailing .js, since different package + //envs have different conventions: some use a module name, + //some use a file name. + config.pkgs[name] = pkgObj.name + '/' + (pkgObj.main || 'main') + .replace(currDirRegExp, '') + .replace(jsSuffixRegExp, ''); + }); + } + + //If there are any "waiting to execute" modules in the registry, + //update the maps for them, since their info, like URLs to load, + //may have changed. + eachProp(registry, function (mod, id) { + //If module already has init called, since it is too + //late to modify them, and ignore unnormalized ones + //since they are transient. + if (!mod.inited && !mod.map.unnormalized) { + mod.map = makeModuleMap(id, null, true); + } + }); + + //If a deps array or a config callback is specified, then call + //require with those args. This is useful when require is defined as a + //config object before require.js is loaded. + if (cfg.deps || cfg.callback) { + context.require(cfg.deps || [], cfg.callback); + } + }, + + makeShimExports: function (value) { + function fn() { + var ret; + if (value.init) { + ret = value.init.apply(global, arguments); + } + return ret || (value.exports && getGlobal(value.exports)); + } + return fn; + }, + + makeRequire: function (relMap, options) { + options = options || {}; + + function localRequire(deps, callback, errback) { + var id, map, requireMod; + + if (options.enableBuildCallback && callback && isFunction(callback)) { + callback.__requireJsBuild = true; + } + + if (typeof deps === 'string') { + if (isFunction(callback)) { + //Invalid call + return onError(makeError('requireargs', 'Invalid require call'), errback); + } + + //If require|exports|module are requested, get the + //value for them from the special handlers. Caveat: + //this only works while module is being defined. + if (relMap && hasProp(handlers, deps)) { + return handlers[deps](registry[relMap.id]); + } + + //Synchronous access to one module. If require.get is + //available (as in the Node adapter), prefer that. + if (req.get) { + return req.get(context, deps, relMap, localRequire); + } + + //Normalize module name, if it contains . or .. + map = makeModuleMap(deps, relMap, false, true); + id = map.id; + + if (!hasProp(defined, id)) { + return onError(makeError('notloaded', 'Module name "' + + id + + '" has not been loaded yet for context: ' + + contextName + + (relMap ? '' : '. Use require([])'))); + } + return defined[id]; + } + + //Grab defines waiting in the global queue. + intakeDefines(); + + //Mark all the dependencies as needing to be loaded. + context.nextTick(function () { + //Some defines could have been added since the + //require call, collect them. + intakeDefines(); + + requireMod = getModule(makeModuleMap(null, relMap)); + + //Store if map config should be applied to this require + //call for dependencies. + requireMod.skipMap = options.skipMap; + + requireMod.init(deps, callback, errback, { + enabled: true + }); + + checkLoaded(); + }); + + return localRequire; + } + + mixin(localRequire, { + isBrowser: isBrowser, + + /** + * Converts a module name + .extension into an URL path. + * *Requires* the use of a module name. It does not support using + * plain URLs like nameToUrl. + */ + toUrl: function (moduleNamePlusExt) { + var ext, + index = moduleNamePlusExt.lastIndexOf('.'), + segment = moduleNamePlusExt.split('/')[0], + isRelative = segment === '.' || segment === '..'; + + //Have a file extension alias, and it is not the + //dots from a relative path. + if (index !== -1 && (!isRelative || index > 1)) { + ext = moduleNamePlusExt.substring(index, moduleNamePlusExt.length); + moduleNamePlusExt = moduleNamePlusExt.substring(0, index); + } + + return context.nameToUrl(normalize(moduleNamePlusExt, + relMap && relMap.id, true), ext, true); + }, + + defined: function (id) { + return hasProp(defined, makeModuleMap(id, relMap, false, true).id); + }, + + specified: function (id) { + id = makeModuleMap(id, relMap, false, true).id; + return hasProp(defined, id) || hasProp(registry, id); + } + }); + + //Only allow undef on top level require calls + if (!relMap) { + localRequire.undef = function (id) { + //Bind any waiting define() calls to this context, + //fix for #408 + takeGlobalQueue(); + + var map = makeModuleMap(id, relMap, true), + mod = getOwn(registry, id); + + mod.undefed = true; + removeScript(id); + + delete defined[id]; + delete urlFetched[map.url]; + delete undefEvents[id]; + + //Clean queued defines too. Go backwards + //in array so that the splices do not + //mess up the iteration. + eachReverse(defQueue, function(args, i) { + if (args[0] === id) { + defQueue.splice(i, 1); + } + }); + + if (mod) { + //Hold on to listeners in case the + //module will be attempted to be reloaded + //using a different config. + if (mod.events.defined) { + undefEvents[id] = mod.events; + } + + cleanRegistry(id); + } + }; + } + + return localRequire; + }, + + /** + * Called to enable a module if it is still in the registry + * awaiting enablement. A second arg, parent, the parent module, + * is passed in for context, when this method is overridden by + * the optimizer. Not shown here to keep code compact. + */ + enable: function (depMap) { + var mod = getOwn(registry, depMap.id); + if (mod) { + getModule(depMap).enable(); + } + }, + + /** + * Internal method used by environment adapters to complete a load event. + * A load event could be a script load or just a load pass from a synchronous + * load call. + * @param {String} moduleName the name of the module to potentially complete. + */ + completeLoad: function (moduleName) { + var found, args, mod, + shim = getOwn(config.shim, moduleName) || {}, + shExports = shim.exports; + + takeGlobalQueue(); + + while (defQueue.length) { + args = defQueue.shift(); + if (args[0] === null) { + args[0] = moduleName; + //If already found an anonymous module and bound it + //to this name, then this is some other anon module + //waiting for its completeLoad to fire. + if (found) { + break; + } + found = true; + } else if (args[0] === moduleName) { + //Found matching define call for this script! + found = true; + } + + callGetModule(args); + } + + //Do this after the cycle of callGetModule in case the result + //of those calls/init calls changes the registry. + mod = getOwn(registry, moduleName); + + if (!found && !hasProp(defined, moduleName) && mod && !mod.inited) { + if (config.enforceDefine && (!shExports || !getGlobal(shExports))) { + if (hasPathFallback(moduleName)) { + return; + } else { + return onError(makeError('nodefine', + 'No define call for ' + moduleName, + null, + [moduleName])); + } + } else { + //A script that does not call define(), so just simulate + //the call for it. + callGetModule([moduleName, (shim.deps || []), shim.exportsFn]); + } + } + + checkLoaded(); + }, + + /** + * Converts a module name to a file path. Supports cases where + * moduleName may actually be just an URL. + * Note that it **does not** call normalize on the moduleName, + * it is assumed to have already been normalized. This is an + * internal API, not a public one. Use toUrl for the public API. + */ + nameToUrl: function (moduleName, ext, skipExt) { + var paths, syms, i, parentModule, url, + parentPath, bundleId, + pkgMain = getOwn(config.pkgs, moduleName); + + if (pkgMain) { + moduleName = pkgMain; + } + + bundleId = getOwn(bundlesMap, moduleName); + + if (bundleId) { + return context.nameToUrl(bundleId, ext, skipExt); + } + + //If a colon is in the URL, it indicates a protocol is used and it is just + //an URL to a file, or if it starts with a slash, contains a query arg (i.e. ?) + //or ends with .js, then assume the user meant to use an url and not a module id. + //The slash is important for protocol-less URLs as well as full paths. + if (req.jsExtRegExp.test(moduleName)) { + //Just a plain path, not module name lookup, so just return it. + //Add extension if it is included. This is a bit wonky, only non-.js things pass + //an extension, this method probably needs to be reworked. + url = moduleName + (ext || ''); + } else { + //A module that needs to be converted to a path. + paths = config.paths; + + syms = moduleName.split('/'); + //For each module name segment, see if there is a path + //registered for it. Start with most specific name + //and work up from it. + for (i = syms.length; i > 0; i -= 1) { + parentModule = syms.slice(0, i).join('/'); + + parentPath = getOwn(paths, parentModule); + if (parentPath) { + //If an array, it means there are a few choices, + //Choose the one that is desired + if (isArray(parentPath)) { + parentPath = parentPath[0]; + } + syms.splice(0, i, parentPath); + break; + } + } + + //Join the path parts together, then figure out if baseUrl is needed. + url = syms.join('/'); + url += (ext || (/^data\:|\?/.test(url) || skipExt ? '' : '.js')); + url = (url.charAt(0) === '/' || url.match(/^[\w\+\.\-]+:/) ? '' : config.baseUrl) + url; + } + + return config.urlArgs ? url + + ((url.indexOf('?') === -1 ? '?' : '&') + + config.urlArgs) : url; + }, + + //Delegates to req.load. Broken out as a separate function to + //allow overriding in the optimizer. + load: function (id, url) { + req.load(context, id, url); + }, + + /** + * Executes a module callback function. Broken out as a separate function + * solely to allow the build system to sequence the files in the built + * layer in the right sequence. + * + * @private + */ + execCb: function (name, callback, args, exports) { + return callback.apply(exports, args); + }, + + /** + * callback for script loads, used to check status of loading. + * + * @param {Event} evt the event from the browser for the script + * that was loaded. + */ + onScriptLoad: function (evt) { + //Using currentTarget instead of target for Firefox 2.0's sake. Not + //all old browsers will be supported, but this one was easy enough + //to support and still makes sense. + if (evt.type === 'load' || + (readyRegExp.test((evt.currentTarget || evt.srcElement).readyState))) { + //Reset interactive script so a script node is not held onto for + //to long. + interactiveScript = null; + + //Pull out the name of the module and the context. + var data = getScriptData(evt); + context.completeLoad(data.id); + } + }, + + /** + * Callback for script errors. + */ + onScriptError: function (evt) { + var data = getScriptData(evt); + if (!hasPathFallback(data.id)) { + return onError(makeError('scripterror', 'Script error for: ' + data.id, evt, [data.id])); + } + } + }; + + context.require = context.makeRequire(); + return context; + } + + /** + * Main entry point. + * + * If the only argument to require is a string, then the module that + * is represented by that string is fetched for the appropriate context. + * + * If the first argument is an array, then it will be treated as an array + * of dependency string names to fetch. An optional function callback can + * be specified to execute when all of those dependencies are available. + * + * Make a local req variable to help Caja compliance (it assumes things + * on a require that are not standardized), and to give a short + * name for minification/local scope use. + */ + req = requirejs = function (deps, callback, errback, optional) { + + //Find the right context, use default + var context, config, + contextName = defContextName; + + // Determine if have config object in the call. + if (!isArray(deps) && typeof deps !== 'string') { + // deps is a config object + config = deps; + if (isArray(callback)) { + // Adjust args if there are dependencies + deps = callback; + callback = errback; + errback = optional; + } else { + deps = []; + } + } + + if (config && config.context) { + contextName = config.context; + } + + context = getOwn(contexts, contextName); + if (!context) { + context = contexts[contextName] = req.s.newContext(contextName); + } + + if (config) { + context.configure(config); + } + + return context.require(deps, callback, errback); + }; + + /** + * Support require.config() to make it easier to cooperate with other + * AMD loaders on globally agreed names. + */ + req.config = function (config) { + return req(config); + }; + + /** + * Execute something after the current tick + * of the event loop. Override for other envs + * that have a better solution than setTimeout. + * @param {Function} fn function to execute later. + */ + req.nextTick = typeof setTimeout !== 'undefined' ? function (fn) { + setTimeout(fn, 4); + } : function (fn) { fn(); }; + + /** + * Export require as a global, but only if it does not already exist. + */ + if (!require) { + require = req; + } + + req.version = version; + + //Used to filter out dependencies that are already paths. + req.jsExtRegExp = /^\/|:|\?|\.js$/; + req.isBrowser = isBrowser; + s = req.s = { + contexts: contexts, + newContext: newContext + }; + + //Create default context. + req({}); + + //Exports some context-sensitive methods on global require. + each([ + 'toUrl', + 'undef', + 'defined', + 'specified' + ], function (prop) { + //Reference from contexts instead of early binding to default context, + //so that during builds, the latest instance of the default context + //with its config gets used. + req[prop] = function () { + var ctx = contexts[defContextName]; + return ctx.require[prop].apply(ctx, arguments); + }; + }); + + if (isBrowser) { + head = s.head = document.getElementsByTagName('head')[0]; + //If BASE tag is in play, using appendChild is a problem for IE6. + //When that browser dies, this can be removed. Details in this jQuery bug: + //http://dev.jquery.com/ticket/2709 + baseElement = document.getElementsByTagName('base')[0]; + if (baseElement) { + head = s.head = baseElement.parentNode; + } + } + + /** + * Any errors that require explicitly generates will be passed to this + * function. Intercept/override it if you want custom error handling. + * @param {Error} err the error object. + */ + req.onError = defaultOnError; + + /** + * Creates the node for the load command. Only used in browser envs. + */ + req.createNode = function (config, moduleName, url) { + var node = config.xhtml ? + document.createElementNS('http://www.w3.org/1999/xhtml', 'html:script') : + document.createElement('script'); + node.type = config.scriptType || 'text/javascript'; + node.charset = 'utf-8'; + node.async = true; + return node; + }; + + /** + * Does the request to load a module for the browser case. + * Make this a separate function to allow other environments + * to override it. + * + * @param {Object} context the require context to find state. + * @param {String} moduleName the name of the module. + * @param {Object} url the URL to the module. + */ + req.load = function (context, moduleName, url) { + var config = (context && context.config) || {}, + node; + if (isBrowser) { + //In the browser so use a script tag + node = req.createNode(config, moduleName, url); + + node.setAttribute('data-requirecontext', context.contextName); + node.setAttribute('data-requiremodule', moduleName); + + //Set up load listener. Test attachEvent first because IE9 has + //a subtle issue in its addEventListener and script onload firings + //that do not match the behavior of all other browsers with + //addEventListener support, which fire the onload event for a + //script right after the script execution. See: + //https://connect.microsoft.com/IE/feedback/details/648057/script-onload-event-is-not-fired-immediately-after-script-execution + //UNFORTUNATELY Opera implements attachEvent but does not follow the script + //script execution mode. + if (node.attachEvent && + //Check if node.attachEvent is artificially added by custom script or + //natively supported by browser + //read https://github.com/jrburke/requirejs/issues/187 + //if we can NOT find [native code] then it must NOT natively supported. + //in IE8, node.attachEvent does not have toString() + //Note the test for "[native code" with no closing brace, see: + //https://github.com/jrburke/requirejs/issues/273 + !(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) && + !isOpera) { + //Probably IE. IE (at least 6-8) do not fire + //script onload right after executing the script, so + //we cannot tie the anonymous define call to a name. + //However, IE reports the script as being in 'interactive' + //readyState at the time of the define call. + useInteractive = true; + + node.attachEvent('onreadystatechange', context.onScriptLoad); + //It would be great to add an error handler here to catch + //404s in IE9+. However, onreadystatechange will fire before + //the error handler, so that does not help. If addEventListener + //is used, then IE will fire error before load, but we cannot + //use that pathway given the connect.microsoft.com issue + //mentioned above about not doing the 'script execute, + //then fire the script load event listener before execute + //next script' that other browsers do. + //Best hope: IE10 fixes the issues, + //and then destroys all installs of IE 6-9. + //node.attachEvent('onerror', context.onScriptError); + } else { + node.addEventListener('load', context.onScriptLoad, false); + node.addEventListener('error', context.onScriptError, false); + } + node.src = url; + + //For some cache cases in IE 6-8, the script executes before the end + //of the appendChild execution, so to tie an anonymous define + //call to the module name (which is stored on the node), hold on + //to a reference to this node, but clear after the DOM insertion. + currentlyAddingScript = node; + if (baseElement) { + head.insertBefore(node, baseElement); + } else { + head.appendChild(node); + } + currentlyAddingScript = null; + + return node; + } else if (isWebWorker) { + try { + //In a web worker, use importScripts. This is not a very + //efficient use of importScripts, importScripts will block until + //its script is downloaded and evaluated. However, if web workers + //are in play, the expectation that a build has been done so that + //only one script needs to be loaded anyway. This may need to be + //reevaluated if other use cases become common. + importScripts(url); + + //Account for anonymous modules + context.completeLoad(moduleName); + } catch (e) { + context.onError(makeError('importscripts', + 'importScripts failed for ' + + moduleName + ' at ' + url, + e, + [moduleName])); + } + } + }; + + function getInteractiveScript() { + if (interactiveScript && interactiveScript.readyState === 'interactive') { + return interactiveScript; + } + + eachReverse(scripts(), function (script) { + if (script.readyState === 'interactive') { + return (interactiveScript = script); + } + }); + return interactiveScript; + } + + //Look for a data-main script attribute, which could also adjust the baseUrl. + if (isBrowser && !cfg.skipDataMain) { + //Figure out baseUrl. Get it from the script tag with require.js in it. + eachReverse(scripts(), function (script) { + //Set the 'head' where we can append children by + //using the script's parent. + if (!head) { + head = script.parentNode; + } + + //Look for a data-main attribute to set main script for the page + //to load. If it is there, the path to data main becomes the + //baseUrl, if it is not already set. + dataMain = script.getAttribute('data-main'); + if (dataMain) { + //Preserve dataMain in case it is a path (i.e. contains '?') + mainScript = dataMain; + + //Set final baseUrl if there is not already an explicit one. + if (!cfg.baseUrl) { + //Pull off the directory of data-main for use as the + //baseUrl. + src = mainScript.split('/'); + mainScript = src.pop(); + subPath = src.length ? src.join('/') + '/' : './'; + + cfg.baseUrl = subPath; + } + + //Strip off any trailing .js since mainScript is now + //like a module name. + mainScript = mainScript.replace(jsSuffixRegExp, ''); + + //If mainScript is still a path, fall back to dataMain + if (req.jsExtRegExp.test(mainScript)) { + mainScript = dataMain; + } + + //Put the data-main script in the files to load. + cfg.deps = cfg.deps ? cfg.deps.concat(mainScript) : [mainScript]; + + return true; + } + }); + } + + /** + * The function that handles definitions of modules. Differs from + * require() in that a string for the module should be the first argument, + * and the function to execute after dependencies are loaded should + * return a value to define the module corresponding to the first argument's + * name. + */ + define = function (name, deps, callback) { + var node, context; + + //Allow for anonymous modules + if (typeof name !== 'string') { + //Adjust args appropriately + callback = deps; + deps = name; + name = null; + } + + //This module may not have dependencies + if (!isArray(deps)) { + callback = deps; + deps = null; + } + + //If no name, and callback is a function, then figure out if it a + //CommonJS thing with dependencies. + if (!deps && isFunction(callback)) { + deps = []; + //Remove comments from the callback string, + //look for require calls, and pull them into the dependencies, + //but only if there are function args. + if (callback.length) { + callback + .toString() + .replace(commentRegExp, '') + .replace(cjsRequireRegExp, function (match, dep) { + deps.push(dep); + }); + + //May be a CommonJS thing even without require calls, but still + //could use exports, and module. Avoid doing exports and module + //work though if it just needs require. + //REQUIRES the function to expect the CommonJS variables in the + //order listed below. + deps = (callback.length === 1 ? ['require'] : ['require', 'exports', 'module']).concat(deps); + } + } + + //If in IE 6-8 and hit an anonymous define() call, do the interactive + //work. + if (useInteractive) { + node = currentlyAddingScript || getInteractiveScript(); + if (node) { + if (!name) { + name = node.getAttribute('data-requiremodule'); + } + context = contexts[node.getAttribute('data-requirecontext')]; + } + } + + //Always save off evaluating the def call until the script onload handler. + //This allows multiple modules to be in a file without prematurely + //tracing dependencies, and allows for anonymous module support, + //where the module name is not known until the script onload event + //occurs. If no context, use the global queue, and get it processed + //in the onscript load callback. + (context ? context.defQueue : globalDefQueue).push([name, deps, callback]); + }; + + define.amd = { + jQuery: true + }; + + /** + * Executes the text. Normally just uses eval, but can be modified + * to use a better, environment-specific call. Only used for transpiling + * loader plugins, not for plain JS modules. + * @param {String} text the text to execute/evaluate. + */ + req.exec = function (text) { + /*jslint evil: true */ + return eval(text); + }; + + //Set up with config info. + req(cfg); +}(this)); diff --git a/pre-segmentation.js b/pre-segmentation.js deleted file mode 100644 index c02efb1..0000000 --- a/pre-segmentation.js +++ /dev/null @@ -1,148 +0,0 @@ -/** - * Pre-segmentation from PNG annotated images. - * - * API - * --- - * - * PreSegmentation(imageURL, options) - * - * The function takes the following options. - * * `annotation` - URL of the annotation PNG data. - * * `toDataURL` - Callback function to receive the result as a data URL. - * * `callback` - Function to be called on finish. The function takes a single - * argument of result object that contains following fields. - * * `width` - Width of the image in pixels. - * * `height` - Height of the image in pixels. - * * `size` - Number of segments. - * * `indexMap` - Int32Array of `width * height` elements containing - * segment index for each pixel location. The segment index - * at pixel `(i, j)` is `indexMap(i * width + j)`, where - * `i` is the y coordinate of the pixel and `j` is the x - * coordinate. - * - * Jonathan Passerat-Palmbach 2015. - */ -(function() { - // Compute Pre-Segmentation. - function computePreSegmentation(imageData, options) { - var d = imageData.data; - var numPixels = imageData.width * imageData.height; - var segmentation = new Int32Array(numPixels); - // can skip with a stride of 4 since labels' RGB components are all the - // same. - for (var i = 0; i < numPixels; i++) { - // TODO would be great to rewrite this ugly loop. - segmentation[i] = d[i*4]; - } - return segmentation; - } - - // Remap label indices. - function remapLabels(segmentation) { - var map = {}, - index = 0; - for (var i = 0; i < segmentation.length; ++i) { - var label = segmentation[i]; - if (map[label] === undefined) - map[label] = index++; - segmentation[i] = map[label]; - } - return index; - } - - // Retrieve segmentation from label map. - function retrieveSegmentation(annotationsImageData, imageData, options) { - var segmentation = computePreSegmentation(annotationsImageData, options); - // numSegments is not necessarilly the biggest label as there might be a - // gap in labels presence in the image ex: [0, 1, 3, 4] still fine as the - // export preserves the original labels \o/ - var numSegments = remapLabels(segmentation); - if (options.callback) { - var rgbData = new Uint8Array(imageData.data); - options.callback({ - width: imageData.width, - height: imageData.height, - size: numSegments, - indexMap: segmentation, - rgbData: rgbData - }); - } - if (options.toDataURL) - getDataURL(imageData.width, imageData.height, indexMap, options); - } - - // Convert to Data URL. - function getDataURL(width, height, indexMap, options) { - var canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - var context = canvas.getContext('2d'), - imageData = context.createImageData(width, height), - data = imageData.data; - for (var i = 0; i < indexMap.length; ++i) { - var value = indexMap[i]; - data[4 * i + 0] = value & 255; - data[4 * i + 1] = (value >>> 8) & 255; - data[4 * i + 2] = (value >>> 16) & 255; - } - context.putImageData(imageData, 0, 0); - options.toDataURL(canvas.toDataURL()); - } - - // When image is loaded. - function onSuccessImageLoad(image, options) { - var canvas = document.createElement('canvas'); - canvas.width = image.width; - canvas.height = image.height; - var context = canvas.getContext('2d'); - // draw actual image. - context.drawImage(image, 0, 0); - var imageData = context.getImageData(0, 0, image.width, image.height); - var annotations = new Image(); - annotations.src = options.annotation; - annotations.crossOrigin = null; - annotations.onerror = function() { onErrorImageLoad(annotations); }; - annotations.onload = function() { - onSuccessAnnotationLoad(annotations, imageData, options); - }; - } - - // When annotations are loaded. - function onSuccessAnnotationLoad(annotations, imageData, options) { - var canvas = document.createElement('canvas'); - canvas.width = annotations.width; - canvas.height = annotations.height; - var context = canvas.getContext('2d'); - - // Draw actual image. - context.drawImage(annotations, 0, 0); - var annotationsImageData = context.getImageData(0, - 0, - annotations.width, - annotations.height); - - // Fill in `segmentation` with retrieved annotations. - segmentation = retrieveSegmentation(annotationsImageData, - imageData, - options); - } - - // When image is invalid. - function onErrorImageLoad(image) { - alert('Failed to load an image: ' + image.src); - } - - // Public API. - window.PreSegmentation = function(imageURL, options) { - if (typeof options === 'undefined') options = {}; - if (options.annotation) - throw "Annotation URL missing"; - - var image = new Image(); - image.src = imageURL; - image.crossOrigin = null; - image.onerror = function() { onErrorImageLoad(image); }; - image.onload = function() { onSuccessImageLoad(image, options); }; - - }; -}).call(this); diff --git a/segment-annotator.js b/segment-annotator.js deleted file mode 100644 index 8cc3b95..0000000 --- a/segment-annotator.js +++ /dev/null @@ -1,573 +0,0 @@ -/** Javascript segment annotator. - * - * Kota Yamaguchi 2013. - */ - -// SegmentAnnotator constructor. -SegmentAnnotator = function(segmentation, options) { - if (typeof options === 'undefined') options = {}; - this.backgroundColor = options.backgroundColor || [192, 192, 192]; - this.highlightAlpha = options.highlightAlpha || 128; - this.fillAlpha = options.fillAlpha || 128; - this.boundaryAlpha = options.boundaryAlpha || 192; - // Variables. - this.width = segmentation.width; - this.height = segmentation.height; - this.indexMap = segmentation.indexMap; - this.rgbData = segmentation.rgbData; - this.segments = segmentation.size; - this.layers = { - image: { canvas: null, image: null }, - annotation: { canvas: null, image: null }, - highlight: { canvas: null, image: null } - }; - this.currentSegment = null; - this.currentLabel = null; - // Initialize internal variables. - this._initializeContainer(options.container); - this._initializePixelsIndex(); - this._initializeBackgroundLayer(); - this._initializeColorMap(options.labels); - - // otherwise the closure won't capture the right this - var self = this; - - this._initializeAnnotations(options.annotation, function() { - self._initializeImageLayer(); - self._initializeAnnotationLayer(); - self._initializeHighlightLayer(); - if (options.onload) - options.onload.call(self); -}); -}; - -// Disable input. -SegmentAnnotator.prototype.disable = function() { - this.layers.highlight.canvas.display = 'none'; - return this; -}; - -// Enable input. -SegmentAnnotator.prototype.enable = function() { - this.layers.highlight.canvas.display = 'block'; - return this; -}; - -/** Set the current label to annotate. - * - * It can be an numeric index for the label definition or the name of the - * label. - */ -SegmentAnnotator.prototype.setCurrentLabel = function(label) { - var index = label; - if (typeof label == 'string') - for (var i = 0; i < this.labels.length; ++i) - if (this.labels[i].name == label) { - index = i; - break; - } - if (typeof index !== 'number' || index < 0 || index >= this.labels.length) - throw 'Invalid label: ' + label; - this.currentLabel = index; - return this; -}; - -// Get the current annotation label in a numeric index. -SegmentAnnotator.prototype.getCurrentLabel = function() { - return this.currentLabel; -}; - -/** Get the current label definitions. - * - * The return value is an array: - * [{ name: 'label', color: [r, g, b] }, ...] - */ -SegmentAnnotator.prototype.getLabels = function() { - return this.labels.slice(0); -}; - -/** Reset the label definitions. - * - * It can take an array of strings or array of objects of this format: - * [{ name: 'label', color: [r, g, b] }, ...] - * This method will not translate existing annotations. - */ -SegmentAnnotator.prototype.setLabels = function(newLabels) { - this._initializeColorMap(newLabels); - this._renderAnnotation(); - return this; -}; - -// Remove a label. -SegmentAnnotator.prototype.removeLabel = function(index) { - var newLabels = [], - i; - for (i = 0; i < this.labels.length; ++i) - if (i !== index) - newLabels.push(this.labels[i]); - this._initializeColorMap(newLabels); - for (i = 0; i < this.segments; ++i) { - var value = this.annotations[i]; - if (value == index) - this.annotations[i] = 0; - else if (value > index) - --this.annotations[i]; - } - this._renderAnnotation(); - return this; -}; - -// Set the alpha value for the image layer. -SegmentAnnotator.prototype.setImageAlpha = function(alpha) { - if (alpha === undefined) - alpha = 255; - var context = this.layers.image.canvas.getContext('2d'), - data = this.layers.image.image.data; - for (var i = 3; i < data.length; i += 4) - data[i] = alpha; - context.putImageData(this.layers.image.image, 0, 0); - return this; -}; - -// Set the alpha value for the segment boundary. -SegmentAnnotator.prototype.setBoundaryAlpha = function(alpha) { - if (alpha === undefined) - alpha = this.boundaryAlpha; - this._setAnnotationAlpha(alpha, true); - return this; -}; - -// Set the alpha value for the segment fill. -SegmentAnnotator.prototype.setFillAlpha = function(alpha) { - if (alpha === undefined) - alpha = this.fillAlpha; - this._setAnnotationAlpha(alpha, false); - return this; -}; - -// Set annotation. -SegmentAnnotator.prototype.setAnnotation = function(imageURL, callback) { - this.layers.highlight.canvas.display = 'none'; - var _this = this; - this._importAnnotation(imageURL, function() { - _this._renderAnnotation(); - _this.layers.highlight.canvas.display = 'block'; - if (typeof callback === 'function') callback(self); - }); - return this; -}; - -// Get annotation as a PNG data URL. -SegmentAnnotator.prototype.getAnnotation = function() { - var canvas = document.createElement('canvas'); - canvas.width = this.width; - canvas.height = this.height; - var context = canvas.getContext('2d'), - imageData = context.getImageData(0, 0, canvas.width, canvas.height), - data = imageData.data; - for (var i = 0; i < this.indexMap.length; ++i) { - var label = this.annotations[this.indexMap[i]]; - data[4 * i + 0] = label & 255; - data[4 * i + 1] = (label >>> 8) & 255; - data[4 * i + 2] = (label >>> 16) & 255; - data[4 * i + 3] = 255; - } - context.putImageData(imageData, 0, 0); - return canvas.toDataURL(); -}; - -// Given mouse coordinates, get an index of the segment. -SegmentAnnotator.prototype._getSegmentIndex = function(event) { - var x = event.pageX - this.container.offsetLeft + this.container.scrollLeft, - y = event.pageY - this.container.offsetTop + this.container.scrollTop; - x = Math.max(Math.min(x, this.layers.highlight.canvas.width - 1), 0); - y = Math.max(Math.min(y, this.layers.highlight.canvas.height - 1), 0); - return this.indexMap[y * this.layers.highlight.canvas.width + x]; -}; - -// Update highlight layers.highlight.canvas. -SegmentAnnotator.prototype._updateHighlight = function(index) { - var data = this.layers.highlight.image.data, - i, - pixels; - if (this.currentSegment !== null) { - pixels = this.pixelsMap[this.currentSegment]; - for (i = 0; i < pixels.length; ++i) - data[4 * pixels[i] + 3] = 0; - } - this.currentSegment = index; - if (this.currentSegment !== null) { - pixels = this.pixelsMap[this.currentSegment]; - for (i = 0; i < pixels.length; ++i) - data[4 * pixels[i] + 3] = this.highlightAlpha; - } - this.layers.highlight.canvas - .getContext('2d') - .putImageData(this.layers.highlight.image, 0, 0); - return this; -}; - -// Update label. -SegmentAnnotator.prototype._updateAnnotation = function(index, render) { - if (render && this.annotations[index] === this.currentLabel) - return; - var data = this.layers.annotation.image.data, - pixels = this.pixelsMap[index]; - this.annotations[index] = this.currentLabel; - for (var i = 0; i < pixels.length; ++i) { - var offset = 4 * pixels[i], - color = this.labels[this.currentLabel].color; - data[offset + 0] = color[0]; - data[offset + 1] = color[1]; - data[offset + 2] = color[2]; - } - if (render) - this.layers.annotation.canvas - .getContext('2d') - .putImageData(this.layers.annotation.image, 0, 0); - return this; -}; - -// Initialize pixels index. -SegmentAnnotator.prototype._initializePixelsIndex = function() { - var i; - this.pixelsMap = new Array(this.segments); - for (i = 0; i < this.segments; ++i) - this.pixelsMap[i] = []; - for (i = 0; i < this.indexMap.length; ++i) - this.pixelsMap[this.indexMap[i]].push(i); - return this; -}; - -// Initialize a color map. -SegmentAnnotator.prototype._initializeColorMap = function(newLabels) { - // Calculate RGB value of HSV input. - function hsv2rgb(h, s, v) { - var i = Math.floor(h * 6), - f = h * 6 - i, - p = v * (1 - s), - q = v * (1 - f * s), - t = v * (1 - (1 - f) * s), - r, - g, - b; - switch(i % 6) { - case 0: r = v; g = t; b = p; break; - case 1: r = q; g = v; b = p; break; - case 2: r = p; g = v; b = t; break; - case 3: r = p; g = q; b = v; break; - case 4: r = t; g = p; b = v; break; - case 5: r = v; g = p; b = q; break; - } - return [Math.round(r*255), Math.round(g*255), Math.round(b*255)]; - } - // Calculate a color value in the range input. [white, hsv() ...] - function pickColor(index, range) { - if (index === 0) - return [255, 255, 255]; - else - return hsv2rgb((index - 1) / Math.max(1, range - 1), 1, 1); - } - - if (newLabels === undefined) { - this.labels = [ - { name: 'background', color: [255, 255, 255] }, - { name: 'foreground', color: [255, 0, 0] } - ]; - } - else { - if (typeof newLabels !== 'object') - throw 'Labels must be an array'; - if (newLabels.length < 1) - throw 'Empty labels'; - var uncolored = 0, - index = 0, - i; - this.labels = newLabels.slice(0); - for (i = 0; i < this.labels.length; ++i) { - if (typeof this.labels[i] === 'string') - this.labels[i] = { name: this.labels[i] }; - if (this.labels[i].color === undefined) - ++uncolored; - } - for (i = 0; i < this.labels.length; ++i) - if (this.labels[i].color === undefined) - this.labels[i].color = hsv2rgb((index++) / - Math.max(1, uncolored), 1, 1); - } - return this; -}; - -// Import existing annotation data. -SegmentAnnotator.prototype._importAnnotation = function(url, callback) { - var image = new Image(), - _this = this; - image.src = url; - image.onload = function() { - var canvas = document.createElement('canvas'); - canvas.width = _this.width; - canvas.height = _this.height; - var context = canvas.getContext('2d'); - context.drawImage(image, 0, 0, _this.width, _this.height); - var sourceData = context.getImageData(0, 0, _this.width, _this.height) - .data; - // For each segment, assign the dominant label. - var label; - for (var i = 0; i < _this.segments; ++i) { - var pixels = _this.pixelsMap[i], - histogram = {}; - for (var j = 0; j < pixels.length; ++j) { - var offset = 4 * pixels[j]; - label = sourceData[offset + 0] | - (sourceData[offset + 1] << 8) | - (sourceData[offset + 2] << 16); - var count = histogram[label] || 0; - histogram[label] = ++count; - } - var dominantLabel = sourceData[4 * pixels[0]]; - for (label in histogram) - if (histogram[label] > histogram[dominantLabel]) - dominantLabel = label; - if (dominantLabel >= _this.labels.length) - dominantLabel = 0; - _this.annotations[i] = dominantLabel; - } - _this.currentLabel = 0; - callback.call(this); - }; - return this; -}; - -// Initialize pixels index. -SegmentAnnotator.prototype._initializeAnnotations = function(url, callback) { - this.annotations = new Array(this.segments); - if (url === undefined) { - for (var i = 0; i < this.segments; ++i) - this.annotations[i] = 0; - callback.call(this); - } - else { - this._importAnnotation(url, callback); - } - return this; -}; - -// Render annotation layer. -SegmentAnnotator.prototype._renderAnnotation = function() { - var current = this.currentLabel; - if (current >= this.labels.length) - current = 0; - var context = this.layers.annotation.canvas.getContext('2d'); - for (var i = 0; i < this.segments; ++i) { - this.currentLabel = this.annotations[i]; - if (this.currentLabel >= this.labels.length) - this.currentLabel = 0; - this._updateAnnotation(i, false); - } - context.putImageData(this.layers.annotation.image, 0, 0); - this.currentLabel = current; - return this; -}; - -// Create an empty canvas layer. -SegmentAnnotator.prototype._createLayer = function() { - var canvas = document.createElement('canvas'); - canvas.style.position = 'absolute'; - canvas.style.left = '0px'; - canvas.style.top = '0px'; - canvas.width = this.width; - canvas.height = this.height; - this.container.appendChild(canvas); - return canvas; -}; - -// Initialize the annotation layer. -SegmentAnnotator.prototype._initializeAnnotationLayer = function() { - var canvas = this._createLayer(), - context = canvas.getContext('2d'), - imageData = context.getImageData(0, 0, this.width, this.height); - this.layers.annotation.canvas = canvas; - this.layers.annotation.image = imageData; - this._renderAnnotation(); - this._setAnnotationAlpha(this.boundaryAlpha, true); - this._setAnnotationAlpha(this.fillAlpha, false); - return this; -}; - -// Initialize the background layer. -SegmentAnnotator.prototype._initializeBackgroundLayer = function() { - var canvas = this._createLayer(), - context = canvas.getContext('2d'), - imageData = context.createImageData(this.width, this.height), - data = imageData.data, - color = this.backgroundColor; - for (var i = 0; i < data.length; i += 4) { - data[i + 0] = color[0]; - data[i + 1] = color[1]; - data[i + 2] = color[2]; - data[i + 3] = 255; - } - context.putImageData(imageData, 0, 0); - return this; -}; - -// Initialize the image layer. -SegmentAnnotator.prototype._initializeImageLayer = function() { - var canvas = this._createLayer(), - context = canvas.getContext('2d'), - imageData = context.createImageData(this.width, this.height); - imageData.data.set(this.rgbData); - context.putImageData(imageData, 0, 0); - this.layers.image.canvas = canvas; - this.layers.image.image = imageData; - return this; -}; - -// Initialize the highlight layer. -SegmentAnnotator.prototype._initializeHighlightLayer = function() { - var canvas = this._createLayer(); - canvas.style.cursor = 'pointer'; - canvas.oncontextmenu = function() { return false; }; - var context = canvas.getContext('2d'), - imageData = context.createImageData(this.width, this.height), - data = imageData.data; - for (var i = 0; i < data.length; i += 4) { - data[i + 0] = 255; - data[i + 1] = 255; - data[i + 2] = 255; - data[i + 3] = 0; - } - this.layers.highlight.canvas = canvas; - this.layers.highlight.image = imageData; - var mousestate = { down: false, button: 0 }, - _this = this; - // On mousemove or mouseup. - function updateIfActive(event) { - var segmentId = _this._getSegmentIndex(event); - _this._updateHighlight(segmentId); - if (mousestate.down) { - var label = _this.currentLabel; - if (mousestate.button == 2) - _this.currentLabel = 0; - _this._updateAnnotation(segmentId, true); - _this.currentLabel = label; - } - } - this.layers.highlight.canvas.addEventListener('mousemove', updateIfActive); - this.layers.highlight.canvas.addEventListener('mouseup', updateIfActive); - // Mouseleave. - this.layers.highlight.canvas.addEventListener('mouseleave', function(event) { - _this._updateHighlight(null); - }); - // Mousedown. - this.layers.highlight.canvas.addEventListener('mousedown', function(event) { - mousestate.down = true; - mousestate.button = event.button; - }); - // Mouseup. - window.addEventListener('mouseup', function(event) { - mousestate.down = false; - }); - return this; -}; - -// Set alpha value at the annotation layer. -SegmentAnnotator.prototype._setAnnotationAlpha = function(alpha, atBoundary) { - var context = this.layers.annotation.canvas.getContext('2d'), - indexMap = this.indexMap, - width = this.width, - height = this.height, - data = this.layers.annotation.image.data; - for (var i = 0; i < height; ++i) { - for (var j = 0; j < width; ++j) { - var index = indexMap[i * width + j], - isBoundary = (i === 0 || - j === 0 || - i === (height - 1) || - j === (width - 1) || - index !== indexMap[i * width + j - 1] || - index !== indexMap[i * width + j + 1] || - index !== indexMap[(i - 1) * width + j] || - index !== indexMap[(i + 1) * width + j]); - // if current pixel is part of the boundary - if (isBoundary && atBoundary) - data[4 * (i * width + j) + 3] = alpha; - // if current pixel is part of the superpixel, then fill it - else if (!isBoundary && !atBoundary) - data[4 * (i * width + j) + 3] = alpha; - } - } - context.putImageData(this.layers.annotation.image, 0, 0); - return this; -}; - -// Add style to the container element. -SegmentAnnotator.prototype._initializeContainer = function(container) { - if (container) - this.container = container; - else { - this.container = document.createElement('div'); - document.body.appendChild(this.container); - } - this.container.innerHTML = ''; - this.container.style.position = 'relative'; - this.container.style.width = this.width; - this.container.style.height = this.height; - this.container.style.display = 'inline-block'; - return this; -}; - -/** Create an annotation tool based on PFSegmentation. - * - * Include pf-segmentation.js before use. - */ -PFSegmentAnnotator = function(imageURL, options) { - var _this = this; - PFSegmentation(imageURL, { - sigma: options.sigma, - threshold: options.threshold, - minSize: options.minSize, - callback: function(result) { - SegmentAnnotator.call(_this, result, options); - } - }); -}; - -// Set up inheritance. -PFSegmentAnnotator.prototype = Object.create(SegmentAnnotator.prototype); - -/** Create an annotation tool based on SLIC segmentation. - * - * Include slic-segmentation.js before use. - */ -SLICSegmentAnnotator = function(imageURL, options){ -var _this = this; - SLICSegmentation(imageURL, { - regionSize: options.regionSize, - callback: function(result) { - SegmentAnnotator.call(_this, result, options); - } - }); -}; - -// Set up inheritance. -SLICSegmentAnnotator.prototype = Object.create(SegmentAnnotator.prototype); - - -/** Create an annotation tool based on offline pre-segmentation. - * - * Include pre-segmentation.js before use. - */ -PreSegmentAnnotator = function(imageURL, options){ -var _this = this; - PreSegmentation(imageURL, { - regionSize: options.regionSize, - annotation: options.annotation, - callback: function(result) { - SegmentAnnotator.call(_this, result, options); - } - }); -}; - -// Set up inheritance. -PreSegmentAnnotator.prototype = Object.create(SegmentAnnotator.prototype);