Compare commits

...

71 Commits

Author SHA1 Message Date
Varun Gupta
f4315159a9 fix remove step UI (#288)
* fix remove step UI

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* 2.1.0
2018-06-08 15:46:32 -04:00
Varun Gupta
de20254637 2.0.0 (#287) 2018-06-08 14:31:50 -04:00
Varun Gupta
10c6ad5865 Run optimize (#275)
* fixes #269

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* update demo

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* refactor and comments

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* update readme

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* updated and refactored

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* update readme

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* trying to fix tests

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* update documentation

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* update to config.mode = test

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* finaly everything works

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* 1.8.0

* update test

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* add test for run

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>
2018-06-08 14:13:41 -04:00
Varun Gupta
a0f7e6c766 add importString and refactor (#283)
* add importString and refactor

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* update tests and minor changes

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* update importImage

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>
2018-06-06 21:21:53 -04:00
Varun Gupta
2b07ecdb36 Fix import image (#282)
* fix default image path

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* fix importImage to take proper input

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>
2018-06-06 20:03:50 -04:00
Jeffrey Warren
37122d4c28 test import string with no configs (#279)
* test import string with no configs

* now working without ()

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* update test

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* refactor for readability

* refactor for pipes | between multiple step settings

* fixed bugs

* test fixed
2018-06-03 13:22:40 -07:00
Varun Gupta
871453b857 Url params 🎉 (#276)
* basic implementation

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* fix bug

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* fix everything

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* add test and refactor

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* update demo

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* fix comma issue

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* update test

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* now doing with native encode

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>
2018-06-03 09:11:07 -07:00
Varun Gupta
ac183205f0 implement basic toString() (#272)
* implement basic toString()

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* refactor

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* remove load-image from string

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* more refactor

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* doing es5 way

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* final refactor

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* refactor

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* remove if else from map

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* fix function call

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* add sequencer.steps

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* add tests for toString and stepToString

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* fix toString final

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* fix build failing

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* refactored and fix tests

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* refactor tests

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>
2018-06-01 12:45:22 -04:00
Varun Gupta
be810fb9c4 add basic test and remove some bugs (#271)
Signed-off-by: tech4GT <varun.gupta1798@gmail.com>
2018-05-28 20:54:23 -04:00
Varun Gupta
8d3ca0887f fixes #267 dragZone listener getting confused (#270)
* fixes #267 dragZone listener getting confused

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* remove log

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>
2018-05-28 18:44:42 -04:00
Varun Gupta
1de72d7325 Prev step final API (#268)
* 1.7.0

* fix the api

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* fixes #266

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* finally got this to work! Phew :D

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* remove extra log

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* getStepAPI in insertStep

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* add getPreviousStep API

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* more api

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* changes and refactor

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* now using this.getStep and related functions

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* add getNextStep

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* minor refactor

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* fix

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* update getIndex

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>
2018-05-28 01:33:17 -04:00
Varun Gupta
58a4798674 ImportImage (#264)
* not working yet...

* fixed basic functioning!

* working! now on to blending
2018-05-24 17:18:48 -04:00
Varun Gupta
f8006da07e Blend module updated (#263)
* blend initial steps

* access to prior steps via passed util api

* Update Blend And add missing start script

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* blend complete

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>
2018-05-24 16:01:09 -04:00
Varun Gupta
09c4f250a2 add API to get steps (#259)
* add API to get steps

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* update dist and refactor

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* rename to getStepOutput

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* api complete

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* add test

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* change indentation to 2 spaces

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* minor fixes

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* new API

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* remove commented code

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* modifiication to invert

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>
2018-05-23 17:38:20 -07:00
Varun Gupta
bd490d2515 add serve grunt task (#258)
* add serve grunt task

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* add npm start

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* update readme

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* fix typo

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>
2018-05-22 10:56:46 -07:00
Varun Gupta
ca79924d11 fixes #254 (#257)
Signed-off-by: tech4GT <varun.gupta1798@gmail.com>
2018-05-22 03:04:14 -07:00
Jeffrey Warren
630eb773de start breaking out a ui feature to set an input step (#253)
* start

* refactoring

* working

* version bump
2018-05-09 13:49:11 -04:00
Jeffrey Warren
6ce78f87f4 ui output for average (#252) 2018-05-09 10:11:20 -04:00
Jeffrey Warren
991e9bb29c start to average module (#249)
* start to average module

* completed average module

* added average module, version bump
2018-05-09 09:39:33 -04:00
Jeffrey Warren
cbbfbff4bc bugfix version bump (#248) 2018-05-07 10:46:38 -04:00
Jeffrey Warren
768e2ce95d Ui refactor and drag to crop (#245)
* Drag to crop rebase step (#244)

* 1.1.0

* 1.2.0

* added plugin

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* drag to crop enabled

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* fix

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* improvement

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* done dragToCrop

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* done with updated ui

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* solved bug for multiple consecutive crops

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* fixed and updated

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* externalized image area select code

* major refactoring of crop drag and demo ui modules

* work on crop module and ui refactor

* completed Step drag ui refactor

* revert unbuilt to rebase

* built and version bump

* various fixes
2018-05-04 14:27:48 -04:00
Jeffrey Warren
51524f747a uses jquery if available in replace (#240) 2018-05-03 18:02:25 -04:00
Jeffrey Warren
b16b45afa8 removed jquery (#239)
* removed jquery

* version bump
2018-05-03 17:33:01 -04:00
Jeffrey Warren
39418da187 Format detection fix (#238)
* better format detection

* version
2018-05-03 16:35:02 -04:00
Jeffrey Warren
8547c60873 Update PixelManipulation.js (#236) 2018-05-02 17:09:09 -04:00
Jeffrey Warren
adc21ae843 Rename colormap (#235) 2018-05-02 16:08:50 -04:00
Jeffrey Warren
2abf0fae09 Code cleanup, re-organizing and tidying (#226)
* initial cleanup

* build

* additions and build
2018-04-29 12:16:11 -04:00
Jeffrey Warren
711ae62ee3 simplified demo ui (#224)
* simplified demo ui

* show description

* smaller, labelled example image
2018-04-28 18:40:34 -04:00
Jeffrey Warren
4d75fb9640 Infos updated for new title/description format (#221)
* initial work

* working with failing test

* info modules

* compiled

* rework of CONTRIBUTING
2018-04-24 17:15:27 -04:00
Marzanna
5ac3ef008d demo images can be downloaded by clicking (#220)
* demo images can be downloaded by clicking

* modified download to extract image type from url

* refactored image output into standalone function

* updated to call fileExtension function
2018-04-20 19:56:32 -04:00
Varun Gupta
98f913b32e Material icon (#217)
* add pwa icon

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* icon working

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>
2018-04-20 14:07:52 -04:00
Jeffrey Warren
c2756ffdbb Module info.json -based demo UI (WIP) (#219)
* initial work

* working with failing test

* now should pass test
2018-04-20 09:34:08 -04:00
Varun Gupta
d887f5eb61 Update version (#208)
* 1.1.0

* 1.2.0

* check in dist

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>
2018-04-09 16:53:40 -04:00
Varun Gupta
3b791ad58b add loading spinner fallback (#202)
* add loading spinner fallback fixes #189

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* handle testing with progress spinners

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* apply fixes and improvements

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* add example

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* bug fixes

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* resolves #189

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* fix

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>
2018-03-31 01:21:28 -04:00
Varun Gupta
fba80bb151 Dynamic fix as fun (#207)
getNeighborPixel(x, y) method for Dynamic module
2018-03-30 17:41:30 -04:00
Varun Gupta
0ceb36ffde 1.1.0 (#206) 2018-03-24 10:06:50 -04:00
Jeffrey Warren
edaa8895c7 Update info.json with NDVI description (#199)
* Update info.json

* compiled
2018-03-12 13:13:43 -04:00
Raounak Sharma
efd5bf7624 Changed the warning log (#195)
Since our execution is not exactly async, hence the corresponding change in
the log message is more suitable log.

close 194
2018-03-09 17:14:03 -05:00
Jeffrey Warren
96143564fa Update index.html 2018-03-01 13:39:29 -06:00
Gayathri
973ec6d6e4 Added how to run the debug script (#192) 2018-02-26 10:53:39 -05:00
Danielle Shwed
801ea35393 Add Saturation Module (#193)
* fixed blur module as well as issue with no output when running debug

* added saturation module

* fixing requested changes

* fixing exportBin

* requested fixes
2018-02-26 10:52:25 -05:00
Varun Gupta
e8ab480687 Added native gaussian Blurring module(no dependencies) (#186)
* Added Blur Module

* Issue 177 fixing Blur module

* blur module fixed info json

* fixing blur module

* Delete .DS_Store

* add a native blurring solution

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* fixed package.json

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* delete .ds_store and update gitignore

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>
2018-02-22 16:43:14 -05:00
Varun Gupta
9d3e511bca resolves #184 (#185) 2018-02-21 22:10:25 -05:00
mayank1010
d3a4c336a0 Empty directory bug fixed (#179)
added the check to test if the directory is empty by comparing it's length with 0.
2018-02-21 14:55:24 -05:00
Varun Gupta
0fd797ed5a resolves #182 (#183)
Signed-off-by: tech4GT <varun.gupta1798@gmail.com>
2018-02-21 11:31:20 -05:00
Varun Gupta
27ee18b3d2 resolves #153 (#180)
Signed-off-by: tech4GT <varun.gupta1798@gmail.com>
2018-02-20 17:28:19 -05:00
Varun Gupta
5922b0de58 resolves #174 (#175)
Update readme -- resolves #174
2018-02-16 18:36:21 -05:00
Varun Gupta
fd45d3caf3 resolves #132 fix options object (#171)
* resolves #132 fix options object

* Add Documentation for --details parameter

* fix spacing issue

* update naming of options parameter

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>
2018-02-16 18:03:39 -05:00
Varun Gupta
bbef4bca57 Added Edge Detection module (#168)
* step 1 grayscale

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* add basic edge detection resolves #166

* updated the description

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* fixed typo

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* updated the description for edge detection module

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* add NonMaxSuppression and DoubleThreshold

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* comlpeted edge detection

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* cleared remaining weak edges

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>
2018-02-16 17:07:00 -05:00
Varun Gupta
32c5a29906 resolves #131 create output directory immediately (#167)
* resolves #131 create output directory immediately

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* fix error if no steps are passed

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* add exit message if steps not passed

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* add test for creating output directory
2018-02-06 16:42:22 -05:00
Varun Gupta
c5dee8aef3 Add bookmarklet (#164)
* resolves #162

* add bookmarklet link to readme resolves #148

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* Update README.md
2018-02-02 12:52:08 -05:00
Varun Gupta
071aa68de7 resolves #162 (#163)
remove module count test - resolves #162
2018-02-01 13:37:58 -05:00
Varun Gupta
87fa166595 Only brightness module (#161)
* fixed package.json

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* resolves #113 add brightness module

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>
2018-02-01 12:27:37 -05:00
Varun Gupta
a5d5dd9f52 fixed package.json (#160)
Signed-off-by: tech4GT <varun.gupta1798@gmail.com>
2018-02-01 11:55:01 -05:00
Rishabh Chaudhary
38accebcc3 Fixed - move module tests into separate test files #141 (#155)
* Fixed -  move module tests into separate test files #141

* Modification

* Restructure, fixed tests
2018-01-29 13:18:15 -05:00
Varun Gupta
c19a663fa2 resolves #133 add -b --basic mode to only output final image update readme (#154)
* resolves #133 add -b --basic mode to only output final image update readme

Signed-off-by: tech4GT <varun.gupta1798@gmail.com>

* format logs and help

* update dist files
2018-01-23 12:26:02 -05:00
Varun Gupta
38cf05bb9e resolves #129 message to user informing about the async execution (#152)
Signed-off-by: tech4GT <varun.gupta1798@gmail.com>
2018-01-21 10:26:37 -05:00
Chinmay Pandhare
3f99e2b44c fixed replaceImage (#151)
* 1.0.0

* Fixed Replace Image

* Added test to check if replaceImage works
2017-10-26 14:35:23 -04:00
Jeffrey Warren
73e5d14569 various tweaks and adjustments to demo; bugfix for select/input changes (#149) 2017-10-23 23:16:22 -04:00
Chinmay Pandhare
135c142ba8 Updates to Demo (#143)
* 1.0.0

* Added 'select' input

* Added loader

* Fix File Reader
2017-10-18 17:02:42 -04:00
Jeffrey Warren
14c59be19c Additions to README - diagrams (#144)
* Additions to README

* Update README.md

* Add files via upload
2017-10-18 16:32:33 -04:00
Jeffrey Warren
ec10ab0b0d Update imageSelection.js 2017-10-18 16:28:23 -04:00
Jeffrey Warren
1c7f59cbfd Dynamic module, new demo image, demo cleanup, re-configurable demo steps (#139)
* rebase and build

* bump version to v0.1.1
2017-10-13 21:30:06 -04:00
Jeffrey Warren
bef53ef1a2 Step descriptions (work in progress) (#135)
* additional changes, not sure of yet

* working, but probably redundant code

* remove comment line

* rebuild
2017-10-13 13:16:07 -04:00
Jeffrey Warren
1c7f7c15af assortment of tweaks, additional tests (#134) 2017-10-05 15:16:25 -06:00
makeupsomething
7cbf793655 Added uglify command to build function (#124) 2017-10-05 14:29:46 -06:00
Jeffrey Warren
829e19e58d various fixes and changes to SegmentedColormap module (#128) 2017-10-04 14:31:43 -04:00
Jeffrey Warren
ca6910017d url hash initial implementation (#125)
* url hash initial implementation

* completed url hash UI
2017-10-02 08:37:10 -04:00
Jeffrey Warren
4e780f3a91 drag drop added to demo (#121) 2017-09-11 20:30:50 -04:00
Jeffrey Warren
3a4e4f9575 Merge pull request #117 from publiclab/readme-fixes
Update README.md with fixed Contributing links
2017-09-10 13:11:25 -04:00
Jeffrey Warren
6420a70aad Update CONTRIBUTING.md 2017-09-10 13:11:07 -04:00
89 changed files with 36304 additions and 22818 deletions

2
.gitignore vendored
View File

@@ -33,6 +33,8 @@ node_modules/*
# Optional REPL history
.node_repl_history
.DS_Store
*.swp
todo.txt
test.js

View File

@@ -12,26 +12,46 @@ Most contribution (we imagine) would be in the form of API-compatible modules, w
* [Info File](#info-file)
* [Ideas](#ideas)
****
## Contribution ideas
See [this issue](https://github.com/publiclab/image-sequencer/issues/118) for a range of ideas for new contributions, and links to possibly helpful libraries. Also see the [new features issues list](https://github.com/publiclab/image-sequencer/labels/new-feature).
### Bugs
If you find a bug please list it here, and help us develop Image Sequencer by [opening an issue](https://github.com/publiclab/image-sequencer/issues/new)!
****
## Contributing modules
Any module must look like this :
Most contributions can happen in modules, rather than to core library code. Modules and their [corresponding info files](#info-file) are included into the library in this file: https://github.com/publiclab/image-sequencer/blob/master/src/Modules.js#L5-L7
Module names, descriptions, and parameters are set in the `info.json` file -- [see below](#info-file).
Any module must follow this basic format:
```js
module.exports = function ModuleName(options,UI) {
options = options || {};
options.title = "Title of the Module";
UI.onSetup(options.step);
var output;
function draw(input,callback) {
UI.onDraw(options.step);
var output = /*do something with the input*/ ;
UI.onDraw(options.step); // tell the UI to "draw"
this.output = output;
var output = function(input){
/* do something with the input */
return input;
}
this.output = output(input); // run the output and assign it to this.output
options.step.output = output.src;
callback();
UI.onComplete(options.step);
UI.onComplete(options.step); // tell UI we are done
}
return {
@@ -43,6 +63,7 @@ module.exports = function ModuleName(options,UI) {
}
```
### options
The object `options` stores some important information. This is how you can accept
@@ -54,6 +75,19 @@ whether your module is being run on a browser.
### draw()
To add a module to Image Sequencer, it must have a `draw` method; you can wrap an existing module to add them:
* `module.draw(input, callback)`
The `draw` method should accept an `input` parameter, which will be an object of the form:
```js
input = {
src: "datauri of an image here",
format: "jpeg/png/etc"
}
```
The draw method is run every time the step is `run` using `sequencer.run()`.
So any calculations must go **into** the `draw()` method's definition.
@@ -69,17 +103,17 @@ constant definitions must be done **outside** the `draw()` method's definition.
format: "<png|jpeg|gif>"
}
```
* `callback` is a function which is responsible to tell the sequencer that the
step has been "drawn".
* `callback` is a function which is responsible to tell the sequencer that the step has been "drawn".
When you have done your calculations and produced an image output, you are required
to set `this.output` to an object similar to what the input object was, call
`callback()`, and set `options.step.output` equal to the output DataURL
When you have done your calculations and produced an image output, you are required to set `this.output` to an object similar to what the input object was, call `callback()`, and set `options.step.output` equal to the output DataURL
* `progressObj` is an optional additional Object that can be passed in the format `draw(input, callback, progressObj)`, which handles the progress output; see [Progress reporting](#progress-reporting) below.
### UI Methods
The module is responsible to emit various events for the UI to capture. There are
four events in all:
The module is responsible for emitting various events for the UI to capture.
There are four events in all:
* `UI.onSetup(options.step)` must be emitted when the module is added. So it must be emitted outside the draw method's definition as shown above.
* `UI.onDraw(options.step)` must be emitted whenever the `draw()` method is called. So it should ideally be the first line of the definition of the `draw` method.
@@ -87,29 +121,19 @@ four events in all:
is ready. An argument, that is the DataURL of the output image must be passed in.
* `UI.onRemove(options.step)` is emitted automatically and the module should not emit it.
To add a module to Image Sequencer, it must have the following method; you can wrap an existing module to add them:
### Name and description
* `module.draw()`
The `draw(input,callback)` method should accept an `input` parameter, which will be an object of the form:
```js
input = {
src: "datauri here",
format: "jpeg/png/etc"
}
```
### options.title
For display in the web-based UI, each module may also have a title `options.title`.
For display in the web-based demo UI, set the `name` and `description` fields in the `info.json` file for the module.
## Info file
All module folders must have an `info.json` file which looks like the following:
```json
{
"name": "Name of Module to be displayed",
"description": "Optional longer text explanation of the module's function",
"url": "Optional link to module's source code or documentation",
"inputs": {
"var1": {
"type": "text",
@@ -140,126 +164,56 @@ Similarly, "Select" type inputs should have a `values` array.
Also, A module may have output values. These must be defined as shown above.
### Progress reporting
The default "loading spinner" can be optionally overriden with a custom progress object to draw progress on the CLI, following is a basic module format for the same:
```js
module.exports = function ModuleName(options,UI) {
options = options || {};
UI.onSetup(options.step);
var output;
function draw(input,callback,progressObj) {
/* If you wish to supply your own progress bar you need to override progressObj */
progressObj.stop() // Stop the current progress spinner
progressObj.overrideFlag = true; // Tell image sequencer that you will supply your own progressBar
/* Override the object and give your own progress Bar */
progressObj = /* Your own progress Object */
UI.onDraw(options.step);
var output = function(input){
/* do something with the input */
return input;
};
this.output = output();
options.step.output = output.src;
callback();
UI.onComplete(options.step);
}
return {
options: options,
draw: draw,
output: output,
UI: UI
}
}
```
The `progressObj` parameter of `draw()` is not consumed unless a custom progress bar needs to be drawn, for which this default spinner should be stopped with `progressObj.stop()` and image-sequencer is informed about the custom progress bar with `progressObj.overrideFlag = true;` following which this object can be overriden with custom progress object.
### Module example
See existing module `green-channel` for an example: https://github.com/publiclab/image-sequencer/tree/master/src/modules/GreenChannel/Module.js
The `green-channel` module is included into the core modules here: https://github.com/publiclab/image-sequencer/blob/master/src/Modules.js#L5-L7
For help integrating, please open an issue.
## Ideas
Notes on development next steps:
### UI
* [ ] spinners before panels are complete
* [ ] click to expand for all images
### Modules
* [ ] remotely includable modules, not compiled in -- see plugin structures in other libs
* [ ] `sequencer.addModule('path/to/module.js')` style module addition -- also to avoid browserifying all of Plotly :-P
### Core Functionality
* [ ] allow passing data as data-uri or Image object, or stream, or ndarray or ImageData array, if both of neighboring pair has ability?
* [ ] (for node-and-line style UIs) non-linear sequences with Y-splitters
### Testing
* [ ] comparisons with diff
* [ ] testing a module's promised functionality: each module could offer before/after images as part of their API; by running the module on the before image, you should get exactly the after image, comparing with an image diff
### Use cases
* [ ] make an Infragram module that accepts a math expression
### Bugs
If you find a bug please list it here, and help us develop Image Sequencer by opening an issue!
## Module Candidates, related Ideas
* https://github.com/linuxenko/rextract.js
* https://www.npmjs.com/package/histogram
* https://github.com/hughsk/flood-fill
* https://github.com/yahoo/pngjs-image has lots of useful general-purpose image getters like `image.getLuminosityAtIndex(idx)`
* some way to add in a new image (respecting alpha) -- `add-image` (with blend mode, default `normal`?)
* https://github.com/yuta1984/CannyJS - edge detection
* http://codepen.io/taylorcoffelt/pen/EsCcr - more edge detection
* https://github.com/jywarren/webgl-distort/ - an image distortion module
* https://github.com/jadnco/whirl - scrubbable image sequence player
* non graphics card GL functions could be shimmed with https://github.com/Overv/JSGL
* or this: https://github.com/stackgl/headless-gl
* https://github.com/mattdesl/fontpath-simple-renderer
### Referencing earlier states
Complex sequences with masking could require accessing previous states (or nonlinearity):
* flood-fill an area
* select only the flooded area
* roundabout: lighten everything to <50%, then flood-fill with black? Not 100% reliable.
* roundabout 2: `flood fill`, then `blink-diff` with original
* then add step which recovers original image, repeat `flood-fill`/`blink-diff` for second region
* reference above masked states in a `mask` module, with `maskModule.draw(image, { getMask: function() { return maskImg } })`
**Notes:**
`pattern-fill` module to use patterns in JS canvas:
```js
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
var img=document.getElementById("lamp");
var pat=ctx.createPattern(img,"repeat");
ctx.rect(0,0,150,100);
ctx.fillStyle=pat;
ctx.fill();
```
Masking:
```js
ctx.save();
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(160, 600);
ctx.rect(0, 0, 160, 600);
ctx.closePath();
ctx.clip();
ctx.drawImage(img, 0, 0);
ctx.restore();
```
****
## UI notes:
* visual nodes-and-lines UI: https://github.com/flowhub/the-graph
* https://flowhub.github.io/the-graph/examples/demo-simple.html
```js
settings: {
'threshold': {
type: 'slider',
label: 'Threshold',
default: 50,
min: 0,
max: 100
},
'colors': {
type: 'select',
label: 'Colors',
options: [
{ name: '0', value: '0', default: true },
{ name: '1', value: '1' },
{ name: '2', value: '2' }
]
}
}
```
Possible web-based commandline interface: https://hyper.is/?

View File

@@ -1,41 +1,50 @@
module.exports = function(grunt) {
module.exports = function (grunt) {
grunt.loadNpmTasks("grunt-browserify");
grunt.loadNpmTasks("grunt-contrib-uglify-es");
grunt.loadNpmTasks("grunt-browser-sync");
grunt.loadNpmTasks('grunt-browserify');
require("matchdep")
.filterDev("grunt-*")
.forEach(grunt.loadNpmTasks);
require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks);
grunt.initConfig({
pkg: grunt.file.readJSON("package.json"),
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
watch: {
options : {
livereload: true
},
source: {
files: [
'src/*.js',
'src/*/*.js',
'Gruntfile.js'
],
tasks: [ 'build:js' ]
}
watch: {
options: {
livereload: true
},
browserify: {
dist: {
src: ['src/ImageSequencer.js'],
dest: 'dist/image-sequencer.js'
}
source: {
files: ["src/*.js", "src/*/*.js", "src/*/*/*.js", "Gruntfile.js"],
tasks: ["build:js"]
}
},
});
browserify: {
dist: {
src: ["src/ImageSequencer.js"],
dest: "dist/image-sequencer.js"
}
},
/* Default (development): Watch files and build on change. */
grunt.registerTask('default', ['watch']);
grunt.registerTask('build', [
'browserify:dist'
]);
uglify: {
dist: {
src: ["./dist/image-sequencer.js"],
dest: "./dist/image-sequencer.min.js"
}
},
browserSync: {
dev: {
options: {
watchTask: true,
server: "./"
}
}
}
});
/* Default (development): Watch files and build on change. */
grunt.registerTask("default", ["watch"]);
grunt.registerTask("build", ["browserify:dist", "uglify:dist"]);
grunt.registerTask("serve", ["browserify:dist", "uglify:dist", "browserSync", "watch"]);
};

View File

@@ -5,11 +5,14 @@ Image Sequencer
## Why
Image Sequencer is different from other image processing systems in that it's non-destructive: instead of modifying the original image, it creates a new image at each step in a sequence. This is because it:
Image Sequencer is different from other image processing systems in that it's _non-destructive_: instead of modifying the original image, it **creates a new image at each step in a sequence**. This is because it:
* produces a legible trail of operations, to "show your work" for evidential, educational, or reproducibility reasons
* makes the creation of new tools or "modules" simpler -- each must accept an input image, and produce an output image
* allows many images to be run through the same sequence of steps
* works identically in the browser, on Node.js, and on the commandline
![workflow diagram](https://raw.githubusercontent.com/publiclab/image-sequencer/master/examples/images/diagram-workflows.png)
It is also for prototyping some other related ideas:
@@ -19,9 +22,13 @@ It is also for prototyping some other related ideas:
* cascading changes -- change an earlier step's settings, and see those changes affect later steps
* "small modules"-based extensibility: see [Contributing](https://github.com/publiclab/image-sequencer/blob/master/CONTRIBUTING.md)
## Examples:
## Examples
* [Single Image Demo](https://publiclab.github.io/image-sequencer/)
* [Simple Demo](https://publiclab.github.io/image-sequencer/)
A diagram of this running 5 steps on a single sample image may help explain how it works:
![example workflow](https://raw.githubusercontent.com/publiclab/image-sequencer/master/examples/images/diagram-6-steps.png)
## Jump to:
@@ -34,12 +41,15 @@ It is also for prototyping some other related ideas:
* [Creating a User Interface](#creating-a-user-interface)
* [Contributing](https://github.com/publiclab/image-sequencer/blob/master/CONTRIBUTING.md)
* [Submit a Module](https://github.com/publiclab/image-sequencer/blob/master/CONTRIBUTING.md#contributing-modules)
* [Get Demo Bookmarklet](https://publiclab.org/w/imagesequencerbookmarklet)
## Installation
This library works in the browser, in Node, and on the commandline (CLI), which we think is great. You can start a local environement to test the UI with `npm start`
### Browser
Just include [image-sequencer.js](https://publiclab.github.io/image-sequencer/dist/image-sequencer.js) in the Head section of your web page.
Just include [image-sequencer.js](https://publiclab.github.io/image-sequencer/dist/image-sequencer.js) in the Head section of your web page. See the [demo here](https://publiclab.github.io/image-sequencer/)!
### Node (via NPM)
@@ -48,12 +58,19 @@ Add `image-sequencer` to your list of dependancies and run `$ npm install`
### CLI
(You should have Node.JS and NPM for this.)
Globally install Image Sequencer:
```
$ npm install image-sequencer -g
```
(You should have Node.JS and NPM for this.)
### To run the debug script
```
$ npm run debug invert
```
## Quick Usage
@@ -84,8 +101,9 @@ Image Sequencer also provides a CLI for applying operations to local files. The
-i | --image [PATH/URL] | Input image URL. (required)
-s | --step [step-name] | Name of the step to be added. (required)
-b | --basic | Basic mode only outputs the final image
-o | --output [PATH] | Directory where output will be stored. (optional)
-op | --opions {object} | Options for the step. (optional)
-c | --config {object} | Options for the step. (optional)
The basic format for using the CLI is as follows:
@@ -103,6 +121,22 @@ The CLI also can take multiple steps at once, like so:
But for this, double quotes must wrap the space-separated steps.
Options for the steps can be passed in one line as json in the details option like
```
$ ./index.js -i [PATH] -s "brightness" -d '{"brightness":50}'
```
Or the values can be given through terminal prompt like
<img width="1436" alt="screen shot 2018-02-14 at 5 18 50 pm" src="https://user-images.githubusercontent.com/25617855/36202790-3c6e8204-11ab-11e8-9e17-7f3387ab0158.png">
The CLI is also chainable with other commands using `&&`
```
sequencer -i <Image Path> -s <steps> && mv <Output Image Path> <New path>
```
## Classic Usage
### Initializing the Sequencer
@@ -170,13 +204,35 @@ modules.
sequencer.run();
```
Additionally, an optional callback can be passed to this method.
Sequencer can be run with a custom config object
```js
sequencer.run(function(out){
// The config object enables custom progress bars in node environment and
// ability to run the sequencer from a particular index(of the steps array)
sequencer.run(config);
```
The config object can have the following keys
```js
config: {
progressObj: , //A custom object to handle progress bar
index: //Index to run the sequencer from (defaults to 0)
}
```
Additionally, an optional callback function can be passed to this method.
```js
sequencer.run(function callback(out){
// this gets called back.
// "out" is the DataURL of the final image.
});
sequencer.run(config,function callback(out){
// the callback is supported with all types of invocations
});
```
return value: **`sequencer`** (To allow method chaining)

55310
dist/image-sequencer.js vendored Normal file → Executable file

File diff suppressed because one or more lines are too long

1
dist/image-sequencer.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -31,7 +31,7 @@ h1 {
margin: 0 !important;
}
#imageSelect {
#dropzone {
padding: 30px;
margin: 0 20% 30px;
border: 4px dashed #ccc;
@@ -40,8 +40,12 @@ h1 {
color: #444;
}
#imageSelect input {
max-width: 100%;
.hover {
background: #eee;
}
#dropzone input {
max-width: 100%;
}
.step {
@@ -72,6 +76,15 @@ h1 {
margin: 10px 0;
}
.load {
padding: 30px;
background: #eee;
border-radius: 8px;
text-align: center;
font-size: 2em;
color: #444;
}
#addStep {
max-width: 500px;
margin: 20px auto;

38
examples/demo.js Normal file
View File

@@ -0,0 +1,38 @@
window.onload = function() {
sequencer = ImageSequencer();
// Load information of all modules (Name, Inputs, Outputs)
var modulesInfo = sequencer.modulesInfo();
// Add modules to the addStep dropdown
for (var m in modulesInfo) {
$("#addStep select").append(
'<option value="' + m + '">' + modulesInfo[m].name + "</option>"
);
}
// UI for each step:
sequencer.setUI(DefaultHtmlStepUi(sequencer));
// UI for the overall demo:
var ui = DefaultHtmlSequencerUi(sequencer);
sequencer.loadImage("images/tulips.png", ui.onLoad);
$("#addStep select").on("change", ui.selectNewStepUi);
$("#addStep button").on("click", ui.addStepUi);
$('body').on('click', 'button.remove', ui.removeStepUi);
// image selection and drag/drop handling from examples/lib/imageSelection.js
sequencer.setInputStep({
dropZoneSelector: "#dropzone",
fileInputSelector: "#fileInput",
onLoad: function onFileReaderLoad(progress) {
var reader = progress.target;
var step = sequencer.images.image1.steps[0];
step.output.src = reader.result;
sequencer.run(0);
step.options.step.imgElement.src = reader.result;
}
});
};

View File

@@ -29,6 +29,7 @@
}
</style>
<script src="../node_modules/jquery/dist/jquery.min.js"></script>
<script src="../dist/image-sequencer.js"></script>
</head>

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

BIN
examples/images/load.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

BIN
examples/images/monarch.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 KiB

BIN
examples/images/tulips.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 KiB

View File

@@ -3,219 +3,92 @@
<html>
<head>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="content-type" content="text/html; charset=UTF8">
<title>Image Sequencer</title>
<link href="../node_modules/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="../node_modules/font-awesome/css/font-awesome.min.css" rel="stylesheet">
<link rel="stylesheet" href="demo.css">
<script src="../node_modules/jquery/dist/jquery.min.js"></script>
<script src="../node_modules/bootstrap/dist/js/bootstrap.min.js"></script>
<script src="../dist/image-sequencer.js" charset="utf-8"></script>
</head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="content-type" content="text/html; charset=UTF8">
<link rel="icon" sizes="192x192" href="../icons/ic_192.png">
<body>
<title>Image Sequencer</title>
<div class="container-fluid">
<script src="../node_modules/jquery/dist/jquery.min.js"></script>
<script src="../node_modules/bootstrap/dist/js/bootstrap.min.js"></script>
<script src="../dist/image-sequencer.js" charset="utf-8"></script>
<script src="lib/urlHash.js" charset="utf-8"></script>
<script src="lib/defaultHtmlStepUi.js" charset="utf-8"></script>
<script src="lib/defaultHtmlSequencerUi.js" charset="utf-8"></script>
<script src="demo.js" charset="utf-8"></script>
<!-- for crop module: -->
<script src="../node_modules/imgareaselect/jquery.imgareaselect.dev.js"></script>
<header class="text-center">
<h1>Image Sequencer</h1>
<h3>
<a href="https://github.com/publiclab/image-sequencer"><i class="fa fa-github"></i></a>
</h3>
</header>
</head>
<div id="imageSelect">
<p><i>Select an image to start with!</i></p>
<center><input type="file" id="file" value=""></center>
</div>
<section id="steps" class="row"></section>
<body>
<section id="addStep">
<div class="row">
<select class="form-control">
<option value="none" disabled selected>Select a new step...</option>
</select>
</div>
<hr />
<div class="row">
<div id="options"></div>
</div>
<div class="row add">
<button class="btn btn-success btn-lg" name="add">Add Step</button>
</div>
</section>
<link href="../node_modules/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="../node_modules/font-awesome/css/font-awesome.min.css" rel="stylesheet">
<link href="../node_modules/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="../node_modules/font-awesome/css/font-awesome.min.css" rel="stylesheet">
<!-- for crop module: -->
<link href="../node_modules/imgareaselect/distfiles/css/imgareaselect-default.css" rel="stylesheet">
<link rel="stylesheet" href="demo.css">
<div class="container-fluid">
<header class="text-center">
<h1>Image Sequencer</h1>
<p>
A pure JavaScript sequential image processing system, inspired by storyboards. Instead of modifying the original image, it
creates a new image at each step in a sequence.
<a href="https://publiclab.org/image-sequencer">Learn more</a>
</p>
<p>
Open Source
<a href="https://github.com/publiclab/image-sequencer">
<i class="fa fa-github"></i>
</a> by
<a href="https://publiclab.org">Public Lab</a>
</p>
</header>
<div id="dropzone">
<p>
<i>Select or drag in an image to start!</i>
</p>
<center>
<input type="file" id="fileInput" value="">
</center>
</div>
<script type="text/javascript">
<section id="steps" class="row"></section>
// Initialize Image Sequencer
var sequencer = ImageSequencer();
<hr />
window.onload = function() {
<section id="addStep" class="panel panel-primary">
<div class="form-inline">
<div class="panel-body">
<div style="text-align:center;">
<select class="form-control input-lg" id="selectStep">
<option value="none" disabled selected>Select a new step...</option>
</select>
<button class="btn btn-success btn-lg" name="add">Add Step</button>
</div>
<br />
<p class="info" style="padding:8px;">Select a new module to add to your sequence.</p>
</div>
</div>
</section>
// Load information of all modules (Name, Inputs, Outputs)
var modulesInfo = sequencer.modulesInfo();
</div>
// Add modules to the addStep dropdown
for(var m in modulesInfo) {
$('#addStep select').append(
'<option value="'+m+'">'+modulesInfo[m].name+'</option>'
);
}
// Initial definitions
var steps = document.querySelector('#steps');
var parser = new DOMParser();
var reader = new FileReader();
initial = "images/grid.png";
// Set the UI in sequencer. This Will generate HTML based on
// Image Sequencer events :
// onSetup : Called every time a step is added
// onDraw : Called every time a step starts draw
// onComplete : Called every time a step finishes drawing
// onRemove : Called everytime a step is removed
// The variable 'step' stores useful data like input and
// output values, step information.
// See documetation for more details.
sequencer.setUI({
onSetup: function(step) {
step.ui = '\
<div class="row step">\
<div class="col-md-4 details">\
<h3>'+step.name+'</h3>\
<p><i>'+(step.description || '')+'</i></p>\
</div>\
<div class="col-md-8">\
<img alt="" class="img-thumbnail"/>\
</div>\
</div>\
';
var tools =
'<div class="tools btn-group">\
<button confirm="Are you sure?" class="remove btn btn-xs btn-default">\
<i class="fa fa-trash"></i>\
</button>\
</div>';
step.ui = parser.parseFromString(step.ui,'text/html');
step.ui = step.ui.querySelector('div.row');
step.imgElement = step.ui.querySelector('img');
if(sequencer.modulesInfo().hasOwnProperty(step.name)) {
var inputs = sequencer.modulesInfo(step.name).inputs;
var outputs = sequencer.modulesInfo(step.name).outputs;
var io = Object.assign(inputs,outputs);
for (var i in io) {
var div = document.createElement('div');
div.className = "row";
div.setAttribute('name',i);
div.innerHTML = "<div class='det'>" + i + ": " + "<span></span></div>";
step.ui.querySelector('div.details').appendChild(div);
}
}
if(step.name != "load-image")
step.ui.querySelector('div.details').appendChild(
parser.parseFromString(tools,'text/html')
.querySelector('div')
);
steps.appendChild(step.ui);
},
onComplete: function(step) {
step.imgElement.src = step.output;
if(sequencer.modulesInfo().hasOwnProperty(step.name)) {
var inputs = sequencer.modulesInfo(step.name).inputs;
var outputs = sequencer.modulesInfo(step.name).outputs;
for (var i in inputs) {
step.ui.querySelector('div[name="'+i+'"] span')
.innerHTML = step.options[i];
}
for (var i in outputs) {
step.ui.querySelector('div[name="'+i+'"] span')
.innerHTML = step[i];
}
}
},
onRemove: function(step) {
step.ui.remove();
}
});
sequencer.loadImage('images/grid.png',function(){
this.addSteps('invert').run();
});
$('#addStep select').on('change', function(){
$('#options').html('');
var m = $('#addStep select').val();
for(var input in modulesInfo[m].inputs) {
$('#options').append(
'<div class="row">\
<div class="col-md-5 labels">\
'+input+':\
</div>\
<div class="col-md-5">\
<input class="form-control" type="text" name="'+input+'" value="" placeholder="'+
modulesInfo[m].inputs[input].default+'"/>\
</div>\
</div>'
);
}
});
$('#addStep button').on('click',function(){
var options = {};
var inputs = $('#options input');
$.each(inputs, function() {
options[this.name] = $(this).val();
});
if($('#addStep select').val()=="none") return;
sequencer.addSteps($('#addStep select').val(),options).run();
});
$('body').on('click','button.remove',function(){
var index = $('button.remove').index(this) + 1;
sequencer.removeSteps(index).run();
});
$('#file').on('change',function(e){
var file = e.target.files[0];
if(!file) return;
reader.onload = function(){
var loadStep = sequencer.images.image1.steps[0];
loadStep.output.src = reader.result;
sequencer.run(0);
loadStep.options.step.imgElement.src = reader.result;
}
reader.readAsDataURL(file);
});
}
</script>
</body>
<script type="text/javascript">
$(function () {
var sequencer;
})
</script>
</body>
</html>

View File

@@ -0,0 +1,60 @@
function DefaultHtmlSequencerUi(_sequencer, options) {
options = options || {};
var addStepSel = options.addStepSel = options.addStepSel || "#addStep";
var removeStepSel = options.removeStepSel = options.removeStepSel || "button.remove";
var selectStepSel = options.selectStepSel = options.selectStepSel || "#selectStep";
function onLoad() {
importStepsFromUrlHash();
}
// look up needed steps from Url Hash:
function importStepsFromUrlHash() {
var hash = getUrlHashParameter("steps");
if (hash) {
_sequencer.importString(hash);
_sequencer.run({index:0});
}
setUrlHashParameter("steps", sequencer.toString());
}
function selectNewStepUi() {
var m = $(addStepSel + " select").val();
$(addStepSel + " .info").html(_sequencer.modulesInfo(m).description);
}
function removeStepUi() {
var index = $(removeStepSel).index(this) + 1;
sequencer.removeSteps(index).run({index : index-1});
// remove from URL hash too
setUrlHashParameter("steps", sequencer.toString());
}
function addStepUi() {
if ($(addStepSel + " select").val() == "none") return;
var newStepName = $(addStepSel + " select").val();
/*
* after adding the step we run the sequencer from defined step
* and since loadImage is not a part of the drawarray the step lies at current
* length - 2 of the drawarray
*/
_sequencer
.addSteps(newStepName, options)
.run({index: _sequencer.images.image1.steps.length - 2});
// add to URL hash too
setUrlHashParameter("steps", _sequencer.toString());
}
return {
onLoad: onLoad,
importStepsFromUrlHash: importStepsFromUrlHash,
selectNewStepUi: selectNewStepUi,
removeStepUi: removeStepUi,
addStepUi: addStepUi
}
}

View File

@@ -0,0 +1,185 @@
// Set the UI in sequencer. This Will generate HTML based on
// Image Sequencer events :
// onSetup : Called every time a step is added
// onDraw : Called every time a step starts draw
// onComplete : Called every time a step finishes drawing
// onRemove : Called everytime a step is removed
// The variable 'step' stores useful data like input and
// output values, step information.
// See documetation for more details.
function DefaultHtmlStepUi(_sequencer, options) {
options = options || {};
var stepsEl = options.stepsEl || document.querySelector("#steps");
var selectStepSel = options.selectStepSel = options.selectStepSel || "#selectStep";
function onSetup(step) {
if (step.options && step.options.description)
step.description = step.options.description;
step.ui =
'\
<div class="row step">\
<div class="col-md-4 details">\
<h3>' +
step.name +
"</h3>\
<p><i>" +
(step.description || "") +
'</i></p>\
</div>\
<div class="col-md-8">\
<div class="load" style="display:none;"><i class="fa fa-circle-o-notch fa-spin"></i></div>\
<a><img alt="" style="max-width=100%" class="img-thumbnail" /></a>\
</div>\
</div>\
';
var tools =
'<div class="tools btn-group">\
<button confirm="Are you sure?" class="remove btn btn btn-default">\
<i class="fa fa-trash"></i>\
</button>\
</div>';
var parser = new DOMParser();
step.ui = parser.parseFromString(step.ui, "text/html");
step.ui = step.ui.querySelector("div.row");
step.linkElement = step.ui.querySelector("a");
step.imgElement = step.ui.querySelector("a img");
if (_sequencer.modulesInfo().hasOwnProperty(step.name)) {
var inputs = _sequencer.modulesInfo(step.name).inputs;
var outputs = _sequencer.modulesInfo(step.name).outputs;
var merged = Object.assign(inputs, outputs); // combine outputs w inputs
for (var paramName in merged) {
var isInput = inputs.hasOwnProperty(paramName);
var html = "";
var inputDesc = isInput ? inputs[paramName] : {};
if (!isInput) {
html += '<span class="output"></span>';
} else if (inputDesc.type.toLowerCase() == "select") {
html += '<select class="form-control" name="' + paramName + '">';
for (var option in inputDesc.values) {
html += "<option>" + inputDesc.values[option] + "</option>";
}
html += "</select>";
} else {
html =
'<input class="form-control" type="' +
inputDesc.type +
'" name="' +
paramName +
'">';
}
var div = document.createElement("div");
div.className = "row";
div.setAttribute("name", paramName);
var description = inputs[paramName].desc || paramName;
div.innerHTML =
"<div class='det'>\
<label for='" +
paramName +
"'>" +
description +
"</label>\
" +
html +
"\
</div>";
step.ui.querySelector("div.details").appendChild(div);
}
$(step.ui.querySelector("div.details")).append(
"<p><button class='btn btn-default btn-save'>Save</button></p>"
);
function saveOptions() {
$(step.ui.querySelector("div.details"))
.find("input,select")
.each(function(i, input) {
step.options[$(input).attr("name")] = input.value;
});
_sequencer.run({index: _sequencer.images.image1.steps.length - 2});
// modify the url hash
setUrlHashParameter("steps", _sequencer.toString());
}
// on clicking Save in the details pane of the step
$(step.ui.querySelector("div.details .btn-save")).click(saveOptions);
}
if (step.name != "load-image")
step.ui
.querySelector("div.details")
.appendChild(
parser.parseFromString(tools, "text/html").querySelector("div")
);
stepsEl.appendChild(step.ui);
}
function onDraw(step) {
$(step.ui.querySelector(".load")).show();
$(step.ui.querySelector("img")).hide();
}
function onComplete(step) {
$(step.ui.querySelector(".load")).hide();
$(step.ui.querySelector("img")).show();
step.imgElement.src = step.output;
step.linkElement.href = step.output;
// TODO: use a generalized version of this
function fileExtension(output) {
return output.split("/")[1].split(";")[0];
}
step.linkElement.download = step.name + "." + fileExtension(step.output);
step.linkElement.target = "_blank";
// fill inputs with stored step options
if (_sequencer.modulesInfo().hasOwnProperty(step.name)) {
var inputs = _sequencer.modulesInfo(step.name).inputs;
var outputs = _sequencer.modulesInfo(step.name).outputs;
for (var i in inputs) {
if (
step.options[i] !== undefined &&
inputs[i].type.toLowerCase() === "input"
)
step.ui.querySelector('div[name="' + i + '"] input').value =
step.options[i];
if (
step.options[i] !== undefined &&
inputs[i].type.toLowerCase() === "select"
)
step.ui.querySelector('div[name="' + i + '"] select').value =
step.options[i];
}
for (var i in outputs) {
if (step[i] !== undefined)
step.ui.querySelector('div[name="' + i + '"] input').value =
step[i];
}
}
}
function onRemove(step) {
step.ui.remove();
}
function getPreview() {
return step.imgElement;
}
return {
getPreview: getPreview,
onSetup: onSetup,
onComplete: onComplete,
onRemove: onRemove,
onDraw: onDraw
}
}

42
examples/lib/urlHash.js Normal file
View File

@@ -0,0 +1,42 @@
function getUrlHashParameter(param) {
var params = getUrlHashParameters();
return params[param];
}
function getUrlHashParameters() {
var sPageURL = window.location.hash;
if (sPageURL) sPageURL = sPageURL.split('#')[1];
var pairs = sPageURL.split('&');
var object = {};
pairs.forEach(function(pair, i) {
pair = pair.split('=');
if (pair[0] != '') object[pair[0]] = pair[1];
});
return object;
}
// accepts an object like { paramName: value, paramName1: value }
// and transforms to: url.com#paramName=value&paramName1=value
function setUrlHashParameters(params) {
var keys = Object.keys(params);
var values = Object.values(params);
var pairs = [];
keys.forEach(function(key, i) {
if (key != '') pairs.push(keys[i] + '=' + values[i]);
});
var hash = pairs.join('&');
window.location.hash = hash;
}
function setUrlHashParameter(param, value) {
var params = getUrlHashParameters();
params[param] = value;
setUrlHashParameters(params);
}

BIN
icons/ic_144.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

BIN
icons/ic_192.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
icons/ic_96.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

173
index.js
View File

@@ -1,10 +1,11 @@
#!/usr/bin/env node
require('./src/ImageSequencer');
sequencer = ImageSequencer({ui: false});
require("./src/ImageSequencer");
sequencer = ImageSequencer({ ui: false });
var Spinner = require("ora");
var program = require('commander');
var readlineSync = require('readline-sync');
var program = require("commander");
var readlineSync = require("readline-sync");
function exit(message) {
console.error(message);
@@ -12,26 +13,28 @@ function exit(message) {
}
program
.version('0.1.0')
.option('-i, --image [PATH/URL]', 'Input image URL')
.option('-s, --step [step-name]', 'Name of the step to be added.')
.option('-o, --output [PATH]', 'Directory where output will be stored.')
.option('-op, --opions {object}', 'Options for the step')
.version("0.1.0")
.option("-i, --image [PATH/URL]", "Input image URL")
.option("-s, --step [step-name]", "Name of the step to be added.")
.option("-o, --output [PATH]", "Directory where output will be stored.")
.option("-b, --basic", "Basic mode outputs only final image")
.option("-c, --config [Object]", "Options for the step")
.parse(process.argv);
// Parse step into an array to allow for multiple steps.
if (!program.step) exit("No steps passed");
program.step = program.step.split(" ");
// User must input an image.
if(!program.image) exit("Can't read file.")
if (!program.image) exit("Can't read file.");
// User must input an image.
require('fs').access(program.image, function(err){
if(err) exit("Can't read file.")
require("fs").access(program.image, function(err) {
if (err) exit("Can't read file.");
});
// User must input a step. If steps exist, check that every step is a valid step.
if(!program.step || !validateSteps(program.step))
if (!program.step || !validateSteps(program.step))
exit("Please ensure all steps are valid.");
// If there's no user defined output directory, select a default directory.
@@ -39,65 +42,111 @@ program.output = program.output || "./output/";
// Set sequencer to log module outputs, if any.
sequencer.setUI({
onComplete: function(step) {
// Get information of outputs.
step.info = sequencer.modulesInfo(step.name);
for (var output in step.info.outputs) {
console.log("["+program.step+"]: "+output+" = "+step[output]);
console.log("[" + program.step + "]: " + output + " = " + step[output]);
}
}
});
// Finally, if everything is alright, load the image, add the steps and run the sequencer.
sequencer.loadImages(program.image,function(){
sequencer.loadImages(program.image, function() {
console.warn(
"\x1b[33m%s\x1b[0m",
"Please wait \n output directory generated will be empty until the execution is complete"
);
// Iterate through the steps and retrieve their inputs.
program.step.forEach(function(step){
var options = Object.assign({}, sequencer.modulesInfo(step).inputs);
//Generate the Output Directory
require("./src/CliUtils").makedir(program.output, () => {
console.log('Files will be exported to "' + program.output + '"');
// If inputs exists, print to console.
if (Object.keys(options).length) {
console.log("[" + step + "]: Inputs");
}
if (program.basic)
console.log("Basic mode is enabled, outputting only final image");
// If inputs exists, print them out with descriptions.
Object.keys(options).forEach(function(input) {
// The array below creates a variable number of spaces. This is done with (length + 1).
// Iterate through the steps and retrieve their inputs.
program.step.forEach(function(step) {
var options = Object.assign({}, sequencer.modulesInfo(step).inputs);
// If inputs exists, print to console.
if (Object.keys(options).length) {
console.log("[" + step + "]: Inputs");
}
// If inputs exists, print them out with descriptions.
Object.keys(options).forEach(function(input) {
// The array below creates a variable number of spaces. This is done with (length + 1).
// The extra 4 that makes it (length + 5) is to account for the []: characters
console.log(new Array(step.length + 5).join(' ') + input + ": " + options[input].desc);
console.log(
new Array(step.length + 5).join(" ") +
input +
": " +
options[input].desc
);
});
if (program.config) {
try {
program.config = JSON.parse(program.config);
console.log(`The parsed options object: `, program.config);
} catch (e) {
console.error(
"\x1b[31m%s\x1b[0m",
`Options(Config) is not a not valid JSON Fallback activate`
);
program.config = false;
console.log(e);
}
}
if (program.config && validateConfig(program.config, options)) {
console.log("Now using Options object");
Object.keys(options).forEach(function(input) {
options[input] = program.config[input];
});
} else {
// If inputs exist, iterate through them and prompt for values.
Object.keys(options).forEach(function(input) {
var value = readlineSync.question(
"[" +
step +
"]: Enter a value for " +
input +
" (" +
options[input].type +
", default: " +
options[input].default +
"): "
);
options[input] = value;
});
}
// Add the step and its inputs to the sequencer.
sequencer.addSteps(step, options);
});
// If inputs exist, iterate through them and prompt for values.
Object.keys(options).forEach(function(input) {
var value = readlineSync.question("[" + step + "]: Enter a value for " + input + " (" + options[input].type + ", default: " + options[input].default + "): ");
options[input] = value;
var spinnerObj = { succeed: () => {}, stop: () => {} };
if (!process.env.TEST)
spinnerObj = Spinner("Your Image is being processed..").start();
// Run the sequencer.
sequencer.run({progressObj: spinnerObj}, function() {
// Export all images or final image as binary files.
sequencer.exportBin(program.output, program.basic);
//check if spinner was not overriden stop it
if (!spinnerObj.overrideFlag) {
spinnerObj.succeed();
console.log(`\nDone!!`);
}
});
// Add the step and its inputs to the sequencer.
sequencer.addSteps(step, options);
});
// Run the sequencer.
sequencer.run(function(){
// Export all images as binary files.
sequencer.exportBin(program.output);
console.log("Files will be exported to \""+program.output+"\"");
});
});
// Takes an array of steps and checks if they are valid steps for the sequencer.
function validateSteps(steps) {
// Assume all are valid in the beginning.
// Assume all are valid in the beginning.
var valid = true;
steps.forEach(function(step) {
// If any step in the array is not valid (not a property of modulesInfo), set valid to false.
@@ -108,4 +157,26 @@ function validateSteps(steps) {
// Return valid. (If all of the steps are valid properties, valid will have remained true).
return valid;
}
}
//Takes config and options object and checks if all the keys exist in config
function validateConfig(config_, options_) {
options_ = Object.keys(options_);
if (
(function() {
for (var input in options_) {
if (!config_[options_[input]]) {
console.error(
"\x1b[31m%s\x1b[0m",
`Options Object does not have the required details "${
options_[input]
}" not specified. Fallback case activated`
);
return false;
}
}
})() == false
)
return false;
else return true;
}

View File

@@ -1,10 +1,12 @@
{
"name": "image-sequencer",
"version": "0.1.0",
"version": "2.1.0",
"description": "A modular JavaScript image manipulation library modeled on a storyboard.",
"main": "src/ImageSequencer.js",
"scripts": {
"test": "tape test/*.js | tap-spec; browserify test/image-sequencer.js test/chain.js | tape-run --render=\"tap-spec\""
"debug": "TEST=true node ./index.js -i ./examples/images/monarch.png -s invert",
"test": "TEST=true tape test/**/*.js test/*.js | tap-spec; browserify test/modules/image-sequencer.js test/modules/chain.js test/modules/replace.js test/modules/import-export.js test/modules/run.js | tape-run --render=\"tap-spec\"",
"start": "grunt serve"
},
"repository": {
"type": "git",
@@ -25,10 +27,15 @@
"commander": "^2.11.0",
"data-uri-to-buffer": "^2.0.0",
"fisheyegl": "^0.1.2",
"get-pixels": "~3.3.0",
"font-awesome": "~4.5.0",
"get-pixels": "~3.3.0",
"imgareaselect": "git://github.com/jywarren/imgareaselect.git#v1.0.0-rc.2",
"jquery": "~2",
"jsqr": "^0.2.2",
"lodash": "^4.17.5",
"ndarray-gaussian-filter": "^1.0.0",
"ora": "^2.0.0",
"pace": "0.0.4",
"readline-sync": "^1.4.7",
"save-pixels": "~2.3.4",
"urify": "^2.1.0"
@@ -37,8 +44,10 @@
"browserify": "13.0.0",
"data-uri-to-buffer": "^2.0.0",
"grunt": "^0.4.5",
"grunt-browser-sync": "^2.2.0",
"grunt-browserify": "^5.0.0",
"grunt-contrib-concat": "^0.5.0",
"grunt-contrib-uglify-es": "^3.3.0",
"grunt-contrib-watch": "^0.6.1",
"image-filter-core": "~1.0.0",
"image-filter-threshold": "~1.0.0",
@@ -46,7 +55,8 @@
"matchdep": "^0.3.0",
"tap-spec": "^4.1.1",
"tape": ">=4.7.0",
"tape-run": "^3.0.0"
"tape-run": "^3.0.0",
"uglify-es": "^3.3.7"
},
"homepage": "https://github.com/publiclab/image-sequencer",
"bin": {

View File

@@ -1,29 +1,37 @@
function AddStep(ref, image, name, o) {
// add steps to the sequencer
// TODO: reduce redundancy with InsertStep; this should be a specific usage of InsertStep at the final position
function AddStep(_sequencer, image, name, o) {
function addStep(image, name, o_) {
var o = ref.copy(o_);
o.number = ref.options.sequencerCounter++; //Gives a Unique ID to each step
o.name = o_.name || name;
if (_sequencer.modules[name]) var moduleInfo = _sequencer.modules[name][1];
else console.log('Module ' + name + ' not found.');
var o = _sequencer.copy(o_);
o.number = _sequencer.options.sequencerCounter++; // gives a unique ID to each step
o.name = o_.name || name || moduleInfo.name;
o.description = o_.description || moduleInfo.description;
o.selector = o_.selector || 'ismod-' + name;
o.container = o_.container || ref.options.selector;
o.container = o_.container || _sequencer.options.selector;
o.image = image;
o.inBrowser = ref.options.inBrowser;
o.inBrowser = _sequencer.options.inBrowser;
o.step = {
name: o.name,
description: o.description,
ID: o.number,
imageName: o.image,
inBrowser: ref.options.inBrowser,
ui: ref.options.ui,
inBrowser: _sequencer.options.inBrowser,
ui: _sequencer.options.ui,
options: o
};
var UI = ref.events;
var module = ref.modules[name][0](o,UI);
ref.images[image].steps.push(module);
var UI = _sequencer.events;
var module = _sequencer.modules[name][0](o, UI);
_sequencer.images[image].steps.push(module);
return true;
}
addStep(image, name, o);
_sequencer.steps = _sequencer.images[image].steps;
}
module.exports = AddStep;

21
src/CliUtils.js Normal file
View File

@@ -0,0 +1,21 @@
const fs = require('fs')
/*
* This function checks if the directory exists, if not it creates one on the given path
* Callback is called with argument error if an error is encountered
*/
function makedir(path,callback){
fs.access(path,function(err){
if(err) fs.mkdir(path,function(err){
if(err) callback(err);
callback();
});
else callback()
});
};
module.exports = exports = {
makedir: makedir
}

26
src/ExportBin.js Normal file → Executable file
View File

@@ -2,7 +2,7 @@ var fs = require('fs');
var getDirectories = function(rootDir, cb) {
fs.readdir(rootDir, function(err, files) {
var dirs = [];
if(typeof(files)=="undefined") {
if(typeof(files)=="undefined" || files.length == 0) {
cb(dirs);
return [];
}
@@ -23,11 +23,11 @@ var getDirectories = function(rootDir, cb) {
});
}
module.exports = function ExportBin(dir = "./output/",ref) {
module.exports = function ExportBin(dir = "./output/",ref,basic) {
dir = (dir[dir.length-1]=="/") ? dir : dir + "/";
if(ref.options.inBrowser) return false;
fs.access(dir, function(err){
if(err) fs.mkdir(dir, function() {});
if(err) console.error(err)
});
getDirectories(dir,function(dirs){
var num = 1;
@@ -40,13 +40,19 @@ module.exports = function ExportBin(dir = "./output/",ref) {
var root = dir+'sequencer'+num+'/';
for(var image in ref.images) {
var steps = ref.images[image].steps;
for(var i in steps) {
var datauri = steps[i].output.src;
var ext = steps[i].output.format;
var buffer = require('data-uri-to-buffer')(datauri);
fs.writeFile(root+image+"_"+i+"."+ext,buffer,function(){
});
if(basic){
var datauri = steps.slice(-1)[0].output.src;
var ext = steps.slice(-1)[0].output.format;
var buffer = require('data-uri-to-buffer')(datauri);
fs.writeFile(root+image+"_"+(steps.length-1)+"."+ext,buffer,function(){});
}
else{
for(var i in steps) {
var datauri = steps[i].output.src;
var ext = steps[i].output.format;
var buffer = require('data-uri-to-buffer')(datauri);
fs.writeFile(root+image+"_"+i+"."+ext,buffer,function(){});
}
}
}
})

View File

@@ -1,11 +1,12 @@
if (typeof window !== 'undefined') {window.$ = window.jQuery = require('jquery'); isBrowser = true}
if (typeof window !== 'undefined') {isBrowser = true}
else {var isBrowser = false}
require('./util/getStep.js')
ImageSequencer = function ImageSequencer(options) {
var sequencer = (this.name == "ImageSequencer")?this:this.sequencer;
options = options || {};
options.inBrowser = options.inBrowser || isBrowser;
// if (options.inBrowser) options.ui = options.ui || require('./UserInterface');
options.sequencerCounter = 0;
function objTypeOf(object){
@@ -37,13 +38,13 @@ ImageSequencer = function ImageSequencer(options) {
}
var image,
steps = [],
modules = require('./Modules'),
formatInput = require('./FormatInput'),
images = {},
inputlog = [],
events = require('./UserInterface')(),
fs = require('fs');
steps = [],
modules = require('./Modules'),
formatInput = require('./FormatInput'),
images = {},
inputlog = [],
events = require('./ui/UserInterface')(),
fs = require('fs');
// if in browser, prompt for an image
// if (options.imageSelect || options.inBrowser) addStep('image-select');
@@ -59,8 +60,8 @@ ImageSequencer = function ImageSequencer(options) {
inputlog.push({method:"addSteps", json_q:copy(json_q)});
for (var i in json_q)
for (var j in json_q[i])
require("./AddStep")(this_,i,json_q[i][j].name,json_q[i][j].o);
for (var j in json_q[i])
require("./AddStep")(this_,i,json_q[i][j].name,json_q[i][j].o);
return this;
}
@@ -88,7 +89,7 @@ ImageSequencer = function ImageSequencer(options) {
indices = json_q[img].sort(function(a,b){return b-a});
run[img] = indices[indices.length-1];
for (var i in indices)
removeStep(img,indices[i]);
removeStep(img,indices[i]);
}
// this.run(run); // This is creating problems
return this;
@@ -107,26 +108,42 @@ ImageSequencer = function ImageSequencer(options) {
var details = json_q[img];
details = details.sort(function(a,b){return b.index-a.index});
for (var i in details)
require("./InsertStep")(this_,img,details[i].index,details[i].name,details[i].o);
require("./InsertStep")(this_,img,details[i].index,details[i].name,details[i].o);
run[img] = details[details.length-1].index;
}
// this.run(run); // This is Creating issues
return this;
}
function run(t_image,t_from) {
// Config is an object which contains the runtime configuration like progress bar
// information and index from which the sequencer should run
function run(config,t_image,t_from) {
let progressObj,index=0;
config = config || {mode: 'no-arg'};
if(config.index) index = config.index;
if(config.mode != 'test'){
if(config.mode != "no-arg" && typeof config != 'function'){
if(config.progressObj) progressObj = config.progressObj;
delete arguments['0'];
}
}
else{
arguments['0'] = config.mode;
}
var this_ = (this.name == "ImageSequencer")?this:this.sequencer;
var args = (this.name == "ImageSequencer")?[]:[this.images];
for (var arg in arguments) args.push(copy(arguments[arg]));
var callback = function() {};
for (var arg in args)
if(objTypeOf(args[arg]) == "Function")
callback = args.splice(arg,1)[0];
if(objTypeOf(args[arg]) == "Function")
callback = args.splice(arg,1)[0];
var json_q = formatInput.call(this_,args,"r");
require('./Run')(this_, json_q, callback);
require('./Run')(this_, json_q, callback,index,progressObj);
return true;
}
@@ -139,7 +156,6 @@ ImageSequencer = function ImageSequencer(options) {
inputlog.push({method:"loadImages", json_q:copy(json_q)});
var loadedimages = this.copy(json_q.loadedimages);
// require('./LoadImage')(this,i,json_q.images[i]);
var ret = {
name: "ImageSequencer Wrapper",
@@ -159,7 +175,7 @@ ImageSequencer = function ImageSequencer(options) {
return;
}
var img = loadedimages[i];
require('./LoadImage')(sequencer,img,json_q.images[img],function(){
require('./ui/LoadImage')(sequencer,img,json_q.images[img],function(){
load(++i);
});
}
@@ -169,28 +185,113 @@ ImageSequencer = function ImageSequencer(options) {
function replaceImage(selector,steps,options) {
options = options || {};
return require('./ReplaceImage')(this,selector,steps);
options.callback = options.callback || function() {};
return require('./ReplaceImage')(this,selector,steps,options);
}
function setUI(UI) {
this.events = require('./UserInterface')(UI);
this.events = require('./ui/UserInterface')(UI);
}
var exportBin = function(dir) {
return require('./ExportBin')(dir,this);
var exportBin = function(dir,basic) {
return require('./ExportBin')(dir,this,basic);
}
function modulesInfo(name) {
var modulesdata = {}
if(name == "load-image") return {};
if(arguments.length==0)
for (var modulename in modules) {
modulesdata[modulename] = modules[modulename][1];
}
for (var modulename in modules) {
modulesdata[modulename] = modules[modulename][1];
}
else modulesdata = modules[name][1];
return modulesdata;
}
// Strigifies the current sequence
function toString(step) {
if(step) {
return stepToString(step);
} else {
return copy(this.images.image1.steps).map(stepToString).slice(1).join(',');
}
}
// Stringifies one step of the sequence
function stepToString(step) {
let inputs = copy(modulesInfo(step.options.name).inputs);
inputs = inputs || {};
for(let input in inputs) {
inputs[input] = step.options[input] || inputs[input].default;
inputs[input] = encodeURIComponent(inputs[input]);
}
var configurations = Object.keys(inputs).map(key => key + ':' + inputs[key]).join('|');
return `${step.options.name}(${configurations})`;
}
// exports the current sequence as an array of JSON steps
function toJSON(str){
return this.stringToJSON(this.toString());
}
// Coverts stringified sequence into an array of JSON steps
function stringToJSON(str){
let steps = str.split(',');
return steps.map(stringToJSONstep);
}
// Converts one stringified step into JSON
function stringToJSONstep(str){
if(str.indexOf('(') === -1) { // if there are no settings specified
var moduleName = str.substr(0);
stepSettings = "";
} else {
var moduleName = str.substr(0, str.indexOf('('));
stepSettings = str.slice(str.indexOf('(') + 1, -1);
}
stepSettings = stepSettings.split('|').reduce(function formatSettings(accumulator, current, i){
var settingName = current.substr(0, current.indexOf(':')),
settingValue = current.substr(current.indexOf(':') + 1);
settingValue = settingValue.replace(/^\(/, '').replace(/\)$/, ''); // strip () at start/end
settingValue = decodeURIComponent(settingValue);
current = [
settingName,
settingValue
];
if (!!settingName) accumulator[settingName] = settingValue;
return accumulator;
}, {});
return {
name : moduleName,
options: stepSettings
}
}
// imports a string into the sequencer steps
function importString(str){
let sequencer = this;
if(this.name != "ImageSequencer")
sequencer = this.sequencer;
var stepsFromString = stringToJSON(str);
stepsFromString.forEach(function eachStep(stepObj) {
sequencer.addSteps(stepObj.name,stepObj.options);
});
}
// imports a array of JSON steps into the sequencer steps
function importJSON(obj){
let sequencer = this;
if(this.name != "ImageSequencer")
sequencer = this.sequencer;
obj.forEach(function eachStep(stepObj) {
sequencer.addSteps(stepObj.name,stepObj.options);
});
}
return {
//literals and objects
name: "ImageSequencer",
@@ -211,11 +312,20 @@ ImageSequencer = function ImageSequencer(options) {
setUI: setUI,
exportBin: exportBin,
modulesInfo: modulesInfo,
toString: toString,
stepToString: stepToString,
toJSON: toJSON,
stringToJSON: stringToJSON,
stringToJSONstep: stringToJSONstep,
importString: importString,
importJSON: importJSON,
//other functions
log: log,
objTypeOf: objTypeOf,
copy: copy
copy: copy,
setInputStep: require('./ui/SetInputStep')(sequencer)
}
}

View File

@@ -1,3 +1,6 @@
const getStepUtils = require('./util/getStep.js');
// insert one or more steps at a given index in the sequencer
function InsertStep(ref, image, index, name, o) {
function insertStep(image, index, name, o_) {
@@ -8,10 +11,12 @@ function InsertStep(ref, image, index, name, o) {
o.container = o_.container || ref.options.selector;
o.image = image;
if(index==-1) index = ref.images[image].steps.length;
if (index == -1) index = ref.images[image].steps.length;
o.step = {
name: o.name,
description: o.description,
url: o.url,
ID: o.number,
imageName: o.image,
inBrowser: ref.options.inBrowser,
@@ -19,8 +24,8 @@ function InsertStep(ref, image, index, name, o) {
options: o
};
var UI = ref.events;
var module = ref.modules[name][0](o,UI);
ref.images[image].steps.splice(index,0,module);
var module = ref.modules[name][0](o, UI);
ref.images[image].steps.splice(index, 0, module);
return true;
}

View File

@@ -1,26 +1,50 @@
/*
* Core modules and their info files
*/
* Core modules and their info files
*/
module.exports = {
'green-channel': [
require('./modules/GreenChannel/Module'),require('./modules/GreenChannel/info')
'channel': [
require('./modules/Channel/Module'), require('./modules/Channel/info')
],
'ndvi-red': [
require('./modules/NdviRed/Module'),require('./modules/NdviRed/info')
'brightness': [
require('./modules/Brightness/Module'), require('./modules/Brightness/info')
],
'edge-detect': [
require('./modules/EdgeDetect/Module'), require('./modules/EdgeDetect/info')
],
'ndvi': [
require('./modules/Ndvi/Module'), require('./modules/Ndvi/info')
],
'invert': [
require('./modules/Invert/Module'),require('./modules/Invert/info')
require('./modules/Invert/Module'), require('./modules/Invert/info')
],
'crop': [
require('./modules/Crop/Module'),require('./modules/Crop/info')
require('./modules/Crop/Module'), require('./modules/Crop/info')
],
'segmented-colormap': [
require('./modules/SegmentedColormap/Module'),require('./modules/SegmentedColormap/info')
'colormap': [
require('./modules/Colormap/Module'), require('./modules/Colormap/info')
],
'decode-qr': [
require('./modules/DecodeQr/Module'),require('./modules/DecodeQr/info')
require('./modules/DecodeQr/Module'), require('./modules/DecodeQr/info')
],
'fisheye-gl': [
require('./modules/FisheyeGl/Module'),require('./modules/FisheyeGl/info')
require('./modules/FisheyeGl/Module'), require('./modules/FisheyeGl/info')
],
'dynamic': [
require('./modules/Dynamic/Module'), require('./modules/Dynamic/info')
],
'blur': [
require('./modules/Blur/Module'), require('./modules/Blur/info')
],
'saturation': [
require('./modules/Saturation/Module'), require('./modules/Saturation/info')
],
'average': [
require('./modules/Average/Module'), require('./modules/Average/info')
],
'blend': [
require('./modules/Blend/Module'), require('./modules/Blend/info')
],
'import-image': [
require('./modules/ImportImage/Module'), require('./modules/ImportImage/info')
]
}

View File

@@ -1,21 +1,37 @@
// Uses a given image as input and replaces it with the output.
// Works only in the browser.
function ReplaceImage(ref,selector,steps,options) {
if(!ref.options.inBrowser) return false; // This isn't for Node.js
var tempSequencer = ImageSequencer({ui: false});
var this_ = ref;
var input = document.querySelectorAll(selector);
if (window.hasOwnProperty('$')) var input = $(selector);
else var input = document.querySelectorAll(selector);
var images = [];
for (var i = 0; i < input.length; i++)
for (var i = 0; i < input.length; i++) {
if (input[i] instanceof HTMLImageElement) images.push(input[i]);
for (var i in images) {
var the_image = images[i];
var url = images[i].src;
var ext = url.split('.').pop();
}
function replaceImage (img, steps) {
var url = img.src;
// refactor to filetypeFromUrl()
var ext = url.split('?')[0].split('.').pop();
var xmlHTTP = new XMLHttpRequest();
xmlHTTP.open('GET', url, true);
xmlHTTP.responseType = 'arraybuffer';
xmlHTTP.onload = function(e) {
var arr = new Uint8Array(this.response);
var raw = String.fromCharCode.apply(null,arr);
// in chunks to avoid "RangeError: Maximum call stack exceeded"
// https://github.com/publiclab/image-sequencer/issues/241
// https://stackoverflow.com/a/20048852/1116657
var raw = '';
var i,j,subArray,chunk = 5000;
for (i=0,j=arr.length; i<j; i+=chunk) {
subArray = arr.subarray(i,i+chunk);
raw += String.fromCharCode.apply(null, subArray);
}
var base64 = btoa(raw);
var dataURL="data:image/"+ext+";base64," + base64;
make(dataURL);
@@ -25,11 +41,19 @@ function ReplaceImage(ref,selector,steps,options) {
else make(url);
function make(url) {
this_.loadImage('default',url).addSteps('default',steps).run(function(out){
the_image.src = out;
tempSequencer.loadImage(url, function(){
this.addSteps(steps).run({stop:function(){}},function(out){
img.src = out;
});
});
}
}
for (var i = 0; i < images.length; i++) {
replaceImage(images[i],steps);
if (i == images.length-1)
options.callback();
}
}
module.exports = ReplaceImage;

View File

@@ -1,46 +1,79 @@
function Run(ref, json_q, callback) {
function drawStep(drawarray,pos) {
if(pos==drawarray.length) {
var image = drawarray[pos-1].image;
if(ref.objTypeOf(callback)=='Function'){
const getStepUtils = require('./util/getStep.js');
function Run(ref, json_q, callback,ind, progressObj) {
if (!progressObj) progressObj = { stop: function () { } };
function drawStep(drawarray, pos) {
if (pos == drawarray.length && drawarray[pos - 1] !== undefined) {
var image = drawarray[pos - 1].image;
if (ref.objTypeOf(callback) == "Function" && ref.images[image].steps.slice(-1)[0].output) {
var steps = ref.images[image].steps;
var out = steps[steps.length-1].output.src;
var out = steps[steps.length - 1].output.src;
callback(out);
return true;
}
}
var image = drawarray[pos].image;
var i = drawarray[pos].i;
var input = ref.images[image].steps[i-1].output;
ref.images[image].steps[i].draw(ref.copy(input),function(){
drawStep(drawarray,++pos);
});
// so we don't run on the loadImage module:
if (drawarray[pos] !== undefined) {
var image = drawarray[pos].image;
var i = drawarray[pos].i;
var input = ref.images[image].steps[i - 1].output;
ref.images[image].steps[i].getStep = function getStep(offset) {
if(i + offset >= ref.images[image].steps.length) return {options:{name:undefined}};
else return ref.images[image].steps.slice(i + offset)[0];
};
ref.images[image].steps[i].getIndex = function getIndex(){
return i;
}
for (var util in getStepUtils) {
if (getStepUtils.hasOwnProperty(util)) {
ref.images[image].steps[i][util] = getStepUtils[util];
}
}
ref.images[image].steps[i].draw(
ref.copy(input),
function onEachStep() {
drawStep(drawarray, ++pos);
},
progressObj
);
}
}
function drawSteps(json_q) {
var drawarray = [];
for (var image in json_q) {
var no_steps = ref.images[image].steps.length;
var init = json_q[image];
for(var i = 0; i < no_steps-init; i++) {
drawarray.push({image: image,i: init+i});
for (var i = 0; i < no_steps - init; i++) {
drawarray.push({ image: image, i: init + i });
}
}
drawStep(drawarray,0);
drawStep(drawarray, ind);
}
function filter(json_q){
function filter(json_q) {
for (var image in json_q) {
if (json_q[image]==0 && ref.images[image].steps.length==1)
delete json_q[image];
else if (json_q[image]==0) json_q[image]++;
if (json_q[image] == 0 && ref.images[image].steps.length == 1)
delete json_q[image];
else if (json_q[image] == 0) json_q[image]++;
}
for (var image in json_q) {
var prevstep = ref.images[image].steps[json_q[image]-1];
while (typeof(prevstep) == "undefined" || typeof(prevstep.output) == "undefined") {
prevstep = ref.images[image].steps[(--json_q[image]) - 1];
var prevstep = ref.images[image].steps[json_q[image] - 1];
while (
typeof prevstep == "undefined" ||
typeof prevstep.output == "undefined"
) {
prevstep = ref.images[image].steps[--json_q[image] - 1];
}
}
return json_q;
}
var json_q = filter(json_q);
return drawSteps(json_q);
}

88
src/modules/Average/Module.js Executable file
View File

@@ -0,0 +1,88 @@
/*
* Average all pixel colors
*/
module.exports = function Average(options, UI){
options = options || {};
options.blur = options.blur || 2
//Tell the UI that a step has been set up
UI.onSetup(options.step);
var output;
options.step.metadata = options.step.metadata || {};
function draw(input,callback,progressObj){
progressObj.stop(true);
progressObj.overrideFlag = true;
// Tell the UI that a step is being drawn
UI.onDraw(options.step);
var step = this;
function changePixel(r, g, b, a){
return [r,g,b,a]
}
// do the averaging
function extraManipulation(pixels){
var sum = [0,0,0,0];
for (var i = 0; i < pixels.data.length; i += 4) {
sum[0] += pixels.data[i + 0];
sum[1] += pixels.data[i + 1];
sum[2] += pixels.data[i + 2];
sum[3] += pixels.data[i + 3];
}
sum[0] = parseInt(sum[0] / (pixels.data.length / 4));
sum[1] = parseInt(sum[1] / (pixels.data.length / 4));
sum[2] = parseInt(sum[2] / (pixels.data.length / 4));
sum[3] = parseInt(sum[3] / (pixels.data.length / 4));
for (var i = 0; i < pixels.data.length; i += 4) {
pixels.data[i + 0] = sum[0];
pixels.data[i + 1] = sum[1];
pixels.data[i + 2] = sum[2];
pixels.data[i + 3] = sum[3];
}
// report back and store average in metadata:
options.step.metadata.averages = sum;
console.log("average: ", sum);
// TODO: refactor into a new "display()" method as per https://github.com/publiclab/image-sequencer/issues/242
if (options.step.inBrowser && options.step.ui) $(options.step.ui).find('.details').append("<p><b>Averages</b> (r, g, b, a): " + sum.join(', ') + "</p>");
return pixels;
}
function output(image, datauri, mimetype){
// This output is accessible by Image Sequencer
step.output = {
src: datauri,
format: mimetype
};
// This output is accessible by UI
options.step.output = datauri;
// Tell UI that step has been drawn.
UI.onComplete(options.step);
}
return require('../_nomodule/PixelManipulation.js')(input, {
output: output,
changePixel: changePixel,
extraManipulation: extraManipulation,
format: input.format,
image: options.image,
callback: callback
});
}
return {
options: options,
draw: draw,
output: output,
UI: UI
}
}

6
src/modules/Average/info.json Executable file
View File

@@ -0,0 +1,6 @@
{
"name": "Average",
"description": "Average all pixel color",
"inputs": {
}
}

View File

@@ -0,0 +1,74 @@
module.exports = function Dynamic(options, UI, util) {
options = options || {};
options.func = options.func || "function(r1, g1, b1, a1, r2, g2, b2, a2) { return [ r1, g2, b2, a2 ] }";
// Tell the UI that a step has been set up.
UI.onSetup(options.step);
var output;
// This function is called on every draw.
function draw(input, callback, progressObj) {
progressObj.stop(true);
progressObj.overrideFlag = true;
// Tell the UI that the step is being drawn
UI.onDraw(options.step);
var step = this;
// convert to runnable code:
if (typeof options.func === "string") eval('options.func = ' + options.func);
var getPixels = require('get-pixels');
// save first image's pixels
var priorStep = this.getStep(-2);
getPixels(priorStep.output.src, function (err, pixels) {
options.firstImagePixels = pixels;
function changePixel(r2, g2, b2, a2, x, y) {
// blend!
var p = options.firstImagePixels;
return options.func(
r2, g2, b2, a2,
p.get(x, y, 0),
p.get(x, y, 1),
p.get(x, y, 2),
p.get(x, y, 3)
)
}
function output(image, datauri, mimetype) {
// This output is accessible by Image Sequencer
step.output = { src: datauri, format: mimetype };
// This output is accessible by the UI
options.step.output = datauri;
// Tell the UI that the draw is complete
UI.onComplete(options.step);
}
// run PixelManipulatin on second image's pixels
return require('../_nomodule/PixelManipulation.js')(input, {
output: output,
changePixel: changePixel,
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback
});
});
}
return {
options: options,
draw: draw,
output: output,
UI: UI
}
}

11
src/modules/Blend/info.json Executable file
View File

@@ -0,0 +1,11 @@
{
"name": "Blend",
"description": "Blend the past two image steps with the given function. Defaults to using the red channel from image 1 and the green and blue and alpha channels of image 2. Easier to use interfaces coming soon!",
"inputs": {
"blend": {
"type": "input",
"desc": "Function to use to blend the two images.",
"default": "function(r1, g1, b1, a1, r2, g2, b2, a2) { return [ r1, g2, b2, a2 ] }"
}
}
}

85
src/modules/Blur/Blur.js Executable file
View File

@@ -0,0 +1,85 @@
module.exports = exports = function(pixels,blur){
let kernel = kernelGenerator(blur,1)
kernel = flipKernel(kernel)
var oldpix = pixels
for(let i=0;i<pixels.shape[0];i++){
for(let j=0;j<pixels.shape[1];j++){
let neighboutPos = getNeighbouringPixelPositions([i,j])
let acc = [0.0,0.0,0.0,0.0]
for(let a = 0; a < kernel.length; a++){
for(let b = 0; b < kernel.length; b++){
acc[0] += (oldpix.get(neighboutPos[a][b][0],neighboutPos[a][b][1],0) * kernel[a][b]);
acc[1] += (oldpix.get(neighboutPos[a][b][0],neighboutPos[a][b][1],1) * kernel[a][b]);
acc[2] += (oldpix.get(neighboutPos[a][b][0],neighboutPos[a][b][1],2) * kernel[a][b]);
acc[3] += (oldpix.get(neighboutPos[a][b][0],neighboutPos[a][b][1],3) * kernel[a][b]);
}
}
pixels.set(i,j,0,acc[0])
pixels.set(i,j,1,acc[1])
pixels.set(i,j,2,acc[2])
}
}
return pixels
//Generates a 3x3 Gaussian kernel
function kernelGenerator(sigma,size){
/*
Trying out a variable radius kernel not working as of now
*/
// const coeff = (1.0/(2.0*Math.PI*sigma*sigma))
// const expCoeff = -1 * (1.0/2.0 * sigma * sigma)
// let e = Math.E
// let result = []
// for(let i = -1 * size;i<=size;i++){
// let arr = []
// for(let j= -1 * size;j<=size;j++){
// arr.push(coeff * Math.pow(e,expCoeff * ((i * i) + (j*j))))
// }
// result.push(arr)
// }
// let sum = result.reduce((sum,val)=>{
// return val.reduce((sumInner,valInner)=>{
// return sumInner+valInner
// })
// })
// result = result.map(arr=>arr.map(val=>(val + 0.0)/(sum + 0.0)))
// return result
return [
[2.0/159.0,4.0/159.0,5.0/159.0,4.0/159.0,2.0/159.0],
[4.0/159.0,9.0/159.0,12.0/159.0,9.0/159.0,4.0/159.0],
[5.0/159.0,12.0/159.0,15.0/159.0,12.0/159.0,5.0/159.0],
[4.0/159.0,9.0/159.0,12.0/159.0,9.0/159.0,4.0/159.0],
[2.0/159.0,4.0/159.0,5.0/159.0,4.0/159.0,2.0/159.0]
]
}
function getNeighbouringPixelPositions(pixelPosition){
let x = pixelPosition[0],y=pixelPosition[1]
let result = []
for(let i=-2;i<=2;i++){
let arr = []
for(let j=-2;j<=2;j++){
arr.push([x + i,y + j])
}
result.push(arr)
}
return result
}
function flipKernel(kernel){
let result = []
for(let i =kernel.length-1;i>=0;i--){
let arr = []
for(let j = kernel[i].length-1;j>=0;j--){
arr.push(kernel[i][j])
}
result.push(arr)
}
return result
}
}

59
src/modules/Blur/Module.js Executable file
View File

@@ -0,0 +1,59 @@
/*
* Blur an Image
*/
module.exports = function Blur(options,UI){
options = options || {};
options.blur = options.blur || 2
//Tell the UI that a step has been set up
UI.onSetup(options.step);
var output;
function draw(input,callback,progressObj){
progressObj.stop(true);
progressObj.overrideFlag = true;
// Tell the UI that a step is being drawn
UI.onDraw(options.step);
var step = this;
function changePixel(r, g, b, a){
return [r,g,b,a]
}
function extraManipulation(pixels){
pixels = require('./Blur')(pixels,options.blur)
return pixels
}
function output(image,datauri,mimetype){
// This output is accessible by Image Sequencer
step.output = {src:datauri,format:mimetype};
// This output is accessible by UI
options.step.output = datauri;
// Tell UI that step has been drawn.
UI.onComplete(options.step);
}
return require('../_nomodule/PixelManipulation.js')(input, {
output: output,
changePixel: changePixel,
extraManipulation: extraManipulation,
format: input.format,
image: options.image,
callback: callback
});
}
return {
options: options,
draw: draw,
output: output,
UI: UI
}
}

11
src/modules/Blur/info.json Executable file
View File

@@ -0,0 +1,11 @@
{
"name": "Blur",
"description": "Gaussian blur an image by a given value, typically 0-5",
"inputs": {
"blur": {
"type": "integer",
"desc": "amount of gaussian blur(Less blur gives more detail, typically 0-5)",
"default": 2
}
}
}

View File

@@ -0,0 +1,65 @@
/*
* Changes the Image Brightness
*/
module.exports = function Brightness(options,UI){
options = options || {};
//Tell the UI that a step has been set up
UI.onSetup(options.step);
var output;
function draw(input,callback,progressObj){
progressObj.stop(true);
progressObj.overrideFlag = true;
/*
In this case progress is handled by changepixel internally otherwise progressObj
needs to be overriden and used
For eg. progressObj = new SomeProgressModule()
*/
// Tell the UI that a step is being drawn
UI.onDraw(options.step);
var step = this;
function changePixel(r, g, b, a){
var val = (options.brightness)/100.0
r = val*r<255?val*r:255
g = val*g<255?val*g:255
b = val*b<255?val*b:255
return [r , g, b, a]
}
function output(image,datauri,mimetype){
// This output is accessible by Image Sequencer
step.output = {src:datauri,format:mimetype};
// This output is accessible by UI
options.step.output = datauri;
// Tell UI that step has been drawn.
UI.onComplete(options.step);
}
return require('../_nomodule/PixelManipulation.js')(input, {
output: output,
changePixel: changePixel,
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback
});
}
return {
options: options,
draw: draw,
output: output,
UI: UI
}
}

View File

@@ -0,0 +1,11 @@
{
"name": "Brightness",
"description": "Change the brightness of the image by given percent value",
"inputs": {
"brightness": {
"type": "integer",
"desc": "% brightness for the new image",
"default": 0
}
}
}

View File

@@ -1,24 +1,28 @@
/*
* Display only the green channel
* Display only one color channel
*/
module.exports = function GreenChannel(options,UI) {
module.exports = function Channel(options,UI) {
options = options || {};
options.title = "Green channel only";
options.description = "Displays only the green channel of an image";
options.channel = options.channel || "green";
// Tell UI that a step has been set up
UI.onSetup(options.step);
var output;
function draw(input,callback) {
function draw(input,callback,progressObj) {
progressObj.stop(true);
progressObj.overrideFlag = true;
// Tell UI that a step is being drawn
UI.onDraw(options.step);
var step = this;
function changePixel(r, g, b, a) {
return [0, g, 0, a];
if (options.channel == "red") return [r, 0, 0, a];
if (options.channel == "green") return [0, g, 0, a];
if (options.channel == "blue") return [0, 0, b, a];
}
function output(image,datauri,mimetype){
@@ -38,6 +42,7 @@ module.exports = function GreenChannel(options,UI) {
changePixel: changePixel,
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback
});

View File

@@ -0,0 +1,12 @@
{
"name": "Channel",
"description": "Displays only one color channel of an image -- default is green",
"inputs": {
"channel": {
"type": "select",
"desc": "Color channel",
"default": "green",
"values": ["red", "green", "blue"]
}
}
}

View File

@@ -0,0 +1,87 @@
/*
* Accepts a value from 0-255 and returns the new color-mapped pixel
* from a lookup table, which can be specified as an array of [begin, end]
* gradients, where begin and end are represented as [r, g, b] colors. In
* combination, a lookup table which maps values from 0 - 255 smoothly from black to white looks like:
* [
* [0, [0, 0, 0], [255, 255, 255]],
* [1, [255, 255, 255], [255, 255, 255]]
* ]
*
* Adapted from bgamari's work in Infragram: https://github.com/p-v-o-s/infragram-js/commit/346c97576a07b71a55671d17e0153b7df74e803b
*/
module.exports = function Colormap(value, options) {
options.colormap = options.colormap || colormaps.default;
// if a lookup table is provided as an array:
if(typeof(options.colormap) == "object")
colormapFunction = colormap(options.colormap);
// if a stored colormap is named with a string like "fastie":
else if(colormaps.hasOwnProperty(options.colormap))
colormapFunction = colormaps[options.colormap];
else colormapFunction = colormaps.default;
return colormapFunction(value / 255.00);
}
function colormap(segments) {
return function(x) {
var i, result, x0, x1, xstart, y0, y1, _i, _j, _len, _ref, _ref1, _ref2, _ref3;
_ref = [0, 0], y0 = _ref[0], y1 = _ref[1];
_ref1 = [segments[0][0], 1], x0 = _ref1[0], x1 = _ref1[1];
if (x < x0) {
return y0;
}
for (i = _i = 0, _len = segments.length; _i < _len; i = ++_i) {
_ref2 = segments[i], xstart = _ref2[0], y0 = _ref2[1], y1 = _ref2[2];
x0 = xstart;
if (i === segments.length - 1) {
x1 = 1;
break;
}
x1 = segments[i + 1][0];
if ((xstart <= x && x < x1)) {
break;
}
}
result = [];
for (i = _j = 0, _ref3 = y0.length; 0 <= _ref3 ? _j < _ref3 : _j > _ref3; i = 0 <= _ref3 ? ++_j : --_j) {
result[i] = (x - x0) / (x1 - x0) * (y1[i] - y0[i]) + y0[i];
}
return result;
};
};
var colormaps = {
greyscale: colormap([
[0, [0, 0, 0], [255, 255, 255] ],
[1, [255, 255, 255], [255, 255, 255] ]
]),
default: colormap([
[0, [0, 0, 255], [0, 255, 0] ],
[0.25, [0, 255, 0], [255, 255, 0] ],
[0.50, [0, 255, 255], [255, 255, 0] ],
[0.75, [255, 255, 0], [255, 0, 0] ]
]),
ndvi: colormap([
[0, [0, 0, 255], [38, 195, 195] ],
[0.5, [0, 150, 0], [255, 255, 0] ],
[0.75, [255, 255, 0], [255, 50, 50] ]
]),
stretched: colormap([
[0, [0, 0, 255], [0, 0, 255] ],
[0.1, [0, 0, 255], [38, 195, 195] ],
[0.5, [0, 150, 0], [255, 255, 0] ],
[0.7, [255, 255, 0], [255, 50, 50] ],
[0.9, [255, 50, 50], [255, 50, 50] ]
]),
fastie: colormap([
[0, [255, 255, 255], [0, 0, 0] ],
[0.167, [0, 0, 0], [255, 255, 255] ],
[0.33, [255, 255, 255], [0, 0, 0] ],
[0.5, [0, 0, 0], [140, 140, 255] ],
[0.55, [140, 140, 255], [0, 255, 0] ],
[0.63, [0, 255, 0], [255, 255, 0] ],
[0.75, [255, 255, 0], [255, 0, 0] ],
[0.95, [255, 0, 0], [255, 0, 255] ]
])
}

View File

@@ -1,30 +1,31 @@
module.exports = function SegmentedColormap(options,UI) {
module.exports = function Colormap(options,UI) {
options = options || {};
options.title = "Segmented Colormap";
// Tell the UI that a step has been set up.
UI.onSetup(options.step);
var output;
// This function is called on every draw.
function draw(input,callback) {
function draw(input,callback,progressObj) {
progressObj.stop(true);
progressObj.overrideFlag = true;
// Tell the UI that the step is being drawn
UI.onDraw(options.step);
var step = this;
function changePixel(r, g, b, a) {
var ndvi = (b - r) / (r + b);
var normalized = (ndvi + 1) / 2;
var res = require('./SegmentedColormap')(normalized,options);
var combined = (r + g + b) / 3.000;
var res = require('./Colormap')(combined, options);
return [res[0], res[1], res[2], 255];
}
function output(image,datauri,mimetype){
// This output is accessible by Image Sequencer
step.output = {src:datauri,format:mimetype};
step.output = { src: datauri, format: mimetype };
// This output is accessible by the UI
options.step.output = datauri;
@@ -38,6 +39,7 @@ module.exports = function SegmentedColormap(options,UI) {
changePixel: changePixel,
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback
});

View File

@@ -0,0 +1,12 @@
{
"name": "Colormap",
"description": "Maps brightness values (average of red, green & blue) to a given color lookup table, made up of a set of one more color gradients.\n\nFor example, 'cooler' colors like blue could represent low values, while 'hot' colors like red could represent high values.",
"inputs": {
"colormap": {
"type": "select",
"desc": "Name of the Colormap",
"default": "default",
"values": ["default","greyscale","stretched","fastie"]
}
}
}

View File

@@ -13,47 +13,65 @@
* y = options.y
* y = options.y + options.h
*/
module.exports = function CropModule(options,UI) {
module.exports = function CropModule(options, UI) {
options = options || {};
options.title = "Crop Image";
// TODO: we could also set this to {} if nil in AddModule.js to avoid this line:
options = options || {};
// Tell the UI that a step has been added
UI.onSetup(options.step);
var output;
// Tell the UI that a step has been added
UI.onSetup(options.step); // we should get UI to return the image thumbnail so we can attach our own UI extensions
// This function is caled everytime the step has to be redrawn
function draw(input,callback) {
// add our custom in-module html ui:
if (options.step.inBrowser) var ui = require('./Ui.js')(options.step, UI);
var output,
setupComplete = false;
// Tell the UI that the step has been triggered
UI.onDraw(options.step);
var step = this;
// This function is caled everytime the step has to be redrawn
function draw(input,callback) {
require('./Crop')(input,options,function(out,format){
// Tell the UI that the step has been triggered
UI.onDraw(options.step);
var step = this;
// This output is accessible to Image Sequencer
step.output = {
src: out,
format: format
}
// save the input image;
// TODO: this should be moved to module API to persist the input image
options.step.input = input.src;
// This output is accessible to the UI
options.step.output = out;
require('./Crop')(input, options, function(out, format){
// Tell the UI that the step has been drawn
UI.onComplete(options.step);
// This output is accessible to Image Sequencer
step.output = {
src: out,
format: format
}
// Tell Image Sequencer that step has been drawn
callback();
// This output is accessible to the UI
options.step.output = out;
});
// Tell the UI that the step has been drawn
UI.onComplete(options.step);
}
// we should do this via event/listener:
if (ui && ui.hide) ui.hide();
return {
options: options,
draw: draw,
output: output,
UI: UI
}
}
// start custom UI setup (draggable UI)
// only once we have an input image
if (setupComplete === false && options.step.inBrowser) {
setupComplete = true;
ui.setup();
}
// Tell Image Sequencer that step has been drawn
callback();
});
}
return {
options: options,
draw: draw,
output: output,
UI: UI
}
}

97
src/modules/Crop/Ui.js Normal file
View File

@@ -0,0 +1,97 @@
// hide on save
module.exports = function CropModuleUi(step, ui) {
let inputWidth = 0,
inputHeight = 0;
// We don't have input image dimensions at the
// time of setting up the UI; that comes when draw() is triggered.
// So we trigger setup only on first run of draw()
// TODO: link this to an event rather than an explicit call in Module.js
function setup() {
let x = 0,
y = 0;
// display original uncropped input image on initial setup
showOriginal()
inputWidth = Math.floor(imgEl().naturalWidth);
inputHeight = Math.floor(imgEl().naturalHeight);
// display with 50%/50% default crop:
setOptions(x, y, inputWidth, inputHeight);
$(imgEl()).imgAreaSelect({
handles: true,
x1: x,
y1: y,
x2: x + inputWidth / 2,
y2: y + inputHeight / 2,
// when selection is complete
onSelectEnd: function onSelectEnd(img, selection) {
// assign crop values to module UI form inputs:
let converted = convertToNatural(
selection.x1,
selection.y1,
selection.width,
selection.height
);
setOptions(
converted[0],
converted[1],
converted[2],
converted[3]
);
}
});
}
function convertToNatural(_x, _y, _width, _height) {
let displayWidth = $(imgEl()).width(),
displayHeight = $(imgEl()).height();
// return in same order [ x, y, width, height ]:
return [
Math.floor(( _x / displayWidth ) * inputWidth),
Math.floor(( _y / displayHeight ) * inputHeight),
Math.floor(( _width / displayWidth ) * inputWidth),
Math.floor(( _height / displayHeight ) * inputHeight)
]
}
function remove() {
$(imgEl()).imgAreaSelect({
remove: true
});
}
function hide() {
// then hide the draggable UI
$(imgEl()).imgAreaSelect({
hide: true
});
}
// step.imgSelector is not defined, imgElement is:
function imgEl() {
return step.imgElement;
}
function setOptions(x1, y1, width, height) {
let options = $($(imgEl()).parents()[2]).find("input");
options[0].value = x1;
options[1].value = y1;
options[2].value = width;
options[3].value = height;
}
// replaces currently displayed output thumbnail with the input image, for ui dragging purposes
function showOriginal() {
step.imgElement.src = step.input;
}
return {
setup: setup,
remove: remove,
hide: hide
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "Crop",
"description": "Crop image to given x, y, w, h",
"description": "Crop image to given x, y, w, h in pixels, measured from top left",
"url": "https://github.com/publiclab/image-sequencer/tree/master/MODULES.md",
"inputs": {
"x": {

View File

@@ -4,7 +4,6 @@
module.exports = function DoNothing(options,UI) {
options = options || {};
options.title = "Decode QR Code";
// Tell the UI that a step has been added
UI.onSetup(options.step);

View File

@@ -1,5 +1,6 @@
{
"name": "Decode QR",
"description": "Search for and decode a QR code in the image",
"inputs": {
},
"outputs": {

View File

@@ -0,0 +1,95 @@
module.exports = function Dynamic(options,UI) {
options = options || {};
// Tell the UI that a step has been set up.
UI.onSetup(options.step);
var output;
// This function is called on every draw.
function draw(input,callback,progressObj) {
progressObj.stop(true);
progressObj.overrideFlag = true;
// Tell the UI that the step is being drawn
UI.onDraw(options.step);
var step = this;
// start with monochrome, but if options.red, options.green, and options.blue are set, accept them too
options.monochrome = options.monochrome || "(R+G+B)/3";
function generator(expression) {
var func = 'f = function (r, g, b, a) { var R = r, G = g, B = b, A = a;'
func = func + 'return ';
func = func + expression + '}';
var f;
eval(func);
return f;
}
var channels = ['red', 'green', 'blue', 'alpha'];
channels.forEach(function(channel) {
if (options.hasOwnProperty(channel)) options[channel + '_function'] = generator(options[channel]);
else if (channel === 'alpha') options['alpha_function'] = function() { return 255; }
else options[channel + '_function'] = generator(options.monochrome);
});
function changePixel(r, g, b, a) {
/* neighbourpixels can be calculated by
this.getNeighbourPixel.fun(x,y) or this.getNeighborPixel.fun(x,y)
*/
var combined = (r + g + b) / 3.000;
return [
options.red_function(r, g, b, a),
options.green_function(r, g, b, a),
options.blue_function(r, g, b, a),
options.alpha_function(r, g, b, a),
];
}
/* Functions to get the neighbouring pixel by position (x,y) */
function getNeighbourPixel(pixels,curX,curY,distX,distY){
return [
pixels.get(curX+distX,curY+distY,0)
,pixels.get(curX+distX,curY+distY,1)
,pixels.get(curX+distX,curY+distY,2)
,pixels.get(curX+distX,curY+distY,3)
]
}
function output(image,datauri,mimetype){
// This output is accessible by Image Sequencer
step.output = { src: datauri, format: mimetype };
// This output is accessible by the UI
options.step.output = datauri;
// Tell the UI that the draw is complete
UI.onComplete(options.step);
}
return require('../_nomodule/PixelManipulation.js')(input, {
output: output,
changePixel: changePixel,
getNeighbourPixel: getNeighbourPixel,
getNeighborPixel: getNeighbourPixel,
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback
});
}
return {
options: options,
draw: draw,
output: output,
UI: UI
}
}

View File

@@ -0,0 +1,26 @@
{
"name": "Dynamic",
"description": "A module which accepts JavaScript math expressions to produce each color channel based on the original image's color. See <a href='https://publiclab.org/wiki/infragram-sandbox'>Infragrammar</a>.",
"inputs": {
"red": {
"type": "input",
"desc": "Expression to return for red channel with R, G, B, and A inputs",
"default": "r"
},
"green": {
"type": "input",
"desc": "Expression to return for green channel with R, G, B, and A inputs",
"default": "g"
},
"blue": {
"type": "input",
"desc": "Expression to return for blue channel with R, G, B, and A inputs",
"default": "b"
},
"monochrome (fallback)": {
"type": "input",
"desc": "Expression to return with R, G, B, and A inputs; fallback for other channels if none provided",
"default": "r + g + b"
}
}
}

View File

@@ -0,0 +1,179 @@
const _ = require('lodash')
//define kernels for the sobel filter
const kernelx = [[-1,0,1],[-2,0,2],[-1,0,1]],
kernely = [[-1,-2,-1],[0,0,0],[1,2,1]]
let angles = []
let mags = []
let strongEdgePixels = []
let weakEdgePixels = []
let notInUI
module.exports = exports = function(pixels,highThresholdRatio,lowThresholdRatio,inBrowser){
notInUI = !inBrowser
for(var x = 0; x < pixels.shape[0]; x++) {
angles.push([])
mags.push([])
for(var y = 0; y < pixels.shape[1]; y++) {
var result = changePixel(
pixels,
pixels.get(x,y,0),
pixels.get(x, y, 3),
x,
y
)
let pixel = result.pixel
pixels.set(x, y, 0, pixel[0]);
pixels.set(x, y, 1, pixel[1]);
pixels.set(x, y, 2, pixel[2]);
pixels.set(x, y, 3, pixel[3]);
mags.slice(-1)[0].push(pixel[3])
angles.slice(-1)[0].push(result.angle)
}
}
return hysteresis(doubleThreshold(nonMaxSupress(pixels),highThresholdRatio,lowThresholdRatio))
}
//changepixel function that convolutes every pixel (sobel filter)
function changePixel(pixels,val,a,x,y){
let magX = 0.0
for(let a = 0; a < 3; a++){
for(let b = 0; b < 3; b++){
let xn = x + a - 1;
let yn = y + b - 1;
magX += pixels.get(xn,yn,0) * kernelx[a][b];
}
}
let magY = 0.0
for(let a = 0; a < 3; a++){
for(let b = 0; b < 3; b++){
let xn = x + a - 1;
let yn = y + b - 1;
magY += pixels.get(xn,yn,0) * kernely[a][b];
}
}
let mag = Math.sqrt(Math.pow(magX,2) + Math.pow(magY,2))
let angle = Math.atan2(magY,magX)
return {
pixel:
[val,val,val,mag],
angle: angle
}
}
//Non Maximum Supression without interpolation
function nonMaxSupress(pixels) {
angles = angles.map((arr)=>arr.map(convertToDegrees))
for(let i = 1;i<pixels.shape[0]-1;i++){
for(let j=1;j<pixels.shape[1]-1;j++){
let angle = angles[i][j]
let pixel = pixels.get(i,j)
if ((angle>=-22.5 && angle<=22.5) ||
(angle<-157.5 && angle>=-180))
if ((mags[i][j]>= mags[i][j+1]) &&
(mags[i][j] >= mags[i][j-1]))
pixels.set(i,j,3,mags[i][j])
else
pixels.set(i,j,3,0)
else if ((angle>=22.5 && angle<=67.5) ||
(angle<-112.5 && angle>=-157.5))
if ((mags[i][j] >= mags[i+1][j+1]) &&
(mags[i][j] >= mags[i-1][j-1]))
pixels.set(i,j,3,mags[i][j])
else
pixels.set(i,j,3,0)
else if ((angle>=67.5 && angle<=112.5) ||
(angle<-67.5 && angle>=-112.5))
if ((mags[i][i] >= mags[i+1][j]) &&
(mags[i][j] >= mags[i][j]))
pixels.set(i,j,3,mags[i][j])
else
pixels.set(i,j,3,0)
else if ((angle>=112.5 && angle<=157.5) ||
(angle<-22.5 && angle>=-67.5))
if ((mags[i][j] >= mags[i+1][j-1]) &&
(mags[i][j] >= mags[i-1][j+1]))
pixels.set(i,j,3,mags[i][j])
else
pixels.set(i,j,3,0)
}
}
return pixels
}
//Converts radians to degrees
var convertToDegrees = radians => (radians * 180)/Math.PI
//Finds the max value in a 2d array like mags
var findMaxInMatrix = arr => Math.max(...arr.map(el=>el.map(val=>!!val?val:0)).map(el=>Math.max(...el)))
//Applies the double threshold to the image
function doubleThreshold(pixels,highThresholdRatio,lowThresholdRatio){
const highThreshold = findMaxInMatrix(mags) * 0.2
const lowThreshold = highThreshold * lowThresholdRatio
for(let i =0;i<pixels.shape[0];i++){
for(let j=0;j<pixels.shape[1];j++){
let pixelPos = [i,j]
mags[i][j]>lowThreshold
?mags[i][j]>highThreshold
?strongEdgePixels.push(pixelPos)
:weakEdgePixels.push(pixelPos)
:pixels.set(i,j,3,0)
}
}
strongEdgePixels.forEach(pix=>pixels.set(pix[0],pix[1],3,255))
return pixels
}
// hysteresis edge tracking algorithm
function hysteresis(pixels){
function getNeighbouringPixelPositions(pixelPosition){
let x = pixelPosition[0],y=pixelPosition[1]
return [[x+1,y+1],
[x+1,y],
[x+1,y-1],
[x,y+1],
[x,y-1],
[x-1,y+1],
[x-1,y],
[x-1,y-1]]
}
//This can potentially be improved see https://en.wikipedia.org/wiki/Connected-component_labeling
for(weakPixel in weakEdgePixels){
let neighbourPixels = getNeighbouringPixelPositions(weakEdgePixels[weakPixel])
for(pixel in neighbourPixels){
if(strongEdgePixels.find(el=> _.isEqual(el,neighbourPixels[pixel]))) {
pixels.set(weakPixel[0],weakPixel[1],3,255)
weakEdgePixels.splice(weakPixel,weakPixel)
break
}
}
}
weakEdgePixels.forEach(pix=>pixels.set(pix[0],pix[1],3,0))
return pixels
}

View File

@@ -0,0 +1,67 @@
/*
* Detect Edges in an Image
*/
module.exports = function edgeDetect(options,UI) {
options = options || {};
options.blur = options.blur || 2
options.highThresholdRatio = options.highThresholdRatio||0.2
options.lowThresholdRatio = options.lowThresholdRatio||0.15
// Tell UI that a step has been set up.
UI.onSetup(options.step);
var output;
// The function which is called on every draw.
function draw(input,callback,progressObj) {
progressObj.stop(true);
progressObj.overrideFlag = true;
// Tell UI that a step is being drawn.
UI.onDraw(options.step);
var step = this;
// Extra Manipulation function used as an enveloper for applying gaussian blur and Convolution
function extraManipulation(pixels){
pixels = require('ndarray-gaussian-filter')(pixels,options.blur)
return require('./EdgeUtils')(pixels,options.highThresholdRatio,options.lowThresholdRatio,options.inBrowser)
}
function changePixel(r, g, b, a) {
return [(r+g+b)/3, (r+g+b)/3, (r+g+b)/3, a];
}
function output(image,datauri,mimetype){
// This output is accessible by Image Sequencer
step.output = {src:datauri,format:mimetype};
// This output is accessible by UI
options.step.output = datauri;
// Tell UI that step has been drawn.
UI.onComplete(options.step);
}
return require('../_nomodule/PixelManipulation.js')(input, {
output: output,
changePixel: changePixel,
extraManipulation: extraManipulation,
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback
});
}
return {
options: options,
draw: draw,
output: output,
UI: UI
}
}

View File

@@ -0,0 +1,21 @@
{
"name": "Detect Edges",
"description": "this module detects edges using the Canny method, which first Gaussian blurs the image to reduce noise (amount of blur configurable in settings as `options.blur`), then applies a number of steps to highlight edges, resulting in a greyscale image where the brighter the pixel, the stronger the detected edge. Read more at: https://en.wikipedia.org/wiki/Canny_edge_detector",
"inputs": {
"blur": {
"type": "integer",
"desc": "amount of gaussian blur(Less blur gives more detail, typically 0-5)",
"default": 2
},
"highThresholdRatio":{
"type": "float",
"desc": "The high threshold ratio for the image",
"default": 0.2
},
"lowThresholdRatio": {
"type": "float",
"desc": "The low threshold value for the image",
"default": 0.15
}
}
}

View File

@@ -4,8 +4,6 @@
module.exports = function DoNothing(options,UI) {
options = options || {};
options.title = "Fisheye GL";
var output;
// Tell the UI that a step has been set up.

View File

@@ -1,5 +1,7 @@
{
"name": "Fisheye GL",
"description": "Correct fisheye, or barrel distortion, in images (with WebGL -- adapted from fisheye-correction-webgl by @bluemir).",
"requires": [ "webgl" ],
"inputs": {
"a": {
"type": "float",

View File

@@ -1,5 +0,0 @@
{
"name": "Green Channel",
"inputs": {
}
}

View File

@@ -0,0 +1,68 @@
/*
* Import Image module; this fetches a given remote or local image via URL
* or data-url, and overwrites the current one. It saves the original as
* step.metadata.input for use in future modules such as blending.
* TODO: we could accept an operation for blending like "screen" or "overlay",
* or a function with blend(r1,g1,b1,a1,r2,g2,b2,a2), OR we could simply allow
* subsequent modules to do this blending and keep this one simple.
*/
module.exports = function ImportImageModule(options, UI) {
options = options || {};
options.imageUrl = options.url || "./images/monarch.png";
var output,
imgObj = new Image();
// Tell the UI that a step has been added
UI.onSetup(options.step); // we should get UI to return the image thumbnail so we can attach our own UI extensions
// add our custom in-module html ui:
if (options.step.inBrowser) {
var ui = require('./Ui.js')(options.step, UI);
ui.setup();
}
// This function is caled everytime the step has to be redrawn
function draw(input,callback) {
// Tell the UI that the step has been triggered
UI.onDraw(options.step);
var step = this;
step.metadata = step.metadata || {};
// TODO: develop a standard API method for saving each input state,
// for reference in future steps (for blending, for example)
step.metadata.input = input;
function onLoad() {
// This output is accessible to Image Sequencer
step.output = {
src: imgObj.src,
format: options.format
}
// This output is accessible to the UI
options.step.output = imgObj.src;
// Tell the UI that the step has been drawn
UI.onComplete(options.step);
// Tell Image Sequencer that step has been drawn
callback();
}
options.format = require('../../util/GetFormat')(options.imageUrl);
imgObj.onload = onLoad;
imgObj.src = options.imageUrl;
}
return {
options: options,
draw: draw,
output: output,
UI: UI
}
}

View File

@@ -0,0 +1,54 @@
// hide on save
module.exports = function ImportImageModuleUi(step, ui) {
function setup(setImage, onLoad) {
// generate a unique timestamp based id for the dropzone
var dropzoneId = 'dropzone-import-image-' + step.ID;
// add a file input listener
var dropZone ='\
<div style="padding: 30px;margin: 10px 20% 30px;border: 4px dashed #ccc;border-radius: 8px;text-align: center;color: #444;" id="' + dropzoneId + '">\
<p>\
<i>Select or drag in an image to overlay.</i>\
</p>\
<center>\
<input type="file" class="file-input" value="">\
</center>\
</div>';
// insert into .details area
// TODO: develop API-based consistent way to add UI elements
$(step.ui)
.find('.details')
.prepend(dropZone);
// setup file input listener
sequencer.setInputStep({
dropZoneSelector: "#" + dropzoneId,
fileInputSelector: "#" + dropzoneId + " .file-input",
onLoad: function onLoadFromInput(progress) {
var reader = progress.target;
step.options.imageUrl = reader.result;
step.options.url = reader.result;
sequencer.run();
setUrlHashParameter("steps", sequencer.toString());
}
});
$(step.ui)
.find('.btn-save').on('click', function onClickSave() {
var src = $(step.ui)
.find('.det input').val();
step.options.imageUrl = src;
sequencer.run();
});
}
return {
setup: setup
}
}

View File

@@ -0,0 +1,12 @@
{
"name": "Import Image",
"description": "Import a new image and replace the original with it. Future versions may enable a blend mode. Specify an image by URL or by file selector.",
"url": "https://github.com/publiclab/image-sequencer/tree/master/MODULES.md",
"inputs": {
"url": {
"type": "string",
"desc": "URL of image to import",
"default": "./images/monarch.png"
}
}
}

View File

@@ -1,32 +1,32 @@
/*
* Display only the green channel
* Invert the image
*/
module.exports = function GreenChannel(options,UI) {
module.exports = function Invert(options, UI) {
options = options || {};
options.title = "Invert Colors";
options.description = "Inverts the colors of the image";
// Tell UI that a step has been set up.
UI.onSetup(options.step);
var output;
// The function which is called on every draw.
function draw(input,callback) {
function draw(input, callback, progressObj) {
progressObj.stop(true);
progressObj.overrideFlag = true;
// Tell UI that a step is being drawn.
UI.onDraw(options.step);
var step = this;
function changePixel(r, g, b, a) {
return [255-r, 255-g, 255-b, a];
return [255 - r, 255 - g, 255 - b, a];
}
function output(image,datauri,mimetype){
function output(image, datauri, mimetype) {
// This output is accessible by Image Sequencer
step.output = {src:datauri,format:mimetype};
step.output = { src: datauri, format: mimetype };
// This output is accessible by UI
options.step.output = datauri;
@@ -40,6 +40,7 @@ module.exports = function GreenChannel(options,UI) {
changePixel: changePixel,
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback
});
@@ -47,7 +48,7 @@ module.exports = function GreenChannel(options,UI) {
return {
options: options,
draw: draw,
draw: draw,
output: output,
UI: UI
}

View File

@@ -1,5 +1,6 @@
{
"name": "Invert",
"description": "Inverts the image.",
"inputs": {
}
}

View File

@@ -1,24 +1,28 @@
/*
* NDVI with red filter (blue channel is infrared)
*/
module.exports = function NdviRed(options,UI) {
module.exports = function Ndvi(options,UI) {
options = options || {};
options.title = "NDVI for red-filtered cameras (blue is infrared)";
options.filter = options.filter || "red";
// Tell the UI that a step has been set up.
UI.onSetup(options.step);
var output;
// The function which is called on every draw.
function draw(input,callback) {
function draw(input,callback,progressObj) {
progressObj.stop(true);
progressObj.overrideFlag = true;
// Tell the UI that a step is being drawn.
UI.onDraw(options.step);
var step = this;
function changePixel(r, g, b, a) {
var ndvi = (b - r) / (1.00 * b + r);
if (options.filter == "red") var ndvi = (b - r) / (1.00 * b + r);
if (options.filter == "blue") var ndvi = (r - b) / (1.00 * b + r);
var x = 255 * (ndvi + 1) / 2;
return [x, x, x, a];
}
@@ -40,6 +44,7 @@ module.exports = function NdviRed(options,UI) {
changePixel: changePixel,
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback
});

View File

@@ -0,0 +1,12 @@
{
"name": "NDVI",
"description": "Normalized Difference Vegetation Index, or NDVI, is an image analysis technique used with aerial photography. It's a way to visualize the amounts of infrared and other wavelengths of light reflected from vegetation by comparing ratios of blue and red light absorbed versus green and IR light reflected. NDVI is used to evaluate the health of vegetation in satellite imagery, where it correlates with how much photosynthesis is happening. This is helpful in assessing vegetative health or stress. <a href='https://publiclab.org/ndvi'>Read more</a>.<br /><br/>This is designed for use with red-filtered single camera <a href='http://publiclab.org/infragram'>DIY Infragram cameras</a>; change to 'blue' for blue filters",
"inputs": {
"filter": {
"type": "select",
"desc": "Filter color",
"default": "red",
"values": ["red", "blue"]
}
}
}

View File

@@ -1,5 +0,0 @@
{
"name": "NDVI Red",
"inputs": {
}
}

View File

@@ -0,0 +1,67 @@
/*
* Saturate an image with a value from 0 to 1
*/
module.exports = function Saturation(options,UI) {
options = options || {};
// Tell UI that a step has been set up
UI.onSetup(options.step);
var output;
function draw(input,callback,progressObj) {
progressObj.stop(true);
progressObj.overrideFlag = true;
// Tell UI that a step is being drawn
UI.onDraw(options.step);
var step = this;
function changePixel(r, g, b, a) {
var cR = 0.299;
var cG = 0.587;
var cB = 0.114;
var p = Math.sqrt((cR * (r*r)) + (cG * (g*g)) + (cB * (g*g)));
r = p+(r-p)*(options.saturation);
g = p+(g-p)*(options.saturation);
b = p+(b-p)*(options.saturation);
return [Math.round(r), Math.round(g), Math.round(b), a];
}
function output(image,datauri,mimetype){
// This output is accesible by Image Sequencer
step.output = {src:datauri,format:mimetype};
// This output is accessible by UI
options.step.output = datauri;
// Tell UI that step ahs been drawn
UI.onComplete(options.step);
}
return require('../_nomodule/PixelManipulation.js')(input, {
output: output,
changePixel: changePixel,
format: input.format,
image: options.image,
inBrowser: options.inBrowser,
callback: callback
});
}
return {
options: options,
//setup: setup, // optional
draw: draw,
output: output,
UI: UI
}
}

View File

@@ -0,0 +1,11 @@
{
"name": "Saturation",
"description": "Change the saturation of the image by given value, from 0-1, with 1 being 100% saturated.",
"inputs": {
"saturation": {
"type": "integer",
"desc": "saturation for the new image between 0 and 2, 0 being black and white and 2 being highly saturated",
"default": 0
}
}
}

View File

@@ -1,57 +0,0 @@
/*
* Accepts a normalized ndvi and returns the new color-mapped pixel
*/
module.exports = function SegmentedColormap(normalized,options) {
options.colormap = options.colormap || "default";
if(typeof(options.colormap) == "object")
colormapFunction = segmented_colormap(options.colormap);
else if(colormaps.hasOwnProperty(options.colormap))
colormapFunction = colormaps[options.colormap];
else colormapFunction = colormaps.default;
return colormapFunction(normalized);
}
function segmented_colormap(segments) {
return function(x) {
var i, result, x0, x1, xstart, y0, y1, _i, _j, _len, _ref, _ref1, _ref2, _ref3;
_ref = [0, 0], y0 = _ref[0], y1 = _ref[1];
_ref1 = [segments[0][0], 1], x0 = _ref1[0], x1 = _ref1[1];
if (x < x0) {
return y0;
}
for (i = _i = 0, _len = segments.length; _i < _len; i = ++_i) {
_ref2 = segments[i], xstart = _ref2[0], y0 = _ref2[1], y1 = _ref2[2];
x0 = xstart;
if (i === segments.length - 1) {
x1 = 1;
break;
}
x1 = segments[i + 1][0];
if ((xstart <= x && x < x1)) {
break;
}
}
result = [];
for (i = _j = 0, _ref3 = y0.length; 0 <= _ref3 ? _j < _ref3 : _j > _ref3; i = 0 <= _ref3 ? ++_j : --_j) {
result[i] = (x - x0) / (x1 - x0) * (y1[i] - y0[i]) + y0[i];
}
return result;
};
};
var greyscale_colormap = segmented_colormap([[0, [0, 0, 0], [255, 255, 255]], [1, [255, 255, 255], [255, 255, 255]]]);
var default_colormap = segmented_colormap([[0, [0, 0, 255], [38, 195, 195]], [0.5, [0, 150, 0], [255, 255, 0]], [0.75, [255, 255, 0], [255, 50, 50]]]);
var stretched_colormap = segmented_colormap([[0, [0, 0, 255], [0, 0, 255]], [0.1, [0, 0, 255], [38, 195, 195]], [0.5, [0, 150, 0], [255, 255, 0]], [0.7, [255, 255, 0], [255, 50, 50]], [0.9, [255, 50, 50], [255, 50, 50]]]);
var fastie_colormap = segmented_colormap([[0, [255, 255, 255], [0, 0, 0]], [0.167, [0, 0, 0], [255, 255, 255]], [0.33, [255, 255, 255], [0, 0, 0]], [0.5, [0, 0, 0], [140, 140, 255]], [0.55, [140, 140, 255], [0, 255, 0]], [0.63, [0, 255, 0], [255, 255, 0]], [0.75, [255, 255, 0], [255, 0, 0]], [0.95, [255, 0, 0], [255, 0, 255]]]);
var colormaps = {
greyscale: greyscale_colormap,
default: default_colormap,
stretched: stretched_colormap,
fastie: fastie_colormap
}

View File

@@ -1,11 +0,0 @@
{
"name": "Segmented Colormap",
"inputs": {
"colormap": {
"type": "select",
"desc": "Name of the Colormap",
"default": "default",
"values": ["default","greyscale","stretched","fastie"]
}
}
}

View File

@@ -3,7 +3,6 @@
*/
module.exports = function ImageThreshold(options) {
options = options || {};
options.title = "Threshold image";
options.threshold = options.threshold || 30;
var image;

View File

@@ -0,0 +1,6 @@
{
"name": "Threshold image",
"description": "...",
"inputs": {
}
}

View File

@@ -1,34 +1,58 @@
/*
* General purpose per-pixel manipulation
* accepting a changePixel() method to remix a pixel's channels
*/
* General purpose per-pixel manipulation
* accepting a changePixel() method to remix a pixel's channels
*/
module.exports = function PixelManipulation(image, options) {
options = options || {};
options.changePixel = options.changePixel || function changePixel(r, g, b, a) {
return [r, g, b, a];
};
var getPixels = require('get-pixels'),
savePixels = require('save-pixels');
options.changePixel =
options.changePixel ||
function changePixel(r, g, b, a) {
return [r, g, b, a];
};
getPixels(image.src, function(err, pixels) {
//
options.extraManipulation =
options.extraManipulation ||
function extraManipulation(pixels) {
return pixels;
};
if(err) {
console.log('Bad image path');
var getPixels = require("get-pixels"),
savePixels = require("save-pixels");
getPixels(image.src, function (err, pixels) {
if (err) {
console.log("Bad image path", image);
return;
}
// iterate through pixels;
// this could possibly be more efficient; see
// https://github.com/p-v-o-s/infragram-js/blob/master/public/infragram.js#L173-L181
for(var x = 0; x < pixels.shape[0]; x++) {
for(var y = 0; y < pixels.shape[1]; y++) {
if (options.getNeighbourPixel) {
options.getNeighbourPixel.fun = function getNeighborPixel(distX, distY) {
return options.getNeighbourPixel(pixels, x, y, distX, distY);
};
}
// iterate through pixels;
// TODO: this could possibly be more efficient; see
// https://github.com/p-v-o-s/infragram-js/blob/master/public/infragram.js#L173-L181
if (!options.inBrowser && !process.env.TEST) {
try {
var pace = require("pace")(pixels.shape[0] * pixels.shape[1]);
} catch (e) {
options.inBrowser = true;
}
}
for (var x = 0; x < pixels.shape[0]; x++) {
for (var y = 0; y < pixels.shape[1]; y++) {
var pixel = options.changePixel(
pixels.get(x, y, 0),
pixels.get(x, y, 1),
pixels.get(x, y, 2),
pixels.get(x, y, 3)
pixels.get(x, y, 3),
x,
y
);
pixels.set(x, y, 0, pixel[0]);
@@ -36,24 +60,29 @@ module.exports = function PixelManipulation(image, options) {
pixels.set(x, y, 2, pixel[2]);
pixels.set(x, y, 3, pixel[3]);
if (!options.inBrowser && !process.env.TEST) pace.op();
}
}
// perform any extra operations on the entire array:
if (options.extraManipulation) pixels = options.extraManipulation(pixels);
// there may be a more efficient means to encode an image object,
// but node modules and their documentation are essentially arcane on this point
var chunks = [];
var totalLength = 0;
var r = savePixels(pixels, options.format, {quality: 100});
var r = savePixels(pixels, options.format, { quality: 100 });
r.on('data', function(chunk){
r.on("data", function (chunk) {
totalLength += chunk.length;
chunks.push(chunk);
});
r.on('end', function(){
var data = Buffer.concat(chunks, totalLength).toString('base64');
var datauri = 'data:image/' + options.format + ';base64,' + data;
if (options.output) options.output(options.image,datauri,options.format);
r.on("end", function () {
var data = Buffer.concat(chunks, totalLength).toString("base64");
var datauri = "data:image/" + options.format + ";base64," + data;
if (options.output)
options.output(options.image, datauri, options.format);
if (options.callback) options.callback();
});
});

View File

@@ -1,3 +1,4 @@
// special module to load an image into the start of the sequence; used in the HTML UI
function LoadImage(ref, name, src, main_callback) {
function makeImage(datauri) {
var image = {
@@ -46,6 +47,7 @@ function LoadImage(ref, name, src, main_callback) {
function loadImage(name, src) {
var step = {
name: "load-image",
description: "This initial step loads and displays the original image without any modifications.<br /><br />To work with a new or different image, drag one into the drop zone.",
ID: ref.options.sequencerCounter++,
imageName: name,
inBrowser: ref.options.inBrowser,
@@ -58,6 +60,7 @@ function LoadImage(ref, name, src, main_callback) {
options: {
id: step.ID,
name: "load-image",
description: "This initial step loads and displays the original image without any modifications.",
title: "Load Image",
step: step
},

50
src/ui/SetInputStep.js Normal file
View File

@@ -0,0 +1,50 @@
// TODO: potentially move this into ImportImage module
function setInputStepInit() {
return function setInputStep(options) {
var dropzone = $(options.dropZoneSelector);
var fileInput = $(options.fileInputSelector);
var onLoad = options.onLoad;
var reader = new FileReader();
function handleFile(e) {
e.preventDefault();
e.stopPropagation(); // stops the browser from redirecting.
if (e.target && e.target.files) var file = e.target.files[0];
else var file = e.dataTransfer.files[0];
if(!file) return;
var reader = new FileReader();
reader.onload = onLoad;
reader.readAsDataURL(file);
}
fileInput.on('change', handleFile);
dropzone[0].addEventListener('drop', handleFile, false);
dropzone.on('dragover', function onDragover(e) {
e.stopPropagation();
e.preventDefault();
e.dataTransfer.dropEffect = 'copy'; // Explicitly show this is a copy.
}, false);
dropzone.on('dragenter', function onDragEnter(e) {
dropzone.addClass('hover');
});
dropzone.on('dragleave', function onDragLeave(e) {
dropzone.removeClass('hover');
});
}
}
module.exports = setInputStepInit;

View File

@@ -5,14 +5,12 @@
module.exports = function UserInterface(events = {}) {
events.onSetup = events.onSetup || function(step) {
if(step.ui == false) {
if (step.ui == false) {
// No UI
}
else if(step.inBrowser) {
} else if(step.inBrowser) {
// Create and append an HTML Element
console.log("Added Step \""+step.name+"\" to \""+step.imageName+"\".");
}
else {
} else {
// Create a NodeJS Object
console.log('\x1b[36m%s\x1b[0m',"Added Step \""+step.name+"\" to \""+step.imageName+"\".");
}
@@ -21,12 +19,10 @@ module.exports = function UserInterface(events = {}) {
events.onDraw = events.onDraw || function(step) {
if (step.ui == false) {
// No UI
}
else if(step.inBrowser) {
} else if(step.inBrowser) {
// Overlay a loading spinner
console.log("Drawing Step \""+step.name+"\" on \""+step.imageName+"\".");
}
else {
} else {
// Don't do anything
console.log('\x1b[33m%s\x1b[0m',"Drawing Step \""+step.name+"\" on \""+step.imageName+"\".");
}
@@ -35,27 +31,23 @@ module.exports = function UserInterface(events = {}) {
events.onComplete = events.onComplete || function(step) {
if (step.ui == false) {
// No UI
}
else if(step.inBrowser) {
} else if(step.inBrowser) {
// Update the DIV Element
// Hide the laoding spinner
console.log("Drawn Step \""+step.name+"\" on \""+step.imageName+"\".");
}
else {
} else {
// Update the NodeJS Object
console.log('\x1b[32m%s\x1b[0m',"Drawn Step \""+step.name+"\" on \""+step.imageName+"\".");
}
}
events.onRemove = events.onRemove || function(step) {
if(step.ui == false){
if (step.ui == false){
// No UI
}
else if(step.inBrowser) {
} else if(step.inBrowser) {
// Remove the DIV Element
console.log("Removing Step \""+step.name+"\" of \""+step.imageName+"\".");
}
else {
} else {
// Delete the NodeJS Object
console.log('\x1b[31m%s\x1b[0m',"Removing Step \""+step.name+"\" of \""+step.imageName+"\".");
}

29
src/util/GetFormat.js Normal file
View File

@@ -0,0 +1,29 @@
/*
* Determine format from a URL or data-url, return "jpg" "png" "gif" etc
* TODO: write a test for this using the examples
*/
module.exports = function GetFormat(src) {
var format = undefined; // haha default
// EXAMPLE: "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAQABADASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAf/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAABgj/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABykX//Z";
// EXAMPLE: "http://example.com/example.png"
// EXAMPLE: "/example.png"
if (isDataUrl(src)) {
format = src.split(';')[0].split('/').pop();
} else {
format = src.split('.').pop();
}
function isDataUrl(src) {
return src.substr(0, 10) === "data:image"
}
format = format.toLowerCase();
if (format === "jpeg") format = "jpg";
return format;
}

18
src/util/getStep.js Normal file
View File

@@ -0,0 +1,18 @@
module.exports = {
getPreviousStep : function () {
return this.getStep(-1);
},
getNextStep : function() {
return this.getStep(1);
},
getInput : function(offset){
if(offset + this.getIndex() === 0) offset++;
return this.getStep(offset - 1).output;
},
getOuput : function(offset){
return this.getStep(offset).output;
}
}

13
test/cli.js Normal file
View File

@@ -0,0 +1,13 @@
'use strict';
const cliUtils = require('../src/CliUtils');
const test = require('tape');
test('Output directory is correctly generated',function(t){
cliUtils.makedir('./output/',function(){
require('fs').access('./output/.',function(err){
t.true(!err,"Access the created dir");
t.end();
});
});
});

View File

@@ -1,175 +0,0 @@
'use strict';
var fs = require('fs');
var test = require('tape');
// We should only test headless code here.
// http://stackoverflow.com/questions/21358015/error-jquery-requires-a-window-with-a-document#25622933
require('../src/ImageSequencer.js');
// This function is used to test whether or not any additional global variables are being created
function copy(g,a) {
var b = {};
var i = 0;
for (var v in a)
if(g) {
if(v != "sequencer") b[v] = a[v];
}
else {
if(v != "sequencer" && v!="global1" && v!="global2" && !global1.hasOwnProperty(v)) i++;
}
if(g) return b;
else return i;
}
var parent = (typeof(global)==="undefined")?window:global;
var global1 = copy(true,parent);
var sequencer = ImageSequencer({ ui: false });
var red = "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAQABADASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAf/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAABgj/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABykX//Z";
test('Image Sequencer has tests', function (t) {
t.equal(true, true, "Initial Test");
t.end();
});
test('loadImages loads a DataURL image and creates a step.', function (t){
sequencer.loadImages('test',red);
t.equal(sequencer.images.test.steps.length, 1, "Initial Step Created");
t.equal(typeof(sequencer.images.test.steps[0].output.src), "string", "Initial output exists");
t.end();
});
if(!sequencer.options.inBrowser)
test('loadImage loads an image from URL and creates a step. (NodeJS)', function (t){
require('dns').resolve('www.github.com', function(err) {
if (err) {
console.log("Test aborted due to no internet");
t.end();
}
else {
sequencer.loadImage('URL','https://ccpandhare.github.io/image-sequencer/examples/images/red.jpg', function(){
t.equal(sequencer.images.URL.steps.length, 1, "Initial Step Created");
t.equal(typeof(sequencer.images.URL.steps[0].output.src), "string", "Initial output exists");
t.end();
});
}
});
});
if(!sequencer.options.inBrowser)
test('loadImages loads an image from PATH and creates a step. (NodeJS)', function (t){
sequencer.loadImages('examples/images/red.jpg');
t.equal(sequencer.images.image1.steps.length, 1, "Initial Step Created");
t.equal(typeof(sequencer.images.image1.steps[0].output.src), "string", "Initial output exists");
t.end();
});
test('loadImage works too.', function (t){
sequencer.loadImage('test2',red);
t.equal(sequencer.images.test2.steps.length, 1, "Initial Step Created");
t.equal(typeof(sequencer.images.test2.steps[0].output.src), "string", "Initial output exists");
t.end();
});
test('addSteps("image","name") adds a step', function (t) {
sequencer.addSteps('test','green-channel');
t.equal(sequencer.images.test.steps.length, 2, "Length of steps increased")
t.equal(sequencer.images.test.steps[1].options.name, "green-channel", "Correct Step Added");
t.end();
});
test('addSteps("name") adds a step', function (t) {
sequencer.addSteps('green-channel');
t.equal(sequencer.images.test.steps.length, 3, "Length of steps increased");
t.equal(sequencer.images.test.steps[2].options.name, "green-channel", "Correct Step Added");
t.end();
});
test('addSteps(["name"]) adds a step', function (t) {
sequencer.addSteps(['green-channel','invert']);
t.equal(sequencer.images.test.steps.length, 5, "Length of steps increased by two")
t.equal(sequencer.images.test.steps[3].options.name, "green-channel", "Correct Step Added");
t.equal(sequencer.images.test.steps[4].options.name, "invert", "Correct Step Added");
t.end();
});
test('addSteps("name",o) adds a step', function (t) {
sequencer.addSteps('green-channel',{});
t.equal(sequencer.images.test.steps.length, 6, "Length of steps increased");
t.equal(sequencer.images.test.steps[5].options.name, "green-channel", "Correct Step Added");
t.end();
});
test('addSteps("image","name",o) adds a step', function (t) {
sequencer.addSteps('test','green-channel',{});
t.equal(sequencer.images.test.steps.length, 7, "Length of steps increased");
t.equal(sequencer.images.test.steps[6].options.name, "green-channel", "Correct Step Added");
t.end();
});
test('removeSteps("image",position) removes a step', function (t) {
sequencer.removeSteps('test',1);
t.equal(sequencer.images.test.steps.length, 6, "Length of steps reduced");
t.end();
});
test('removeSteps({image: [positions]}) removes steps', function (t) {
sequencer.removeSteps('test',[1,2]);
t.equal(sequencer.images.test.steps.length, 4, "Length of steps reduced");
t.end();
});
test('removeSteps(position) removes steps', function (t) {
sequencer.removeSteps([1,2]);
t.equal(sequencer.images.test.steps.length, 2, "Length of steps reduced");
t.end();
});
test('insertSteps("image",position,"module",options) inserts a step', function (t) {
sequencer.insertSteps('test',1,'green-channel',{});
t.equal(sequencer.images.test.steps.length, 3, "Length of Steps increased");
t.equal(sequencer.images.test.steps[1].options.name, "green-channel", "Correct Step Inserted");
t.end();
});
test('insertSteps("image",position,"module") inserts a step', function (t) {
sequencer.insertSteps('test',1,'green-channel');
t.equal(sequencer.images.test.steps.length, 4, "Length of Steps increased");
t.equal(sequencer.images.test.steps[1].options.name, "green-channel", "Correct Step Inserted");
t.end();
});
test('insertSteps(position,"module") inserts a step', function (t) {
sequencer.insertSteps(1,'green-channel');
t.equal(sequencer.images.test.steps.length, 5, "Length of Steps increased");
t.equal(sequencer.images.test.steps[1].options.name, "green-channel", "Correct Step Inserted");
t.end();
});
test('insertSteps({image: {index: index, name: "module", o: options} }) inserts a step', function (t) {
sequencer.insertSteps({test: {index:1, name:'green-channel', o:{} } });
t.equal(sequencer.images.test.steps.length, 6, "Length of Steps increased");
t.equal(sequencer.images.test.steps[1].options.name, "green-channel", "Correct Step Inserted");
t.end();
});
test('run() runs the sequencer and returns output to callback', function (t) {
sequencer.run('test',function(out){
t.equal(typeof(sequencer.images.test.steps[sequencer.images.test.steps.length - 1].output), "object", "Output is Generated");
t.equal(out,sequencer.images.test.steps[sequencer.images.test.steps.length - 1].output.src, "Output callback works");
});
t.end();
});
test('replaceImage returns false in NodeJS', function (t) {
var returnvalue = (sequencer.options.inBrowser)?false:sequencer.replaceImage("#selector","test");
t.equal(returnvalue,false,"It does.");
t.end();
});
var global2 = copy(false,parent);
test('No Global Variables Created', function (t) {
t.equal(0,global2,"None Created.");
t.end();
});

View File

@@ -6,7 +6,7 @@ var test = require('tape');
// We should only test headless code here.
// http://stackoverflow.com/questions/21358015/error-jquery-requires-a-window-with-a-document#25622933
require('../src/ImageSequencer.js');
require('../../src/ImageSequencer.js');
var sequencer = ImageSequencer({ ui: false });
var red = "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAQABADASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAf/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAABgj/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABykX//Z";
@@ -41,10 +41,10 @@ test('addSteps is two-way chainable.', function (t){
});
test('addSteps is two-way chainable without loadImages.', function (t){
var returnval = sequencer.addSteps("image3","ndvi-red");
var returnval = sequencer.addSteps("image3","ndvi");
t.equal(returnval.name,"ImageSequencer","Sequencer is returned");
t.equal(sequencer.images.image3.steps.length,3,"Step length increased");
t.equal(sequencer.images.image3.steps[2].options.name,"ndvi-red","Correct Step Added");
t.equal(sequencer.images.image3.steps[2].options.name,"ndvi","Correct Step Added");
t.end();
});
@@ -79,9 +79,9 @@ test('insertSteps is two-way chainable.', function (t){
});
test('insertSteps is two-way chainable without loadImages.', function (t){
var returnval = sequencer.insertSteps("image5",1,"ndvi-red");
var returnval = sequencer.insertSteps("image5",1,"ndvi");
t.equal(returnval.name,"ImageSequencer","Sequencer is returned");
t.equal(sequencer.images.image5.steps.length,3);
t.equal(sequencer.images.image5.steps[1].options.name,"ndvi-red","Correct Step Inserrted");
t.equal(sequencer.images.image5.steps[1].options.name,"ndvi","Correct Step Inserrted");
t.end();
});

View File

@@ -7,7 +7,7 @@ var DataURItoBuffer = require('data-uri-to-buffer');
// We should only test headless code here.
// http://stackoverflow.com/questions/21358015/error-jquery-requires-a-window-with-a-document#25622933
require('../src/ImageSequencer.js');
require('../../src/ImageSequencer.js');
//require image files as DataURLs so they can be tested alike on browser and Node.
var sequencer = ImageSequencer({ ui: false });
@@ -20,7 +20,7 @@ sequencer.loadImages(test_png);
sequencer.addSteps(['invert','invert']);
test("Preload", function(t) {
sequencer.run(function(){
sequencer.run({mode:'test'},function(){
t.end();
});
});
@@ -51,7 +51,7 @@ test("Twice inverted image is identical to original image", function (t) {
test("Decode QR module works properly :: setup", function (t) {
sequencer.loadImage(qr,function(){
this.addSteps('decode-qr').run(function(){
this.addSteps('decode-qr').run({mode:'test'},function(){
t.end();
});
})
@@ -64,7 +64,7 @@ test("Decode QR module works properly :: teardown", function (t) {
test("PixelManipulation works for PNG images", function (t) {
sequencer.loadImages(test_png,function(){
this.addSteps('invert').run(function(out){
this.addSteps('invert').run({mode:'test'},function(out){
t.equal(1,1)
t.end();
});
@@ -73,9 +73,9 @@ test("PixelManipulation works for PNG images", function (t) {
test("PixelManipulation works for GIF images", function (t) {
sequencer.loadImages(test_gif,function(){
this.addSteps('invert').run(function(out){
this.addSteps('invert').run({mode:'test'},function(out){
t.equal(1,1)
t.end();
});
});
});
});

View File

@@ -0,0 +1,196 @@
'use strict';
var fs = require('fs');
var test = require('tape');
// We should only test headless code here.
// http://stackoverflow.com/questions/21358015/error-jquery-requires-a-window-with-a-document#25622933
require('../../src/ImageSequencer.js');
// This function is used to test whether or not any additional global variables are being created
function copy(g, a) {
var b = {};
var i = 0;
for (var v in a)
if (g) {
if (v != "sequencer") b[v] = a[v];
}
else {
if (v != "sequencer" && v != "global1" && v != "global2" && !global1.hasOwnProperty(v)) i++;
}
if (g) return b;
else return i;
}
var parent = (typeof (global) === "undefined") ? window : global;
var global1 = copy(true, parent);
var sequencer = ImageSequencer({ ui: false });
var red = "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAQABADASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAf/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAABgj/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABykX//Z";
test('Image Sequencer has tests', function (t) {
t.equal(true, true, "Initial Test");
t.end();
});
test('loadImages loads a DataURL image and creates a step.', function (t) {
sequencer.loadImages('test', red);
t.equal(sequencer.images.test.steps.length, 1, "Initial Step Created");
t.equal(typeof (sequencer.images.test.steps[0].output.src), "string", "Initial output exists");
t.end();
});
test('modulesInfo() returns info for each module', function (t) {
var info = sequencer.modulesInfo();
t.equal(Object.keys(info).length, Object.keys(sequencer.modules).length);
t.equal(info.hasOwnProperty(Object.keys(sequencer.modules)[0]), true);
t.equal(info[Object.keys(sequencer.modules)[0]].hasOwnProperty('name'), true);
t.equal(info[Object.keys(sequencer.modules)[0]].hasOwnProperty('inputs'), true);
t.end();
});
if (!sequencer.options.inBrowser)
test('loadImage loads an image from URL and creates a step. (NodeJS)', function (t) {
require('dns').resolve('www.github.com', function (err) {
if (err) {
console.log("Test aborted due to no internet");
t.end();
}
else {
sequencer.loadImage('URL', 'https://ccpandhare.github.io/image-sequencer/examples/images/red.jpg', function () {
t.equal(sequencer.images.URL.steps.length, 1, "Initial Step Created");
t.equal(typeof (sequencer.images.URL.steps[0].output.src), "string", "Initial output exists");
t.end();
});
}
});
});
if (!sequencer.options.inBrowser)
test('loadImages loads an image from PATH and creates a step. (NodeJS)', function (t) {
sequencer.loadImages('examples/images/red.jpg');
t.equal(sequencer.images.image1.steps.length, 1, "Initial Step Created");
t.equal(typeof (sequencer.images.image1.steps[0].output.src), "string", "Initial output exists");
t.end();
});
test('loadImage works too.', function (t) {
sequencer.loadImage('test2', red);
t.equal(sequencer.images.test2.steps.length, 1, "Initial Step Created");
t.equal(typeof (sequencer.images.test2.steps[0].output.src), "string", "Initial output exists");
t.end();
});
test('addSteps("image","name") adds a step', function (t) {
sequencer.addSteps('test', 'channel');
t.equal(sequencer.images.test.steps.length, 2, "Length of steps increased")
t.equal(sequencer.images.test.steps[1].options.name, "channel", "Correct Step Added");
t.equal(sequencer.images.test.steps[1].options.description, "Displays only one color channel of an image -- default is green", "Step description shown");
t.end();
});
test('addSteps("name") adds a step', function (t) {
sequencer.addSteps('channel');
t.equal(sequencer.images.test.steps.length, 3, "Length of steps increased");
t.equal(sequencer.images.test.steps[2].options.name, "channel", "Correct Step Added");
t.end();
});
test('addSteps(["name"]) adds a step', function (t) {
sequencer.addSteps(['channel', 'invert']);
t.equal(sequencer.images.test.steps.length, 5, "Length of steps increased by two")
t.equal(sequencer.images.test.steps[3].options.name, "channel", "Correct Step Added");
t.equal(sequencer.images.test.steps[4].options.name, "invert", "Correct Step Added");
t.end();
});
test('addSteps("name",o) adds a step', function (t) {
sequencer.addSteps('channel', {});
t.equal(sequencer.images.test.steps.length, 6, "Length of steps increased");
t.equal(sequencer.images.test.steps[5].options.name, "channel", "Correct Step Added");
t.end();
});
test('addSteps("image","name",o) adds a step', function (t) {
sequencer.addSteps('test', 'channel', {});
t.equal(sequencer.images.test.steps.length, 7, "Length of steps increased");
t.equal(sequencer.images.test.steps[6].options.name, "channel", "Correct Step Added");
t.end();
});
test('removeSteps("image",position) removes a step', function (t) {
sequencer.removeSteps('test', 1);
t.equal(sequencer.images.test.steps.length, 6, "Length of steps reduced");
t.end();
});
test('removeSteps({image: [positions]}) removes steps', function (t) {
sequencer.removeSteps('test', [1, 2]);
t.equal(sequencer.images.test.steps.length, 4, "Length of steps reduced");
t.end();
});
test('removeSteps(position) removes steps', function (t) {
sequencer.removeSteps([1, 2]);
t.equal(sequencer.images.test.steps.length, 2, "Length of steps reduced");
t.end();
});
test('insertSteps("image",position,"module",options) inserts a step', function (t) {
sequencer.insertSteps('test', 1, 'channel', {});
t.equal(sequencer.images.test.steps.length, 3, "Length of Steps increased");
t.equal(sequencer.images.test.steps[1].options.name, "channel", "Correct Step Inserted");
t.end();
});
test('insertSteps("image",position,"module") inserts a step', function (t) {
sequencer.insertSteps('test', 1, 'channel');
t.equal(sequencer.images.test.steps.length, 4, "Length of Steps increased");
t.equal(sequencer.images.test.steps[1].options.name, "channel", "Correct Step Inserted");
t.end();
});
test('insertSteps(position,"module") inserts a step', function (t) {
sequencer.insertSteps(1, 'channel');
t.equal(sequencer.images.test.steps.length, 5, "Length of Steps increased");
t.equal(sequencer.images.test.steps[1].options.name, "channel", "Correct Step Inserted");
t.end();
});
test('insertSteps({image: {index: index, name: "module", o: options} }) inserts a step', function (t) {
sequencer.insertSteps({ test: { index: 1, name: 'channel', o: {} } });
t.equal(sequencer.images.test.steps.length, 6, "Length of Steps increased");
t.equal(sequencer.images.test.steps[1].options.name, "channel", "Correct Step Inserted");
t.end();
});
test('run() runs the sequencer and returns output to callback', function (t) {
sequencer.run({mode:'test'}, function (out) {
t.equal(typeof (sequencer.images.test.steps[sequencer.images.test.steps.length - 1].output), "object", "Output is Generated");
t.equal(out, sequencer.images.test.steps[sequencer.images.test.steps.length - 1].output.src, "Output callback works");
t.end();
});
});
test('getStep(offset) returns the step at offset distance relative to current step',function(t){
sequencer.addSteps('test','invert',{});
sequencer.addSteps('test','blend',{});
sequencer.run({mode:'test'},function(out){
t.equal(!!out,true,"Blend generates output");
t.end();
});
});
test('replaceImage returns false in NodeJS', function (t) {
var returnvalue = (sequencer.options.inBrowser) ? false : sequencer.replaceImage("#selector", "test");
t.equal(returnvalue, false, "It does.");
t.end();
});
var global2 = copy(false, parent);
test('No Global Variables Created', function (t) {
t.equal(0, global2, "None Created.");
t.end();
});

View File

@@ -0,0 +1,58 @@
var test = require('tape');
require('../../src/ImageSequencer.js');
var sequencer = ImageSequencer({ ui: false });
var red = "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAQABADASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAf/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAABgj/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABykX//Z";
test('toString() and stepToString() return the step/steps in string format', function(t){
sequencer.loadImages('image1', red);
sequencer.addSteps('channel');
sequencer.addSteps('invert');
t.equal(sequencer.toString(),"channel(channel:green),invert()","toString works");
t.equal(sequencer.stepToString(sequencer.steps[1]),"channel(channel:green)","stepToString works");
t.end();
});
test('stringToJSON() and stringToJSONstep() return the step/steps from a string', function(t){
t.deepEqual(sequencer.stringToJSON('channel(channel:green),invert(),crop(x:0|y:0|w:50%25|h:50%25),blend(blend:function(r1%2C%20g1%2C%20b1%2C%20a1%2C%20r2%2C%20g2%2C%20b2%2C%20a2)%20%7B%20return%20%5B%20r1%2C%20g2%2C%20b2%2C%20a2%20%5D%20%7D)'),[
{ name: 'channel', options: { channel: 'green' } },
{ name: 'invert', options: {} },
{ name: 'crop', options: { x: '0', y: '0', w: '50%', h: '50%'} },
{ name: 'blend', options: { blend: 'function(r1, g1, b1, a1, r2, g2, b2, a2) { return [ r1, g2, b2, a2 ] }' } }
]);
t.deepEqual(sequencer.stringToJSONstep("channel(channel:green)"),{ name: 'channel', options: { channel: 'green' } });
t.end();
});
test('stringToJSON() and stringToJSONstep() return the step/steps from a string without configs, using defaults', function(t){
t.deepEqual(sequencer.stringToJSON('channel,blur,invert,blend'),[
{ name: 'channel', options: { } },
{ name: 'blur', options: { } },
{ name: 'invert', options: { } },
{ name: 'blend', options: { } }
]);
t.end();
});
test('toJSON() returns the right sequence of steps',function(t){
t.deepEqual(sequencer.toJSON(), [
{ name: 'channel', options: { channel: 'green' } },
{ name: 'invert', options: {} }
]);
t.end();
});
test('importString() imports a string of steps into the sequencer', function(t){
sequencer.importString('brightness(brightness:50),invert');
t.equal(sequencer.toString(),"channel(channel:green),invert(),brightness(brightness:50),invert()");
t.end();
});
test('importJSON() imports a JSON array of steps into the sequencer', function(t){
sequencer.importJSON([
{ name: 'blur',options: {}}
]);
t.equal(sequencer.toString(),"channel(channel:green),invert(),brightness(brightness:50),invert(),blur(blur:2)")
t.end();
});

25
test/modules/replace.js Normal file
View File

@@ -0,0 +1,25 @@
'use strict';
var fs = require('fs');
var test = require('tape');
require('../../src/ImageSequencer.js');
var sequencer = ImageSequencer({ ui: false });
var red = "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAQABADASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAf/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAABgj/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABykX//Z";
if(typeof(document) !== "undefined") {
var image = document.createElement("img");
image.src = red;
document.body.appendChild(image);
}
test('replaceImage works.', function (t){
if (typeof(document) === "undefined")
t.end();
sequencer.replaceImage("img","invert",{ callback: function(){
t.equal(0,0, "replaceImage works");
t.end();
} });
});

29
test/modules/run.js Normal file
View File

@@ -0,0 +1,29 @@
'use strict';
var fs = require('fs');
var test = require('tape');
var DataURItoBuffer = require('data-uri-to-buffer');
require('../../src/ImageSequencer.js');
var sequencer = ImageSequencer({ ui: false });
var red = "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAQABADASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAf/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAABgj/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABykX//Z";
sequencer.loadImages('image1', red);
sequencer.addSteps('invert');
sequencer.addSteps('invert');
sequencer.addSteps('invert');
test('run() works with all possible argument combinations',function(t){
sequencer.run(function (out) {
var output1 = DataURItoBuffer(sequencer.images.image1.steps.slice(-1)[0].output.src);
sequencer.images.image1.steps.splice(1,1);
sequencer.run({index: 1},function(out){
var output2 = DataURItoBuffer(sequencer.images.image1.steps.slice(-1)[0].output.src);
t.deepEqual(output1,output2,"output remains same after removing a step and running sequencer from a greater index");
sequencer.run(function(out){
t.end();
})
});
});
});