├── .Rbuildignore ├── .circleci └── config.yml ├── .eslintignore ├── .eslintrc ├── .github └── FUNDING.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── .pylintrc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DESCRIPTION ├── LICENSE ├── MANIFEST.in ├── NAMESPACE ├── Procfile ├── R ├── dashCanvas.R └── internal.R ├── README.md ├── _validate_init.py ├── app.py ├── app2_correct_segmentation.py ├── app3_background_removal.py ├── app4_measure_length.py ├── app5_stitching.py ├── app_pics ├── app1.png ├── app2.png ├── app3.png ├── app4.png └── app5.png ├── assets ├── bWLwgP.css ├── ballet_dancer.jpg ├── base.css ├── dancer_boy.jpg ├── dress.jpg ├── gallery-style.css ├── general-app-page.css ├── tile_0_0.jpg ├── tile_0_1.jpg ├── tile_0_2.jpg ├── tile_0_3.jpg ├── tile_1_0.jpg ├── tile_1_1.jpg ├── tile_1_2.jpg └── tile_1_3.jpg ├── babel.config.js ├── dash_canvas ├── AlternativeCanvas.py ├── DashCanvas.py ├── __init__.py ├── _imports_.py ├── async-canvas.dev.js ├── async-canvas.js ├── components │ ├── __init__.py │ └── _components.py ├── dash_canvas.dev.js ├── dash_canvas.min.js ├── metadata.json ├── package.json ├── test │ ├── __init__.py │ ├── test_image_processing_utils.py │ ├── test_parse_json.py │ └── test_registration.py └── utils │ ├── __init__.py │ ├── exposure.py │ ├── image_processing_utils.py │ ├── io_utils.py │ ├── parse_json.py │ ├── plot_utils.py │ └── registration.py ├── doc └── segmentation.gif ├── extract-meta.js ├── index.html ├── index.py ├── inst └── deps │ ├── dash_canvas.dev.js │ └── dash_canvas.min.js ├── man └── dashCanvas.Rd ├── package-lock.json ├── package.json ├── requirements.txt ├── requirements ├── app.txt └── package.txt ├── review_checklist.md ├── setup.py ├── src ├── demo │ ├── App.js │ └── index.js └── lib │ ├── components │ └── DashCanvas.react.js │ ├── fragments │ └── DashCanvas.react.js │ └── index.js ├── standalone_app5.py ├── tests ├── __init__.py ├── data_test.json ├── requirements.txt └── test_dash_canvas.py ├── usage.py ├── webpack.config.js └── webpack.serve.config.js /.Rbuildignore: -------------------------------------------------------------------------------- 1 | # ignore JS config files/folders 2 | node_modules/ 3 | coverage/ 4 | src/ 5 | lib/ 6 | .babelrc 7 | .builderrc 8 | .eslintrc 9 | .npmignore 10 | 11 | # demo folder has special meaning in R 12 | # this should hopefully make it still 13 | # allow for the possibility to make R demos 14 | demo/.*\.js 15 | demo/.*\.html 16 | demo/.*\.css 17 | 18 | # ignore python files/folders 19 | setup.py 20 | usage.py 21 | setup.py 22 | requirements.txt 23 | MANIFEST.in 24 | CHANGELOG.md 25 | test/ 26 | # CRAN has weird LICENSE requirements 27 | LICENSE.txt 28 | ^.*\.Rproj$ 29 | ^\.Rproj\.user$ 30 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | 'python-3.6': 5 | docker: 6 | - image: circleci/python:3.6-stretch-node-browsers 7 | environment: 8 | PYTHON_VERSION: py36 9 | PERCY_ENABLE: 0 10 | 11 | steps: 12 | - checkout 13 | 14 | - run: 15 | name: Create virtual env 16 | command: python -m venv || virtualenv venv 17 | 18 | - run: 19 | name: Write job name 20 | command: echo $CIRCLE_JOB > circlejob.txt 21 | 22 | - restore_cache: 23 | key: deps1-{{ .Branch }}-{{ checksum "requirements/package.txt" }}-{{ checksum "package-lock.json" }}-{{ checksum ".circleci/config.yml" }}-{{ checksum "circlejob.txt" }} 24 | 25 | - run: 26 | name: Install dependencies 27 | command: | 28 | . venv/bin/activate 29 | pip install --progress-bar off -r requirements/package.txt 30 | npm ci 31 | 32 | - save_cache: 33 | key: deps1-{{ .Branch }}-{{ checksum "requirements/package.txt" }}-{{ checksum "package-lock.json" }}-{{ checksum ".circleci/config.yml" }}-{{ checksum "circlejob.txt" }} 34 | paths: 35 | - 'venv' 36 | - 'node_modules' 37 | 38 | - run: 39 | name: Build 40 | command: | 41 | . venv/bin/activate 42 | npm run build 43 | python setup.py develop 44 | 45 | - run: 46 | name: Run tests 47 | command: | 48 | . venv/bin/activate 49 | pytest dash_canvas 50 | 51 | - run: 52 | name: Run usage tests 53 | command: | 54 | . venv/bin/activate 55 | pytest tests 56 | 57 | 58 | 59 | 60 | workflows: 61 | version: 2 62 | build: 63 | jobs: 64 | - 'python-3.6' 65 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.css 2 | registerServiceWorker.js -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "prettier"], 3 | "parser": "babel-eslint", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module", 7 | "ecmaFeatures": { 8 | "arrowFunctions": true, 9 | "blockBindings": true, 10 | "classes": true, 11 | "defaultParams": true, 12 | "destructuring": true, 13 | "forOf": true, 14 | "generators": true, 15 | "modules": true, 16 | "templateStrings": true, 17 | "jsx": true 18 | } 19 | }, 20 | "env": { 21 | "browser": true, 22 | "es6": true, 23 | "jasmine": true, 24 | "jest": true, 25 | "node": true 26 | }, 27 | "globals": { 28 | "jest": true 29 | }, 30 | "plugins": [ 31 | "react", 32 | "import" 33 | ], 34 | "rules": { 35 | "accessor-pairs": ["error"], 36 | "block-scoped-var": ["error"], 37 | "consistent-return": ["error"], 38 | "curly": ["error", "all"], 39 | "default-case": ["error"], 40 | "dot-location": ["off"], 41 | "dot-notation": ["error"], 42 | "eqeqeq": ["error"], 43 | "guard-for-in": ["off"], 44 | "import/named": ["off"], 45 | "import/no-duplicates": ["error"], 46 | "import/no-named-as-default": ["error"], 47 | "new-cap": ["error"], 48 | "no-alert": [1], 49 | "no-caller": ["error"], 50 | "no-case-declarations": ["error"], 51 | "no-console": ["off"], 52 | "no-div-regex": ["error"], 53 | "no-dupe-keys": ["error"], 54 | "no-else-return": ["error"], 55 | "no-empty-pattern": ["error"], 56 | "no-eq-null": ["error"], 57 | "no-eval": ["error"], 58 | "no-extend-native": ["error"], 59 | "no-extra-bind": ["error"], 60 | "no-extra-boolean-cast": ["error"], 61 | "no-inline-comments": ["error"], 62 | "no-implicit-coercion": ["error"], 63 | "no-implied-eval": ["error"], 64 | "no-inner-declarations": ["off"], 65 | "no-invalid-this": ["error"], 66 | "no-iterator": ["error"], 67 | "no-labels": ["error"], 68 | "no-lone-blocks": ["error"], 69 | "no-loop-func": ["error"], 70 | "no-multi-str": ["error"], 71 | "no-native-reassign": ["error"], 72 | "no-new": ["error"], 73 | "no-new-func": ["error"], 74 | "no-new-wrappers": ["error"], 75 | "no-param-reassign": ["error"], 76 | "no-process-env": ["warn"], 77 | "no-proto": ["error"], 78 | "no-redeclare": ["error"], 79 | "no-return-assign": ["error"], 80 | "no-script-url": ["error"], 81 | "no-self-compare": ["error"], 82 | "no-sequences": ["error"], 83 | "no-shadow": ["off"], 84 | "no-throw-literal": ["error"], 85 | "no-undefined": ["error"], 86 | "no-unused-expressions": ["error"], 87 | "no-use-before-define": ["error", "nofunc"], 88 | "no-useless-call": ["error"], 89 | "no-useless-concat": ["error"], 90 | "no-with": ["error"], 91 | "prefer-const": ["error"], 92 | "radix": ["error"], 93 | "react/jsx-no-duplicate-props": ["error"], 94 | "react/jsx-no-undef": ["error"], 95 | "react/jsx-uses-react": ["error"], 96 | "react/jsx-uses-vars": ["error"], 97 | "react/no-did-update-set-state": ["error"], 98 | "react/no-direct-mutation-state": ["error"], 99 | "react/no-is-mounted": ["error"], 100 | "react/no-unknown-property": ["error"], 101 | "react/prefer-es6-class": ["error", "always"], 102 | "react/prop-types": "error", 103 | "valid-jsdoc": ["off"], 104 | "yoda": ["error"], 105 | "spaced-comment": ["error", "always", { 106 | "block": { 107 | exceptions: ["*"] 108 | } 109 | }], 110 | "no-unused-vars": ["error", { 111 | "args": "after-used", 112 | "argsIgnorePattern": "^_", 113 | "caughtErrorsIgnorePattern": "^e$" 114 | }], 115 | "no-magic-numbers": ["error", { 116 | "ignoreArrayIndexes": true, 117 | "ignore": [-1, 0, 1, 2, 3, 100, 10, 0.5] 118 | }], 119 | "no-underscore-dangle": ["off"] 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://plotly.com/products/consulting-and-oem/ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | jspm_packages/ 4 | 5 | # Optional npm cache directory 6 | .npm 7 | 8 | # Optional eslint cache 9 | .eslintcache 10 | 11 | # Optional REPL history 12 | .node_repl_history 13 | 14 | # Output of 'npm pack' 15 | *.tgz 16 | 17 | # Yarn Integrity file 18 | .yarn-integrity 19 | 20 | 21 | # Distribution / packaging 22 | .Python 23 | build/ 24 | develop-eggs/ 25 | dist/ 26 | downloads/ 27 | eggs/ 28 | .eggs/ 29 | lib64/ 30 | parts/ 31 | sdist/ 32 | var/ 33 | wheels/ 34 | *.egg-info/ 35 | .installed.cfg 36 | *.egg 37 | MANIFEST 38 | 39 | 40 | # Environments 41 | .venv 42 | env/ 43 | venv/ 44 | ENV/ 45 | env.bak/ 46 | venv.bak/ 47 | 48 | 49 | *.pyc 50 | .DS_Store 51 | .env 52 | 53 | *~ 54 | *swp 55 | *lprof 56 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # testing 5 | /coverage 6 | 7 | # misc 8 | .DS_Store 9 | .env.local 10 | .env.development.local 11 | .env.test.local 12 | .env.production.local 13 | 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | 18 | # Development folders and files 19 | public 20 | src 21 | scripts 22 | config 23 | .travis.yml 24 | CHANGELOG.md 25 | README.md 26 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "singleQuote": true, 4 | "bracketSpacing": false, 5 | "trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code 6 | extension-pkg-whitelist= 7 | 8 | # Add files or directories to the blacklist. They should be base names, not 9 | # paths. 10 | ignore=CVS 11 | 12 | # Add files or directories matching the regex patterns to the blacklist. The 13 | # regex matches against base names, not paths. 14 | ignore-patterns= 15 | 16 | # Python code to execute, usually for sys.path manipulation such as 17 | # pygtk.require(). 18 | #init-hook= 19 | 20 | # Use multiple processes to speed up Pylint. 21 | jobs=1 22 | 23 | # List of plugins (as comma separated values of python modules names) to load, 24 | # usually to register additional checkers. 25 | load-plugins= 26 | 27 | # Pickle collected data for later comparisons. 28 | persistent=yes 29 | 30 | # Specify a configuration file. 31 | #rcfile= 32 | 33 | # When enabled, pylint would attempt to guess common misconfiguration and emit 34 | # user-friendly hints instead of false-positive error messages 35 | suggestion-mode=yes 36 | 37 | # Allow loading of arbitrary C extensions. Extensions are imported into the 38 | # active Python interpreter and may run arbitrary code. 39 | unsafe-load-any-extension=no 40 | 41 | 42 | [MESSAGES CONTROL] 43 | 44 | # Only show warnings with the listed confidence levels. Leave empty to show 45 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 46 | confidence= 47 | 48 | # Disable the message, report, category or checker with the given id(s). You 49 | # can either give multiple identifiers separated by comma (,) or put this 50 | # option multiple times (only on the command line, not in the configuration 51 | # file where it should appear only once).You can also use "--disable=all" to 52 | # disable everything first and then reenable specific checks. For example, if 53 | # you want to run only the similarities checker, you can use "--disable=all 54 | # --enable=similarities". If you want to run only the classes checker, but have 55 | # no Warning level messages displayed, use"--disable=all --enable=classes 56 | # --disable=W" 57 | disable=print-statement, 58 | parameter-unpacking, 59 | unpacking-in-except, 60 | old-raise-syntax, 61 | backtick, 62 | long-suffix, 63 | old-ne-operator, 64 | old-octal-literal, 65 | import-star-module-level, 66 | non-ascii-bytes-literal, 67 | raw-checker-failed, 68 | bad-inline-option, 69 | locally-disabled, 70 | locally-enabled, 71 | file-ignored, 72 | suppressed-message, 73 | useless-suppression, 74 | deprecated-pragma, 75 | apply-builtin, 76 | basestring-builtin, 77 | buffer-builtin, 78 | cmp-builtin, 79 | coerce-builtin, 80 | execfile-builtin, 81 | file-builtin, 82 | long-builtin, 83 | raw_input-builtin, 84 | reduce-builtin, 85 | standarderror-builtin, 86 | unicode-builtin, 87 | xrange-builtin, 88 | coerce-method, 89 | delslice-method, 90 | getslice-method, 91 | setslice-method, 92 | no-absolute-import, 93 | old-division, 94 | dict-iter-method, 95 | dict-view-method, 96 | next-method-called, 97 | metaclass-assignment, 98 | indexing-exception, 99 | raising-string, 100 | reload-builtin, 101 | oct-method, 102 | hex-method, 103 | nonzero-method, 104 | cmp-method, 105 | input-builtin, 106 | round-builtin, 107 | intern-builtin, 108 | unichr-builtin, 109 | map-builtin-not-iterating, 110 | zip-builtin-not-iterating, 111 | range-builtin-not-iterating, 112 | filter-builtin-not-iterating, 113 | using-cmp-argument, 114 | eq-without-hash, 115 | div-method, 116 | idiv-method, 117 | rdiv-method, 118 | exception-message-attribute, 119 | invalid-str-codec, 120 | sys-max-int, 121 | bad-python3-import, 122 | deprecated-string-function, 123 | deprecated-str-translate-call, 124 | deprecated-itertools-function, 125 | deprecated-types-field, 126 | next-method-defined, 127 | dict-items-not-iterating, 128 | dict-keys-not-iterating, 129 | dict-values-not-iterating, 130 | no-member, 131 | missing-docstring, 132 | invalid-name, 133 | redefined-builtin, 134 | wrong-import-order, 135 | too-many-arguments, 136 | too-many-locals, 137 | consider-using-enumerate, 138 | len-as-condition, 139 | too-many-branches, 140 | too-many-statements, 141 | blacklisted-name, 142 | line-too-long, 143 | bare-except, 144 | duplicate-code, 145 | too-many-function-args, 146 | attribute-defined-outside-init, 147 | broad-except 148 | 149 | # Enable the message, report, category or checker with the given id(s). You can 150 | # either give multiple identifier separated by comma (,) or put this option 151 | # multiple time (only on the command line, not in the configuration file where 152 | # it should appear only once). See also the "--disable" option for examples. 153 | enable=c-extension-no-member 154 | 155 | 156 | [REPORTS] 157 | 158 | # Python expression which should return a note less than 10 (10 is the highest 159 | # note). You have access to the variables errors warning, statement which 160 | # respectively contain the number of errors / warnings messages and the total 161 | # number of statements analyzed. This is used by the global evaluation report 162 | # (RP0004). 163 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 164 | 165 | # Template used to display messages. This is a python new-style format string 166 | # used to format the message information. See doc for all details 167 | #msg-template= 168 | 169 | # Set the output format. Available formats are text, parseable, colorized, json 170 | # and msvs (visual studio).You can also give a reporter class, eg 171 | # mypackage.mymodule.MyReporterClass. 172 | output-format=text 173 | 174 | # Tells whether to display a full report or only the messages 175 | reports=no 176 | 177 | # Activate the evaluation score. 178 | score=yes 179 | 180 | 181 | [REFACTORING] 182 | 183 | # Maximum number of nested blocks for function / method body 184 | max-nested-blocks=5 185 | 186 | # Complete name of functions that never returns. When checking for 187 | # inconsistent-return-statements if a never returning function is called then 188 | # it will be considered as an explicit return statement and no message will be 189 | # printed. 190 | never-returning-functions=optparse.Values,sys.exit 191 | 192 | 193 | [BASIC] 194 | 195 | # Naming style matching correct argument names 196 | argument-naming-style=snake_case 197 | 198 | # Regular expression matching correct argument names. Overrides argument- 199 | # naming-style 200 | #argument-rgx= 201 | 202 | # Naming style matching correct attribute names 203 | attr-naming-style=snake_case 204 | 205 | # Regular expression matching correct attribute names. Overrides attr-naming- 206 | # style 207 | #attr-rgx= 208 | 209 | # Bad variable names which should always be refused, separated by a comma 210 | bad-names=foo, 211 | bar, 212 | baz, 213 | toto, 214 | tutu, 215 | tata 216 | 217 | # Naming style matching correct class attribute names 218 | class-attribute-naming-style=any 219 | 220 | # Regular expression matching correct class attribute names. Overrides class- 221 | # attribute-naming-style 222 | #class-attribute-rgx= 223 | 224 | # Naming style matching correct class names 225 | class-naming-style=PascalCase 226 | 227 | # Regular expression matching correct class names. Overrides class-naming-style 228 | #class-rgx= 229 | 230 | # Naming style matching correct constant names 231 | const-naming-style=UPPER_CASE 232 | 233 | # Regular expression matching correct constant names. Overrides const-naming- 234 | # style 235 | #const-rgx= 236 | 237 | # Minimum line length for functions/classes that require docstrings, shorter 238 | # ones are exempt. 239 | docstring-min-length=-1 240 | 241 | # Naming style matching correct function names 242 | function-naming-style=snake_case 243 | 244 | # Regular expression matching correct function names. Overrides function- 245 | # naming-style 246 | #function-rgx= 247 | 248 | # Good variable names which should always be accepted, separated by a comma 249 | good-names=i, 250 | j, 251 | k, 252 | ex, 253 | Run, 254 | _ 255 | 256 | # Include a hint for the correct naming format with invalid-name 257 | include-naming-hint=no 258 | 259 | # Naming style matching correct inline iteration names 260 | inlinevar-naming-style=any 261 | 262 | # Regular expression matching correct inline iteration names. Overrides 263 | # inlinevar-naming-style 264 | #inlinevar-rgx= 265 | 266 | # Naming style matching correct method names 267 | method-naming-style=snake_case 268 | 269 | # Regular expression matching correct method names. Overrides method-naming- 270 | # style 271 | #method-rgx= 272 | 273 | # Naming style matching correct module names 274 | module-naming-style=snake_case 275 | 276 | # Regular expression matching correct module names. Overrides module-naming- 277 | # style 278 | #module-rgx= 279 | 280 | # Colon-delimited sets of names that determine each other's naming style when 281 | # the name regexes allow several styles. 282 | name-group= 283 | 284 | # Regular expression which should only match function or class names that do 285 | # not require a docstring. 286 | no-docstring-rgx=^_ 287 | 288 | # List of decorators that produce properties, such as abc.abstractproperty. Add 289 | # to this list to register other decorators that produce valid properties. 290 | property-classes=abc.abstractproperty 291 | 292 | # Naming style matching correct variable names 293 | variable-naming-style=snake_case 294 | 295 | # Regular expression matching correct variable names. Overrides variable- 296 | # naming-style 297 | #variable-rgx= 298 | 299 | 300 | [FORMAT] 301 | 302 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 303 | expected-line-ending-format= 304 | 305 | # Regexp for a line that is allowed to be longer than the limit. 306 | ignore-long-lines=^\s*(# )??$ 307 | 308 | # Number of spaces of indent required inside a hanging or continued line. 309 | indent-after-paren=4 310 | 311 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 312 | # tab). 313 | indent-string=' ' 314 | 315 | # Maximum number of characters on a single line. 316 | max-line-length=100 317 | 318 | # Maximum number of lines in a module 319 | max-module-lines=1000 320 | 321 | # List of optional constructs for which whitespace checking is disabled. `dict- 322 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 323 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 324 | # `empty-line` allows space-only lines. 325 | no-space-check=trailing-comma, 326 | dict-separator 327 | 328 | # Allow the body of a class to be on the same line as the declaration if body 329 | # contains single statement. 330 | single-line-class-stmt=no 331 | 332 | # Allow the body of an if to be on the same line as the test if there is no 333 | # else. 334 | single-line-if-stmt=no 335 | 336 | 337 | [LOGGING] 338 | 339 | # Logging modules to check that the string format arguments are in logging 340 | # function parameter format 341 | logging-modules=logging 342 | 343 | 344 | [MISCELLANEOUS] 345 | 346 | # List of note tags to take in consideration, separated by a comma. 347 | notes=FIXME, 348 | XXX, 349 | 350 | 351 | [SIMILARITIES] 352 | 353 | # Ignore comments when computing similarities. 354 | ignore-comments=yes 355 | 356 | # Ignore docstrings when computing similarities. 357 | ignore-docstrings=yes 358 | 359 | # Ignore imports when computing similarities. 360 | ignore-imports=no 361 | 362 | # Minimum lines number of a similarity. 363 | min-similarity-lines=4 364 | 365 | 366 | [SPELLING] 367 | 368 | # Limits count of emitted suggestions for spelling mistakes 369 | max-spelling-suggestions=4 370 | 371 | # Spelling dictionary name. Available dictionaries: none. To make it working 372 | # install python-enchant package. 373 | spelling-dict= 374 | 375 | # List of comma separated words that should not be checked. 376 | spelling-ignore-words= 377 | 378 | # A path to a file that contains private dictionary; one word per line. 379 | spelling-private-dict-file= 380 | 381 | # Tells whether to store unknown words to indicated private dictionary in 382 | # --spelling-private-dict-file option instead of raising a message. 383 | spelling-store-unknown-words=no 384 | 385 | 386 | [TYPECHECK] 387 | 388 | # List of decorators that produce context managers, such as 389 | # contextlib.contextmanager. Add to this list to register other decorators that 390 | # produce valid context managers. 391 | contextmanager-decorators=contextlib.contextmanager 392 | 393 | # List of members which are set dynamically and missed by pylint inference 394 | # system, and so shouldn't trigger E1101 when accessed. Python regular 395 | # expressions are accepted. 396 | generated-members= 397 | 398 | # Tells whether missing members accessed in mixin class should be ignored. A 399 | # mixin class is detected if its name ends with "mixin" (case insensitive). 400 | ignore-mixin-members=yes 401 | 402 | # This flag controls whether pylint should warn about no-member and similar 403 | # checks whenever an opaque object is returned when inferring. The inference 404 | # can return multiple potential results while evaluating a Python object, but 405 | # some branches might not be evaluated, which results in partial inference. In 406 | # that case, it might be useful to still emit no-member and other checks for 407 | # the rest of the inferred objects. 408 | ignore-on-opaque-inference=yes 409 | 410 | # List of class names for which member attributes should not be checked (useful 411 | # for classes with dynamically set attributes). This supports the use of 412 | # qualified names. 413 | ignored-classes=optparse.Values,thread._local,_thread._local 414 | 415 | # List of module names for which member attributes should not be checked 416 | # (useful for modules/projects where namespaces are manipulated during runtime 417 | # and thus existing member attributes cannot be deduced by static analysis. It 418 | # supports qualified module names, as well as Unix pattern matching. 419 | ignored-modules= 420 | 421 | # Show a hint with possible names when a member name was not found. The aspect 422 | # of finding the hint is based on edit distance. 423 | missing-member-hint=yes 424 | 425 | # The minimum edit distance a name should have in order to be considered a 426 | # similar match for a missing member name. 427 | missing-member-hint-distance=1 428 | 429 | # The total number of similar names that should be taken in consideration when 430 | # showing a hint for a missing member. 431 | missing-member-max-choices=1 432 | 433 | 434 | [VARIABLES] 435 | 436 | # List of additional names supposed to be defined in builtins. Remember that 437 | # you should avoid to define new builtins when possible. 438 | additional-builtins= 439 | 440 | # Tells whether unused global variables should be treated as a violation. 441 | allow-global-unused-variables=yes 442 | 443 | # List of strings which can identify a callback function by name. A callback 444 | # name must start or end with one of those strings. 445 | callbacks=cb_, 446 | _cb 447 | 448 | # A regular expression matching the name of dummy variables (i.e. expectedly 449 | # not used). 450 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 451 | 452 | # Argument names that match this expression will be ignored. Default to name 453 | # with leading underscore 454 | ignored-argument-names=_.*|^ignored_|^unused_ 455 | 456 | # Tells whether we should check for unused import in __init__ files. 457 | init-import=no 458 | 459 | # List of qualified module names which can have objects that can redefine 460 | # builtins. 461 | redefining-builtins-modules=six.moves,past.builtins,future.builtins 462 | 463 | 464 | [CLASSES] 465 | 466 | # List of method names used to declare (i.e. assign) instance attributes. 467 | defining-attr-methods=__init__, 468 | __new__, 469 | setUp 470 | 471 | # List of member names, which should be excluded from the protected access 472 | # warning. 473 | exclude-protected=_asdict, 474 | _fields, 475 | _replace, 476 | _source, 477 | _make 478 | 479 | # List of valid names for the first argument in a class method. 480 | valid-classmethod-first-arg=cls 481 | 482 | # List of valid names for the first argument in a metaclass class method. 483 | valid-metaclass-classmethod-first-arg=mcs 484 | 485 | 486 | [DESIGN] 487 | 488 | # Maximum number of arguments for function / method 489 | max-args=5 490 | 491 | # Maximum number of attributes for a class (see R0902). 492 | max-attributes=7 493 | 494 | # Maximum number of boolean expressions in a if statement 495 | max-bool-expr=5 496 | 497 | # Maximum number of branch for function / method body 498 | max-branches=12 499 | 500 | # Maximum number of locals for function / method body 501 | max-locals=15 502 | 503 | # Maximum number of parents for a class (see R0901). 504 | max-parents=7 505 | 506 | # Maximum number of public methods for a class (see R0904). 507 | max-public-methods=20 508 | 509 | # Maximum number of return / yield for function / method body 510 | max-returns=6 511 | 512 | # Maximum number of statements in function / method body 513 | max-statements=50 514 | 515 | # Minimum number of public methods for a class (see R0903). 516 | min-public-methods=2 517 | 518 | 519 | [IMPORTS] 520 | 521 | # Allow wildcard imports from modules that define __all__. 522 | allow-wildcard-with-all=no 523 | 524 | # Analyse import fallback blocks. This can be used to support both Python 2 and 525 | # 3 compatible code, which means that the block might have code that exists 526 | # only in one or another interpreter, leading to false positives when analysed. 527 | analyse-fallback-blocks=no 528 | 529 | # Deprecated modules which should not be used, separated by a comma 530 | deprecated-modules=optparse,tkinter.tix 531 | 532 | # Create a graph of external dependencies in the given file (report RP0402 must 533 | # not be disabled) 534 | ext-import-graph= 535 | 536 | # Create a graph of every (i.e. internal and external) dependencies in the 537 | # given file (report RP0402 must not be disabled) 538 | import-graph= 539 | 540 | # Create a graph of internal dependencies in the given file (report RP0402 must 541 | # not be disabled) 542 | int-import-graph= 543 | 544 | # Force import order to recognize a module as part of the standard 545 | # compatibility libraries. 546 | known-standard-library= 547 | 548 | # Force import order to recognize a module as part of a third party library. 549 | known-third-party=enchant 550 | 551 | 552 | [EXCEPTIONS] 553 | 554 | # Exceptions that will emit a warning when being caught. Defaults to 555 | # "Exception" 556 | overgeneral-exceptions=Exception 557 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log for dash-canvas 2 | All notable changes to this project will be documented in this file. 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | ## [Unreleased] 6 | ### Changed 7 | - [#44](https://github.com/plotly/dash-canvas/pull/44) Renamed async modules with hyphen `-` instead of tilde `~` 8 | - [#47](https://github.com/plotly/dash-canvas/pull/47) Update from React 16.8.6 to 16.13.0 9 | 10 | ## [0.1.0] - 2019-11-05 11 | ### Added 12 | - [#29](https://github.com/plotly/dash-canvas/pull/29) Async canvas component 13 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at accounts@plot.ly. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.4, available at [http://contributor-covenant.org/version/1/4](http://contributor-covenant.org/version/1/4/), and may also be found online at . 44 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | Welcome to dash-canvas! 4 | 5 | ## Structure of the code 6 | 7 | React component DashCanvas: `src/lib/components/DashCanvas.react.js` 8 | 9 | Build Javascript component: `npm run build:all` at the root (see https://dash.plotly.com/react-for-python-developers for more details). 10 | 11 | Utility Python functions: `dash_canvas/utils` 12 | 13 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: dashCanvas 2 | Title: Sketching pad for dash based on react-sketch 3 | Version: 0.0.10 4 | Authors @R: as.person(c(Emmanuelle Gouillart )) 5 | Description: Sketching pad for dash based on react-sketch 6 | Depends: R (>= 3.0.2) 7 | Imports: dash 8 | Suggests: testthat, roxygen2 9 | License: MIT + file LICENSE 10 | URL: https://github.com/plotly/dash-canvas 11 | BugReports: https://github.com/plotly/dash-canvas/issues 12 | Encoding: UTF-8 13 | LazyData: true 14 | Author: Emmanuelle Gouillart [aut] 15 | Maintainer: Emmanuelle Gouillart 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-canvas/4f4df65b43acb20bae7eb34572ca240dabfbfb72/LICENSE -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include dash_canvas/dash_canvas.min.js 2 | include dash_canvas/dash_canvas.dev.js 3 | include dash_canvas/async-*.js 4 | include dash_canvas/metadata.json 5 | include dash_canvas/package.json 6 | include README.md 7 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # AUTO GENERATED FILE - DO NOT EDIT 2 | 3 | export(dashCanvas) 4 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn index:server 2 | -------------------------------------------------------------------------------- /R/dashCanvas.R: -------------------------------------------------------------------------------- 1 | # AUTO GENERATED FILE - DO NOT EDIT 2 | 3 | dashCanvas <- function(id=NULL, image_content=NULL, zoom=NULL, width=NULL, height=NULL, scale=NULL, tool=NULL, lineWidth=NULL, lineColor=NULL, goButtonTitle=NULL, filename=NULL, trigger=NULL, json_data=NULL, hide_buttons=NULL) { 4 | 5 | component <- list( 6 | props = list(id=id, image_content=image_content, zoom=zoom, width=width, height=height, scale=scale, tool=tool, lineWidth=lineWidth, lineColor=lineColor, goButtonTitle=goButtonTitle, filename=filename, trigger=trigger, json_data=json_data, hide_buttons=hide_buttons), 7 | type = 'DashCanvas', 8 | namespace = 'dash_canvas', 9 | propNames = c('id', 'image_content', 'zoom', 'width', 'height', 'scale', 'tool', 'lineWidth', 'lineColor', 'goButtonTitle', 'filename', 'trigger', 'json_data', 'hide_buttons'), 10 | package = 'dashCanvas' 11 | ) 12 | 13 | component$props <- filter_null(component$props) 14 | 15 | structure(component, class = c('dash_component', 'list')) 16 | } -------------------------------------------------------------------------------- /R/internal.R: -------------------------------------------------------------------------------- 1 | .dashCanvas_js_metadata <- function() { 2 | deps_metadata <- list(`dash_canvas` = structure(list(name = "dash_canvas", 3 | version = "0.0.10", src = list(href = NULL, 4 | file = "deps"), meta = NULL, 5 | script = "dash_canvas.min.js", 6 | stylesheet = NULL, head = NULL, attachment = NULL, package = "dashCanvas", 7 | all_files = FALSE), class = "html_dependency"), 8 | `dash_canvas_dev` = structure(list(name = "dash_canvas", 9 | version = "0.0.10", src = list(href = NULL, 10 | file = "deps"), meta = NULL, 11 | script = "dash_canvas.dev.js", 12 | stylesheet = NULL, head = NULL, attachment = NULL, package = "dashCanvas", 13 | all_files = FALSE), class = "html_dependency") 14 | ) 15 | return(deps_metadata) 16 | } 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dash-canvas 2 | 3 |
4 | 5 | Maintained by Plotly 6 | 7 |
8 | 9 | 10 | dash-canvas is a package for image processing with 11 | [Dash](https://dash.plotly.com/). It provides a Dash component for 12 | annotating images, as well as utility functions for using such 13 | annotations for various image processing tasks. 14 | 15 | Try out the 16 | [gallery of examples](https://dash-canvas.plotly.host/Portal/) and [read 17 | the docs](https://dash.plotly.com/canvas) to learn how to use dash-canvas. 18 | 19 |

Segmentation app

20 | 21 | 22 | Get started with: 23 | 1. Install `dash_canvas`: `pip install dash-canvas` (you will also need 24 | `dash-core-components` to run the apps). 25 | 2. Run `python app_seg.py` (for interactive segmentation) or 26 | `python correct_segmentation.py` (for correcting a pre-existing 27 | segmentation) 28 | 3. Visit http://localhost:8050 in your web browser 29 | 30 | 31 | -------------------------------------------------------------------------------- /_validate_init.py: -------------------------------------------------------------------------------- 1 | """ 2 | DO NOT MODIFY 3 | This file is used to validate your publish settings. 4 | """ 5 | from __future__ import print_function 6 | 7 | import os 8 | import sys 9 | import importlib 10 | 11 | 12 | components_package = 'dash_canvas' 13 | 14 | components_lib = importlib.import_module(components_package) 15 | 16 | missing_dist_msg = 'Warning {} was not found in `{}.__init__.{}`!!!' 17 | missing_manifest_msg = ''' 18 | Warning {} was not found in `MANIFEST.in`! 19 | It will not be included in the build! 20 | ''' 21 | 22 | with open('MANIFEST.in', 'r') as f: 23 | manifest = f.read() 24 | 25 | 26 | def check_dist(dist, filename): 27 | # Support the dev bundle. 28 | if filename.endswith('dev.js'): 29 | return True 30 | 31 | return any( 32 | filename in x 33 | for d in dist 34 | for x in ( 35 | [d.get('relative_package_path')] 36 | if not isinstance(d.get('relative_package_path'), list) 37 | else d.get('relative_package_path') 38 | ) 39 | ) 40 | 41 | 42 | def check_manifest(filename): 43 | return filename in manifest 44 | 45 | 46 | def check_file(dist, filename): 47 | if not check_dist(dist, filename): 48 | print( 49 | missing_dist_msg.format(filename, components_package, '_js_dist'), 50 | file=sys.stderr 51 | ) 52 | if not check_manifest(filename): 53 | print(missing_manifest_msg.format(filename), 54 | file=sys.stderr) 55 | 56 | 57 | for cur, _, files in os.walk(components_package): 58 | for f in files: 59 | 60 | if f.endswith('js'): 61 | # noinspection PyProtectedMember 62 | check_file(components_lib._js_dist, f) 63 | elif f.endswith('css'): 64 | # noinspection PyProtectedMember 65 | check_file(components_lib._css_dist, f) 66 | elif not f.endswith('py'): 67 | check_manifest(f) 68 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | 2 | from dash_canvas import DashCanvas 3 | import dash 4 | from dash.dependencies import Input, Output, State 5 | import dash_html_components as html 6 | import dash_core_components as dcc 7 | import plotly.graph_objs as go 8 | import dash_daq as daq 9 | 10 | 11 | filename = 'https://www.publicdomainpictures.net/pictures/60000/nahled/flower-outline-coloring-page.jpg' 12 | canvas_width = 300 13 | 14 | app = dash.Dash(__name__) 15 | 16 | app.layout = html.Div([ 17 | html.Div([ 18 | DashCanvas( 19 | id='canvas-color', 20 | width=canvas_width, 21 | filename=filename, 22 | hide_buttons=['line', 'zoom', 'pan'], 23 | ) 24 | ], className="six columns"), 25 | html.Div([ 26 | html.H6(children=['Brush width']), 27 | dcc.Slider( 28 | id='bg-width-slider', 29 | min=2, 30 | max=40, 31 | step=1, 32 | value=[5] 33 | ), 34 | daq.ColorPicker( 35 | id='color-picker', 36 | label='Brush color', 37 | value='#119DFF' 38 | ), 39 | ], className="three columns"), 40 | ]) 41 | 42 | 43 | @app.callback(Output('canvas-color', 'lineColor'), 44 | [Input('color-picker', 'value')]) 45 | def update_canvas_linewidth(value): 46 | if isinstance(value, dict): 47 | return value['hex'] 48 | else: 49 | return value 50 | 51 | 52 | @app.callback(Output('canvas-color', 'lineWidth'), 53 | [Input('bg-width-slider', 'value')]) 54 | def update_canvas_linewidth(value): 55 | return value 56 | 57 | if __name__ == '__main__': 58 | app.run_server(debug=True) 59 | 60 | 61 | -------------------------------------------------------------------------------- /app2_correct_segmentation.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import json 3 | from skimage import io, color, segmentation, img_as_ubyte, filters, measure 4 | from PIL import Image 5 | 6 | 7 | import dash_canvas 8 | import dash 9 | from dash.dependencies import Input, Output, State 10 | import dash_html_components as html 11 | import dash_core_components as dcc 12 | import plotly.graph_objs as go 13 | 14 | from dash_canvas.utils.parse_json import parse_jsonstring 15 | from dash_canvas.utils.io_utils import image_string_to_PILImage, array_to_data_url 16 | from dash_canvas.utils.image_processing_utils import modify_segmentation 17 | 18 | 19 | # Image to segment and shape parameters 20 | filename = 'https://upload.wikimedia.org/wikipedia/commons/1/1b/HumanChromosomesChromomycinA3.jpg' 21 | img = io.imread(filename, as_gray=True) 22 | mask = img > 1.2 * filters.threshold_otsu(img) 23 | labels = measure.label(mask) 24 | 25 | 26 | overlay = segmentation.mark_boundaries(img, labels) 27 | overlay = img_as_ubyte(overlay) 28 | 29 | height, width = img.shape[:2] 30 | canvas_width = 500 31 | canvas_height = round(height * canvas_width / width) 32 | scale = canvas_width / width 33 | 34 | # ------------------ App definition --------------------- 35 | 36 | 37 | def title(): 38 | return "Segmentation post-processing" 39 | 40 | 41 | def description(): 42 | return "Merge or separate labeled regions in order to improve an automatic segmentation" 43 | 44 | 45 | layout = html.Div([ 46 | html.Div([ 47 | html.H3(children='Manual correction of automatic segmentation'), 48 | dcc.Markdown(''' 49 | Annotate the picture to delineate boundaries 50 | between objects (in split mode) or to join objects 51 | together (in merge mode), then press the 52 | "Update segmentation" button to correct 53 | the segmentation. 54 | '''), 55 | html.H5(children='Annotations'), 56 | dcc.RadioItems(id='mode', 57 | options=[ 58 | {'label': 'Merge objects', 'value': 'merge'}, 59 | {'label': 'Split objects', 'value': 'split'}, 60 | ], 61 | value='split', 62 | # labelStyle={'display': 'inline-block'} 63 | ), 64 | html.H5(children='Save segmentation'), 65 | dcc.RadioItems(id='save-mode', 66 | options=[ 67 | {'label': 'png', 'value': 'png'}, 68 | #{'label': 'raw', 'value': 'raw'}, 69 | ], 70 | value='png', 71 | labelStyle={'display': 'inline-block'} 72 | ), 73 | html.A( 74 | 'Download Data', 75 | id='download-link', 76 | download="correct_segmentation.png", 77 | href="", 78 | target="_blank" 79 | ), 80 | dcc.Store(id='cache', data=''), 81 | 82 | ], className="four columns"), 83 | html.Div([ 84 | dash_canvas.DashCanvas( 85 | id='canvas_', 86 | width=canvas_width, 87 | height=canvas_height, 88 | scale=scale, 89 | lineWidth=2, 90 | lineColor='red', 91 | image_content=array_to_data_url(overlay), 92 | goButtonTitle='Update segmentation', 93 | ), 94 | ], className="six columns"), 95 | ]) 96 | 97 | # ----------------------- Callbacks ----------------------------- 98 | 99 | 100 | def callbacks(app): 101 | @app.callback(Output('cache', 'data'), 102 | [Input('canvas_', 'trigger'),], 103 | [State('canvas_', 'json_data'), 104 | State('canvas_', 'scale'), 105 | State('canvas_', 'height'), 106 | State('canvas_', 'width'), 107 | State('cache', 'data'), 108 | State('mode', 'value')]) 109 | def update_segmentation(toggle, string, s, h, w, children, mode): 110 | print("updating") 111 | if len(children) == 0: 112 | labs = labels 113 | else: 114 | labs = np.asarray(children) 115 | with open('data.json', 'w') as fp: 116 | json.dump(string, fp) 117 | mask = parse_jsonstring(string, shape=(height, width)) 118 | new_labels = modify_segmentation(labs, mask, img=img, mode=mode) 119 | return new_labels 120 | 121 | 122 | @app.callback(Output('canvas_', 'image_content'), 123 | [Input('cache', 'data')]) 124 | def update_figure(labs): 125 | new_labels = np.array(labs) 126 | overlay = segmentation.mark_boundaries(img, new_labels) 127 | overlay = img_as_ubyte(overlay) 128 | return array_to_data_url(overlay) 129 | 130 | 131 | @app.callback(Output('download-link', 'download'), 132 | [Input('save-mode', 'value')]) 133 | def download_name(save_mode): 134 | if save_mode == 'png': 135 | return 'correct_segmentation.png' 136 | else: 137 | return 'correct_segmentation.raw' 138 | 139 | 140 | @app.callback(Output('download-link', 'href'), 141 | [Input('cache', 'data')], 142 | [State('save-mode', 'value')]) 143 | def save_segmentation(labs, save_mode): 144 | new_labels = np.array(labs) 145 | np.save('labels.npy', new_labels) 146 | if save_mode == 'png': 147 | color_labels = color.label2rgb(new_labels) 148 | uri = array_to_data_url(new_labels, dtype=np.uint8) 149 | return uri 150 | 151 | -------------------------------------------------------------------------------- /app3_background_removal.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import json 3 | from skimage import io 4 | from PIL import Image 5 | 6 | import dash_canvas 7 | import dash 8 | from dash.dependencies import Input, Output, State 9 | import dash_html_components as html 10 | import dash_core_components as dcc 11 | import plotly.graph_objs as go 12 | 13 | from dash_canvas.utils.parse_json import parse_jsonstring 14 | from dash_canvas.utils.image_processing_utils import \ 15 | superpixel_color_segmentation 16 | from dash_canvas.utils.plot_utils import image_with_contour 17 | from dash_canvas.utils.io_utils import image_string_to_PILImage, \ 18 | array_to_data_url 19 | from dash_canvas.components import image_upload_zone 20 | 21 | # Image to segment and shape parameters 22 | filename = './assets/dress.jpg' 23 | img = io.imread(filename) 24 | height, width, _ = img.shape 25 | canvas_width = 500 26 | canvas_height = round(height * canvas_width / width) 27 | scale = canvas_width / width 28 | 29 | 30 | def title(): 31 | return "Background removal" 32 | 33 | def description(): 34 | return "Remove background of image to extract objects of interest." 35 | 36 | 37 | layout = html.Div([ 38 | html.Div([ 39 | html.Div([ 40 | html.H2(children='Remove image background'), 41 | dcc.Markdown(''' 42 | Draw on the object of interest, and press Save to remove 43 | background.'''), 44 | dash_canvas.DashCanvas( 45 | id='canvas-bg', 46 | width=canvas_width, 47 | height=canvas_height, 48 | scale=scale, 49 | filename=filename, 50 | lineWidth=4, 51 | goButtonTitle='Remove background', 52 | hide_buttons=['line', 'zoom', 'pan'], 53 | ), 54 | html.H6(children=['Brush width']), 55 | dcc.Slider( 56 | id='bg-width-slider', 57 | min=2, 58 | max=40, 59 | step=1, 60 | value=[5] 61 | ), 62 | image_upload_zone('upload-image-bg'), 63 | ], className="six columns"), 64 | html.Div([ 65 | html.H3(children='Image without background'), 66 | html.Img(id='segmentation-bg', 67 | src=array_to_data_url(np.zeros_like(img)), 68 | width=canvas_width) 69 | ], className="six columns")], 70 | className="row") 71 | ]) 72 | 73 | # ----------------------- Callbacks ----------------------------- 74 | def callbacks(app): 75 | 76 | @app.callback(Output('segmentation-bg', 'src'), 77 | [Input('canvas-bg', 'image_content'), 78 | Input('canvas-bg', 'json_data'), 79 | Input('canvas-bg', 'height')], 80 | [State('canvas-bg', 'scale'), 81 | State('canvas-bg', 'width'), 82 | ]) 83 | def update_figure_upload(image, string, h, s, w): 84 | mask = parse_jsonstring(string, shape=(round(h/s), round(w/s))) 85 | if mask.sum() > 0: 86 | if image is None: 87 | im = img 88 | image = img 89 | else: 90 | im = image_string_to_PILImage(image) 91 | im = np.asarray(im) 92 | seg = superpixel_color_segmentation(im, mask) 93 | else: 94 | if image is None: 95 | image = img 96 | seg = np.ones((h, w)) 97 | fill_value = 255 * np.ones(3, dtype=np.uint8) 98 | dat = np.copy(im) 99 | dat[np.logical_not(seg)] = fill_value 100 | return array_to_data_url(dat) 101 | 102 | 103 | 104 | @app.callback(Output('canvas-bg', 'image_content'), 105 | [Input('upload-image-bg', 'contents')]) 106 | def update_canvas_upload(image_string): 107 | if image_string is None: 108 | raise ValueError 109 | if image_string is not None: 110 | return image_string 111 | else: 112 | return None 113 | 114 | 115 | @app.callback(Output('canvas-bg', 'height'), 116 | [Input('upload-image-bg', 'contents')], 117 | [State('canvas-bg', 'width'), 118 | State('canvas-bg', 'height')]) 119 | def update_canvas_upload_shape(image_string, w, h): 120 | if image_string is None: 121 | raise ValueError 122 | if image_string is not None: 123 | # very dirty hack, this should be made more robust using regexp 124 | im = image_string_to_PILImage(image_string) 125 | im_h, im_w = im.height, im.width 126 | return round(w / im_w * im_h) 127 | else: 128 | return canvas_height 129 | 130 | 131 | @app.callback(Output('canvas-bg', 'scale'), 132 | [Input('upload-image-bg', 'contents')]) 133 | def update_canvas_upload_scale(image_string): 134 | if image_string is None: 135 | raise ValueError 136 | if image_string is not None: 137 | # very dirty hack, this should be made more robust using regexp 138 | im = image_string_to_PILImage(image_string) 139 | im_h, im_w = im.height, im.width 140 | return canvas_width / im_w 141 | else: 142 | return scale 143 | 144 | 145 | @app.callback(Output('canvas-bg', 'lineWidth'), 146 | [Input('bg-width-slider', 'value')]) 147 | def update_canvas_linewidth(value): 148 | return value 149 | 150 | -------------------------------------------------------------------------------- /app4_measure_length.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | from skimage import io 4 | 5 | import dash 6 | from dash.dependencies import Input, Output, State 7 | import dash_html_components as html 8 | import dash_core_components as dcc 9 | import dash_table 10 | 11 | import dash_canvas 12 | from dash_canvas.utils.io_utils import (image_string_to_PILImage, 13 | array_to_data_url) 14 | from dash_canvas.utils.parse_json import parse_jsonstring_line 15 | 16 | 17 | def title(): 18 | return "Measure lengths" 19 | 20 | def description(): 21 | return "Draw lines on objects to measure their lengths." 22 | 23 | 24 | filename = 'https://upload.wikimedia.org/wikipedia/commons/a/a4/MRI_T2_Brain_axial_image.jpg' 25 | img = io.imread(filename)[..., 0].T 26 | height, width = img.shape 27 | canvas_width = 600 28 | canvas_height = round(height * canvas_width / width) 29 | scale = canvas_width / width 30 | 31 | list_columns = ['length', 'width', 'height'] 32 | columns = [{"name": i, "id": i} for i in list_columns] 33 | 34 | layout = html.Div([ 35 | html.Div([ 36 | dash_canvas.DashCanvas( 37 | id='canvas-line', 38 | width=canvas_width, 39 | height=canvas_height, 40 | scale=scale, 41 | lineWidth=2, 42 | lineColor='red', 43 | tool='line', 44 | image_content=array_to_data_url(img), 45 | goButtonTitle='Measure', 46 | ), 47 | ], className="seven columns"), 48 | html.Div([ 49 | html.H2('Draw lines and measure object lengths'), 50 | html.H4(children="Objects properties"), 51 | html.Div(id='sh_x', hidden=True), 52 | dash_table.DataTable( 53 | id='table-line', 54 | columns=columns, 55 | editable=True, 56 | ) 57 | ], className="four columns"), 58 | ]) 59 | 60 | 61 | def callbacks(app): 62 | 63 | @app.callback(Output('table-line', 'data'), 64 | [Input('canvas-line', 'json_data')]) 65 | def show_string(string): 66 | props = parse_jsonstring_line(string) 67 | df = pd.DataFrame(props, columns=list_columns) 68 | return df.to_dict("records") 69 | -------------------------------------------------------------------------------- /app5_stitching.py: -------------------------------------------------------------------------------- 1 | from glob import glob 2 | import numpy as np 3 | import pandas as pd 4 | from skimage import io 5 | 6 | import dash 7 | from dash.exceptions import PreventUpdate 8 | from dash.dependencies import Input, Output, State 9 | import dash_html_components as html 10 | import dash_core_components as dcc 11 | import dash_table 12 | 13 | import dash_canvas 14 | from dash_canvas.components import image_upload_zone 15 | from dash_canvas.utils.io_utils import (image_string_to_PILImage, 16 | array_to_data_url) 17 | from dash_canvas.utils.registration import register_tiles 18 | from dash_canvas.utils.parse_json import parse_jsonstring_line 19 | 20 | 21 | def title(): 22 | return "Image stitching" 23 | 24 | 25 | def description(): 26 | return """Automatic or semi-supervized stitching of images acquired 27 | with some overlap""" 28 | 29 | 30 | def tile_images(list_of_images, n_rows, n_cols): 31 | dtype = list_of_images[0].dtype 32 | if len(list_of_images) < n_rows * n_cols: 33 | white = np.zeros(list_of_images[0].shape, dtype=dtype) 34 | n_missing = n_rows * n_cols - len(list_of_images) 35 | list_of_images += [white, ] * n_missing 36 | return np.vstack([np.hstack(list_of_images[i_row*n_cols: 37 | i_row*n_cols + n_cols]) 38 | for i_row in range(n_rows)]) 39 | 40 | 41 | def untile_images(image_string, n_rows, n_cols): 42 | big_im = np.asarray(image_string_to_PILImage(image_string)) 43 | tiles = [np.split(im, n_cols, axis=1) for im in np.split(big_im, n_rows)] 44 | return np.array(tiles) 45 | 46 | 47 | def instructions(): 48 | return html.Div(children=[ 49 | html.H5(children='How to use this stitching app'), 50 | dcc.Markdown(""" 51 | - Choose the number of rows and columns of the mosaic, 52 | - Upload images. 53 | - Try automatic stitching by pressing 54 | the "Run stitching" button. 55 | - If automatic stitching did not work, 56 | try adjusting the overlap parameter. 57 | 58 | If shifts between different images are very diifferent, 59 | draw lines to match points of interest in pairs of 60 | images, then press "Estimate translation" to compute an 61 | estimate of the shifts, then press "Run stitching". 62 | """) 63 | ]) 64 | 65 | app = dash.Dash(__name__) 66 | server = app.server 67 | app.config.suppress_callback_exceptions = False 68 | 69 | height, width = 200, 500 70 | canvas_width = 800 71 | canvas_height = round(height * canvas_width / width) 72 | scale = canvas_width / width 73 | 74 | list_columns = ['length', 'width', 'height'] 75 | columns = [{"name": i, "id": i} for i in list_columns] 76 | 77 | 78 | layout = html.Div([ 79 | html.Div([ 80 | dcc.Tabs( 81 | id='stitching-tabs', 82 | value='canvas-tab', 83 | children=[ 84 | dcc.Tab( 85 | label='Image tiles', 86 | value='canvas-tab', 87 | children=[ 88 | dash_canvas.DashCanvas( 89 | id='canvas-stitch', 90 | width=canvas_width, 91 | height=canvas_height, 92 | scale=scale, 93 | lineWidth=2, 94 | lineColor='red', 95 | tool="line", 96 | image_content=array_to_data_url( 97 | np.zeros((width, width), dtype=np.uint8)), 98 | goButtonTitle='Estimate translation', 99 | ), 100 | html.Button('Upload demo data', id='demo'), 101 | image_upload_zone('upload-stitch', multiple=True, 102 | width=45), 103 | html.Div(id='sh_x', hidden=True), 104 | ] 105 | ), 106 | dcc.Tab( 107 | label='Stitched Image', 108 | value='result-tab', 109 | children=[ 110 | html.Img(id='stitching-result', 111 | src=array_to_data_url( 112 | np.zeros((height, width), dtype=np.uint8)), 113 | width=canvas_width) 114 | 115 | ] 116 | ) 117 | ] 118 | ) 119 | ], className="eight columns"), 120 | html.Div([ 121 | html.Label('Number of rows'), 122 | dcc.Input( 123 | id='nrows-stitch', 124 | type='number', 125 | value=2, 126 | name='number of rows', 127 | ), 128 | html.Label('Number of columns'), 129 | dcc.Input( 130 | id='ncolumns-stitch', 131 | type='number', 132 | value=4, 133 | name='number of columns', 134 | ), 135 | html.Label('Fraction of overlap (in [0-1] range)'), 136 | dcc.Input( 137 | id='overlap-stitch', 138 | type='float', 139 | value=0.15, 140 | ), 141 | html.Label('Measured shifts between images'), 142 | dash_table.DataTable( 143 | id='table-stitch', 144 | columns=columns, 145 | editable=True, 146 | ), 147 | html.Br(), 148 | html.Button('Run stitching', id='button-stitch', 149 | style={'color':'red'}), 150 | html.Br(), 151 | instructions() 152 | ], className="three columns"), 153 | ]) 154 | 155 | 156 | def callbacks(app): 157 | 158 | @app.callback(Output('table-stitch', 'data'), 159 | [Input('canvas-stitch', 'json_data')]) 160 | def estimate_translation(string): 161 | props = parse_jsonstring_line(string) 162 | df = pd.DataFrame(props, columns=list_columns) 163 | return df.to_dict("records") 164 | 165 | 166 | @app.callback(Output('sh_x', 'children'), 167 | [Input('upload-stitch', 'contents'), 168 | Input('upload-stitch', 'filename'), 169 | Input('demo', 'n_clicks')], 170 | [State('nrows-stitch', 'value'), 171 | State('ncolumns-stitch', 'value')]) 172 | def upload_content(list_image_string, list_filenames, click, 173 | n_rows, n_cols): 174 | #if list_image_string is None: 175 | # raise PreventUpdate 176 | if list_image_string is not None: 177 | print('update canvas upload') 178 | order = np.argsort(list_filenames) 179 | image_list = [np.asarray(image_string_to_PILImage( 180 | list_image_string[i])) for i in order] 181 | res = tile_images(image_list, n_rows, n_cols) 182 | return array_to_data_url(res) 183 | elif click: 184 | filelist = glob('./assets/tile*.jpg') 185 | filelist.sort() 186 | print(filelist) 187 | image_list = [io.imread(filename) for filename in filelist] 188 | res = tile_images(image_list, n_rows, n_cols) 189 | return array_to_data_url(res) 190 | else: 191 | raise PreventUpdate 192 | #return None 193 | 194 | 195 | @app.callback(Output('stitching-tabs', 'value'), 196 | [Input('button-stitch', 'n_clicks')]) 197 | def change_focus(click): 198 | print('changing focus') 199 | if click: 200 | return 'result-tab' 201 | return 'canvas-tab' 202 | 203 | 204 | @app.callback(Output('stitching-result', 'src'), 205 | [Input('button-stitch', 'n_clicks')], 206 | [State('nrows-stitch', 'value'), 207 | State('ncolumns-stitch', 'value'), 208 | State('overlap-stitch', 'value'), 209 | State('table-stitch', 'data'), 210 | State('sh_x', 'children')]) 211 | def modify_content(n_cl, n_rows, n_cols, overlap, estimate, image_string): 212 | tiles = untile_images(image_string, n_rows, n_cols) 213 | if estimate is not None and len(estimate) > 0: 214 | overlap = [] 215 | for line in estimate: 216 | overlap.append(1.1 * line['length'] / tiles.shape[3]) 217 | canvas = register_tiles(tiles, n_rows, n_cols, 218 | overlaps=overlap, 219 | pad=100) 220 | return array_to_data_url(canvas) 221 | 222 | 223 | @app.callback(Output('canvas-stitch', 'image_content'), 224 | [Input('sh_x', 'children')]) 225 | def update_canvas_image(im): 226 | print('update image content') 227 | return im 228 | 229 | 230 | @app.callback(Output('canvas-stitch', 'height'), 231 | [Input('sh_x', 'children')], 232 | [State('canvas-stitch', 'width'), 233 | State('canvas-stitch', 'height')]) 234 | def update_canvas_upload_shape(image_string, w, h): 235 | if image_string is None: 236 | raise PreventUpdate 237 | if image_string is not None: 238 | im = image_string_to_PILImage(image_string) 239 | im_h, im_w = im.height, im.width 240 | return round(w / im_w * im_h) 241 | else: 242 | return canvas_height 243 | 244 | 245 | @app.callback(Output('canvas-stitch', 'scale'), 246 | [Input('sh_x', 'children')]) 247 | def update_canvas_upload_scale(image_string): 248 | if image_string is None: 249 | raise PreventUpdate 250 | if image_string is not None: 251 | # very dirty hack, this should be made more robust using regexp 252 | im = image_string_to_PILImage(image_string) 253 | im_h, im_w = im.height, im.width 254 | return canvas_width / im_w 255 | else: 256 | return scale 257 | 258 | -------------------------------------------------------------------------------- /app_pics/app1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-canvas/4f4df65b43acb20bae7eb34572ca240dabfbfb72/app_pics/app1.png -------------------------------------------------------------------------------- /app_pics/app2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-canvas/4f4df65b43acb20bae7eb34572ca240dabfbfb72/app_pics/app2.png -------------------------------------------------------------------------------- /app_pics/app3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-canvas/4f4df65b43acb20bae7eb34572ca240dabfbfb72/app_pics/app3.png -------------------------------------------------------------------------------- /app_pics/app4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-canvas/4f4df65b43acb20bae7eb34572ca240dabfbfb72/app_pics/app4.png -------------------------------------------------------------------------------- /app_pics/app5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-canvas/4f4df65b43acb20bae7eb34572ca240dabfbfb72/app_pics/app5.png -------------------------------------------------------------------------------- /assets/bWLwgP.css: -------------------------------------------------------------------------------- 1 | /* Table of contents 2 | –––––––––––––––––––––––––––––––––––––––––––––––––– 3 | - Plotly.js 4 | - Grid 5 | - Base Styles 6 | - Typography 7 | - Links 8 | - Buttons 9 | - Forms 10 | - Lists 11 | - Code 12 | - Tables 13 | - Spacing 14 | - Utilities 15 | - Clearing 16 | - Media Queries 17 | */ 18 | 19 | /* PLotly.js 20 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 21 | /* plotly.js's modebar's z-index is 1001 by default 22 | * https://github.com/plotly/plotly.js/blob/7e4d8ab164258f6bd48be56589dacd9bdd7fded2/src/css/_modebar.scss#L5 23 | * In case a dropdown is above the graph, the dropdown's options 24 | * will be rendered below the modebar 25 | * Increase the select option's z-index 26 | */ 27 | 28 | /* This was actually not quite right - 29 | dropdowns were overlapping each other (edited October 26) 30 | 31 | .Select { 32 | z-index: 1002; 33 | }*/ 34 | 35 | /* Grid 36 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 37 | .container { 38 | position: relative; 39 | width: 100%; 40 | max-width: 960px; 41 | margin: 0 auto; 42 | padding: 0 20px; 43 | box-sizing: border-box; } 44 | .column, 45 | .columns { 46 | width: 100%; 47 | float: left; 48 | box-sizing: border-box; } 49 | 50 | /* For devices larger than 400px */ 51 | @media (min-width: 400px) { 52 | .container { 53 | width: 85%; 54 | padding: 0; } 55 | } 56 | 57 | /* For devices larger than 550px */ 58 | @media (min-width: 550px) { 59 | .container { 60 | width: 80%; } 61 | .column, 62 | .columns { 63 | margin-left: 4%; } 64 | .column:first-child, 65 | .columns:first-child { 66 | margin-left: 0; } 67 | 68 | .one.column, 69 | .one.columns { width: 4.66666666667%; } 70 | .two.columns { width: 13.3333333333%; } 71 | .three.columns { width: 22%; } 72 | .four.columns { width: 30.6666666667%; } 73 | .five.columns { width: 39.3333333333%; } 74 | .six.columns { width: 48%; } 75 | .seven.columns { width: 56.6666666667%; } 76 | .eight.columns { width: 65.3333333333%; } 77 | .nine.columns { width: 74.0%; } 78 | .ten.columns { width: 82.6666666667%; } 79 | .eleven.columns { width: 91.3333333333%; } 80 | .twelve.columns { width: 100%; margin-left: 0; } 81 | 82 | .one-third.column { width: 30.6666666667%; } 83 | .two-thirds.column { width: 65.3333333333%; } 84 | 85 | .one-half.column { width: 48%; } 86 | 87 | /* Offsets */ 88 | .offset-by-one.column, 89 | .offset-by-one.columns { margin-left: 8.66666666667%; } 90 | .offset-by-two.column, 91 | .offset-by-two.columns { margin-left: 17.3333333333%; } 92 | .offset-by-three.column, 93 | .offset-by-three.columns { margin-left: 26%; } 94 | .offset-by-four.column, 95 | .offset-by-four.columns { margin-left: 34.6666666667%; } 96 | .offset-by-five.column, 97 | .offset-by-five.columns { margin-left: 43.3333333333%; } 98 | .offset-by-six.column, 99 | .offset-by-six.columns { margin-left: 52%; } 100 | .offset-by-seven.column, 101 | .offset-by-seven.columns { margin-left: 60.6666666667%; } 102 | .offset-by-eight.column, 103 | .offset-by-eight.columns { margin-left: 69.3333333333%; } 104 | .offset-by-nine.column, 105 | .offset-by-nine.columns { margin-left: 78.0%; } 106 | .offset-by-ten.column, 107 | .offset-by-ten.columns { margin-left: 86.6666666667%; } 108 | .offset-by-eleven.column, 109 | .offset-by-eleven.columns { margin-left: 95.3333333333%; } 110 | 111 | .offset-by-one-third.column, 112 | .offset-by-one-third.columns { margin-left: 34.6666666667%; } 113 | .offset-by-two-thirds.column, 114 | .offset-by-two-thirds.columns { margin-left: 69.3333333333%; } 115 | 116 | .offset-by-one-half.column, 117 | .offset-by-one-half.columns { margin-left: 52%; } 118 | 119 | } 120 | 121 | 122 | /* Base Styles 123 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 124 | /* NOTE 125 | html is set to 62.5% so that all the REM measurements throughout Skeleton 126 | are based on 10px sizing. So basically 1.5rem = 15px :) */ 127 | html { 128 | font-size: 62.5%; } 129 | body { 130 | font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */ 131 | line-height: 1.6; 132 | font-weight: 400; 133 | font-family: "Open Sans", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; 134 | color: rgb(50, 50, 50); } 135 | 136 | 137 | /* Typography 138 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 139 | h1, h2, h3, h4, h5, h6 { 140 | margin-top: 0; 141 | margin-bottom: 0; 142 | font-weight: 300; } 143 | h1 { font-size: 4.5rem; line-height: 1.2; letter-spacing: -.1rem; margin-bottom: 2rem; } 144 | h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; margin-bottom: 1.8rem; margin-top: 1.8rem;} 145 | h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; margin-bottom: 1.5rem; margin-top: 1.5rem;} 146 | h4 { font-size: 2.6rem; line-height: 1.35; letter-spacing: -.08rem; margin-bottom: 1.2rem; margin-top: 1.2rem;} 147 | h5 { font-size: 2.2rem; line-height: 1.5; letter-spacing: -.05rem; margin-bottom: 0.6rem; margin-top: 0.6rem;} 148 | h6 { font-size: 2.0rem; line-height: 1.6; letter-spacing: 0; margin-bottom: 0.75rem; margin-top: 0.75rem;} 149 | 150 | p { 151 | margin-top: 0; } 152 | 153 | 154 | /* Blockquotes 155 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 156 | blockquote { 157 | border-left: 4px lightgrey solid; 158 | padding-left: 1rem; 159 | margin-top: 2rem; 160 | margin-bottom: 2rem; 161 | margin-left: 0rem; 162 | } 163 | 164 | 165 | /* Links 166 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 167 | a { 168 | color: #1EAEDB; 169 | text-decoration: underline; 170 | cursor: pointer;} 171 | a:hover { 172 | color: #0FA0CE; } 173 | 174 | 175 | /* Buttons 176 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 177 | .button, 178 | button, 179 | input[type="submit"], 180 | input[type="reset"], 181 | input[type="button"] { 182 | display: inline-block; 183 | height: 38px; 184 | padding: 0 30px; 185 | color: #555; 186 | text-align: center; 187 | font-size: 11px; 188 | font-weight: 600; 189 | line-height: 38px; 190 | letter-spacing: .1rem; 191 | text-transform: uppercase; 192 | text-decoration: none; 193 | white-space: nowrap; 194 | background-color: transparent; 195 | border-radius: 4px; 196 | border: 1px solid #bbb; 197 | cursor: pointer; 198 | box-sizing: border-box; } 199 | .button:hover, 200 | button:hover, 201 | input[type="submit"]:hover, 202 | input[type="reset"]:hover, 203 | input[type="button"]:hover, 204 | .button:focus, 205 | button:focus, 206 | input[type="submit"]:focus, 207 | input[type="reset"]:focus, 208 | input[type="button"]:focus { 209 | color: #333; 210 | border-color: #888; 211 | outline: 0; } 212 | .button.button-primary, 213 | button.button-primary, 214 | input[type="submit"].button-primary, 215 | input[type="reset"].button-primary, 216 | input[type="button"].button-primary { 217 | color: #FFF; 218 | background-color: #33C3F0; 219 | border-color: #33C3F0; } 220 | .button.button-primary:hover, 221 | button.button-primary:hover, 222 | input[type="submit"].button-primary:hover, 223 | input[type="reset"].button-primary:hover, 224 | input[type="button"].button-primary:hover, 225 | .button.button-primary:focus, 226 | button.button-primary:focus, 227 | input[type="submit"].button-primary:focus, 228 | input[type="reset"].button-primary:focus, 229 | input[type="button"].button-primary:focus { 230 | color: #FFF; 231 | background-color: #1EAEDB; 232 | border-color: #1EAEDB; } 233 | 234 | 235 | /* Forms 236 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 237 | input[type="email"], 238 | input[type="number"], 239 | input[type="search"], 240 | input[type="text"], 241 | input[type="tel"], 242 | input[type="url"], 243 | input[type="password"], 244 | textarea, 245 | select { 246 | height: 38px; 247 | padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ 248 | background-color: #fff; 249 | border: 1px solid #D1D1D1; 250 | border-radius: 4px; 251 | box-shadow: none; 252 | box-sizing: border-box; 253 | font-family: inherit; 254 | font-size: inherit; /*https://stackoverflow.com/questions/6080413/why-doesnt-input-inherit-the-font-from-body*/} 255 | /* Removes awkward default styles on some inputs for iOS */ 256 | input[type="email"], 257 | input[type="number"], 258 | input[type="search"], 259 | input[type="text"], 260 | input[type="tel"], 261 | input[type="url"], 262 | input[type="password"], 263 | textarea { 264 | -webkit-appearance: none; 265 | -moz-appearance: none; 266 | appearance: none; } 267 | textarea { 268 | min-height: 65px; 269 | padding-top: 6px; 270 | padding-bottom: 6px; } 271 | input[type="email"]:focus, 272 | input[type="number"]:focus, 273 | input[type="search"]:focus, 274 | input[type="text"]:focus, 275 | input[type="tel"]:focus, 276 | input[type="url"]:focus, 277 | input[type="password"]:focus, 278 | textarea:focus, 279 | select:focus { 280 | border: 1px solid #33C3F0; 281 | outline: 0; } 282 | label, 283 | legend { 284 | display: block; 285 | margin-bottom: 0px; } 286 | fieldset { 287 | padding: 0; 288 | border-width: 0; } 289 | input[type="checkbox"], 290 | input[type="radio"] { 291 | display: inline; } 292 | label > .label-body { 293 | display: inline-block; 294 | margin-left: .5rem; 295 | font-weight: normal; } 296 | 297 | 298 | /* Lists 299 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 300 | ul { 301 | list-style: circle inside; } 302 | ol { 303 | list-style: decimal inside; } 304 | ol, ul { 305 | padding-left: 0; 306 | margin-top: 0; } 307 | ul ul, 308 | ul ol, 309 | ol ol, 310 | ol ul { 311 | margin: 1.5rem 0 1.5rem 3rem; 312 | font-size: 90%; } 313 | li { 314 | margin-bottom: 1rem; } 315 | 316 | 317 | /* Tables 318 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 319 | table { 320 | border-collapse: collapse; 321 | } 322 | th, 323 | td { 324 | padding: 12px 15px; 325 | text-align: left; 326 | border-bottom: 1px solid #E1E1E1; } 327 | th:first-child, 328 | td:first-child { 329 | padding-left: 0; } 330 | th:last-child, 331 | td:last-child { 332 | padding-right: 0; } 333 | 334 | 335 | /* Spacing 336 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 337 | button, 338 | .button { 339 | margin-bottom: 0rem; } 340 | input, 341 | textarea, 342 | select, 343 | fieldset { 344 | margin-bottom: 0rem; } 345 | pre, 346 | dl, 347 | figure, 348 | table, 349 | form { 350 | margin-bottom: 0rem; } 351 | p, 352 | ul, 353 | ol { 354 | margin-bottom: 0.75rem; } 355 | 356 | /* Utilities 357 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 358 | .u-full-width { 359 | width: 100%; 360 | box-sizing: border-box; } 361 | .u-max-full-width { 362 | max-width: 100%; 363 | box-sizing: border-box; } 364 | .u-pull-right { 365 | float: right; } 366 | .u-pull-left { 367 | float: left; } 368 | 369 | 370 | /* Misc 371 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 372 | hr { 373 | margin-top: 3rem; 374 | margin-bottom: 3.5rem; 375 | border-width: 0; 376 | border-top: 1px solid #E1E1E1; } 377 | 378 | 379 | /* Clearing 380 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 381 | 382 | /* Self Clearing Goodness */ 383 | .container:after, 384 | .row:after, 385 | .u-cf { 386 | content: ""; 387 | display: table; 388 | clear: both; } 389 | 390 | 391 | /* Media Queries 392 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 393 | /* 394 | Note: The best way to structure the use of media queries is to create the queries 395 | near the relevant code. For example, if you wanted to change the styles for buttons 396 | on small devices, paste the mobile query code up in the buttons section and style it 397 | there. 398 | */ 399 | 400 | 401 | /* Larger than mobile */ 402 | @media (min-width: 400px) {} 403 | 404 | /* Larger than phablet (also point when grid becomes active) */ 405 | @media (min-width: 550px) {} 406 | 407 | /* Larger than tablet */ 408 | @media (min-width: 750px) {} 409 | 410 | /* Larger than desktop */ 411 | @media (min-width: 1000px) {} 412 | 413 | /* Larger than Desktop HD */ 414 | @media (min-width: 1200px) {} -------------------------------------------------------------------------------- /assets/ballet_dancer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-canvas/4f4df65b43acb20bae7eb34572ca240dabfbfb72/assets/ballet_dancer.jpg -------------------------------------------------------------------------------- /assets/base.css: -------------------------------------------------------------------------------- 1 | /* Table of contents 2 | –––––––––––––––––––––––––––––––––––––––––––––––––– 3 | - Plotly.js 4 | - Grid 5 | - Base Styles 6 | - Typography 7 | - Links 8 | - Buttons 9 | - Forms 10 | - Lists 11 | - Code 12 | - Tables 13 | - Spacing 14 | - Utilities 15 | - Clearing 16 | - Media Queries 17 | */ 18 | 19 | /* PLotly.js 20 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 21 | /* plotly.js's modebar's z-index is 1001 by default 22 | * https://github.com/plotly/plotly.js/blob/7e4d8ab164258f6bd48be56589dacd9bdd7fded2/src/css/_modebar.scss#L5 23 | * In case a dropdown is above the graph, the dropdown's options 24 | * will be rendered below the modebar 25 | * Increase the select option's z-index 26 | */ 27 | 28 | /* This was actually not quite right - 29 | dropdowns were overlapping each other (edited October 26) 30 | 31 | .Select { 32 | z-index: 1002; 33 | }*/ 34 | 35 | /* Remove Undo Button 36 | --------------------------------------------------*/ 37 | ._dash-undo-redo { 38 | display: none; 39 | } 40 | 41 | /* Grid 42 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 43 | .container { 44 | position: relative; 45 | width: 100%; 46 | max-width: 960px; 47 | margin: 0 auto; 48 | padding: 0 20px; 49 | box-sizing: border-box; } 50 | .column, 51 | .columns { 52 | width: 100%; 53 | float: left; 54 | box-sizing: border-box; } 55 | 56 | /* For devices larger than 400px */ 57 | @media (min-width: 400px) { 58 | .container { 59 | width: 85%; 60 | padding: 0; } 61 | } 62 | 63 | /* For devices larger than 550px */ 64 | @media (min-width: 550px) { 65 | .container { 66 | width: 80%; } 67 | .column, 68 | .columns { 69 | margin-left: 4%; } 70 | .column:first-child, 71 | .columns:first-child { 72 | margin-left: 0; } 73 | 74 | .one.column, 75 | .one.columns { width: 4.66666666667%; } 76 | .two.columns { width: 13.3333333333%; } 77 | .three.columns { width: 22%; } 78 | .four.columns { width: 30.6666666667%; } 79 | .five.columns { width: 39.3333333333%; } 80 | .six.columns { width: 48%; } 81 | .seven.columns { width: 56.6666666667%; } 82 | .eight.columns { width: 65.3333333333%; } 83 | .nine.columns { width: 74.0%; } 84 | .ten.columns { width: 82.6666666667%; } 85 | .eleven.columns { width: 91.3333333333%; } 86 | .twelve.columns { width: 100%; margin-left: 0; } 87 | 88 | .one-third.column { width: 30.6666666667%; } 89 | .two-thirds.column { width: 65.3333333333%; } 90 | 91 | .one-half.column { width: 48%; } 92 | 93 | /* Offsets */ 94 | .offset-by-one.column, 95 | .offset-by-one.columns { margin-left: 8.66666666667%; } 96 | .offset-by-two.column, 97 | .offset-by-two.columns { margin-left: 17.3333333333%; } 98 | .offset-by-three.column, 99 | .offset-by-three.columns { margin-left: 26%; } 100 | .offset-by-four.column, 101 | .offset-by-four.columns { margin-left: 34.6666666667%; } 102 | .offset-by-five.column, 103 | .offset-by-five.columns { margin-left: 43.3333333333%; } 104 | .offset-by-six.column, 105 | .offset-by-six.columns { margin-left: 52%; } 106 | .offset-by-seven.column, 107 | .offset-by-seven.columns { margin-left: 60.6666666667%; } 108 | .offset-by-eight.column, 109 | .offset-by-eight.columns { margin-left: 69.3333333333%; } 110 | .offset-by-nine.column, 111 | .offset-by-nine.columns { margin-left: 78.0%; } 112 | .offset-by-ten.column, 113 | .offset-by-ten.columns { margin-left: 86.6666666667%; } 114 | .offset-by-eleven.column, 115 | .offset-by-eleven.columns { margin-left: 95.3333333333%; } 116 | 117 | .offset-by-one-third.column, 118 | .offset-by-one-third.columns { margin-left: 34.6666666667%; } 119 | .offset-by-two-thirds.column, 120 | .offset-by-two-thirds.columns { margin-left: 69.3333333333%; } 121 | 122 | .offset-by-one-half.column, 123 | .offset-by-one-half.columns { margin-left: 52%; } 124 | 125 | } 126 | 127 | 128 | /* Base Styles 129 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 130 | /* NOTE 131 | html is set to 62.5% so that all the REM measurements throughout Skeleton 132 | are based on 10px sizing. So basically 1.5rem = 15px :) */ 133 | html { 134 | font-size: 62.5%; } 135 | body { 136 | font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */ 137 | line-height: 1.6; 138 | font-weight: 400; 139 | font-family: "Open Sans", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; 140 | color: rgb(50, 50, 50); 141 | margin: 0!important; 142 | } 143 | 144 | 145 | /* Typography 146 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 147 | h1, h2, h3, h4, h5, h6 { 148 | margin-top: 0; 149 | margin-bottom: 0; 150 | font-weight: 300; } 151 | h1 { font-size: 4.5rem; line-height: 1.2; letter-spacing: -.1rem; margin-bottom: 2rem; } 152 | h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; margin-bottom: 1.8rem; margin-top: 1.8rem;} 153 | h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; margin-bottom: 1.5rem; margin-top: 1.5rem;} 154 | h4 { font-size: 2.6rem; line-height: 1.35; letter-spacing: -.08rem; margin-bottom: 1.2rem; margin-top: 1.2rem;} 155 | h5 { font-size: 2.2rem; line-height: 1.5; letter-spacing: -.05rem; margin-bottom: 0.6rem; margin-top: 0.6rem;} 156 | h6 { font-size: 2.0rem; line-height: 1.6; letter-spacing: 0; margin-bottom: 0.75rem; margin-top: 0.75rem;} 157 | 158 | p { 159 | margin-top: 0; } 160 | 161 | 162 | /* Blockquotes 163 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 164 | blockquote { 165 | border-left: 4px lightgrey solid; 166 | padding-left: 1rem; 167 | margin-top: 2rem; 168 | margin-bottom: 2rem; 169 | margin-left: 0rem; 170 | } 171 | 172 | 173 | /* Links 174 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 175 | a { 176 | color: #1EAEDB; 177 | text-decoration: underline; 178 | cursor: pointer;} 179 | a:hover { 180 | color: #0FA0CE; } 181 | 182 | 183 | /* Buttons 184 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 185 | .button, 186 | button, 187 | input[type="submit"], 188 | input[type="reset"], 189 | input[type="button"] { 190 | display: inline-block; 191 | height: 38px; 192 | padding: 0 30px; 193 | color: #555; 194 | text-align: center; 195 | font-size: 11px; 196 | font-weight: 600; 197 | line-height: 38px; 198 | letter-spacing: .1rem; 199 | text-transform: uppercase; 200 | text-decoration: none; 201 | white-space: nowrap; 202 | background-color: transparent; 203 | border-radius: 4px; 204 | border: 1px solid #bbb; 205 | cursor: pointer; 206 | box-sizing: border-box; } 207 | .button:hover, 208 | button:hover, 209 | input[type="submit"]:hover, 210 | input[type="reset"]:hover, 211 | input[type="button"]:hover, 212 | .button:focus, 213 | button:focus, 214 | input[type="submit"]:focus, 215 | input[type="reset"]:focus, 216 | input[type="button"]:focus { 217 | color: #333; 218 | border-color: #888; 219 | outline: 0; } 220 | .button.button-primary, 221 | button.button-primary, 222 | input[type="submit"].button-primary, 223 | input[type="reset"].button-primary, 224 | input[type="button"].button-primary { 225 | color: #FFF; 226 | background-color: #33C3F0; 227 | border-color: #33C3F0; } 228 | .button.button-primary:hover, 229 | button.button-primary:hover, 230 | input[type="submit"].button-primary:hover, 231 | input[type="reset"].button-primary:hover, 232 | input[type="button"].button-primary:hover, 233 | .button.button-primary:focus, 234 | button.button-primary:focus, 235 | input[type="submit"].button-primary:focus, 236 | input[type="reset"].button-primary:focus, 237 | input[type="button"].button-primary:focus { 238 | color: #FFF; 239 | background-color: #1EAEDB; 240 | border-color: #1EAEDB; } 241 | 242 | 243 | /* Forms 244 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 245 | input[type="email"], 246 | input[type="number"], 247 | input[type="search"], 248 | input[type="text"], 249 | input[type="tel"], 250 | input[type="url"], 251 | input[type="password"], 252 | textarea, 253 | select { 254 | height: 38px; 255 | padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ 256 | background-color: #fff; 257 | border: 1px solid #D1D1D1; 258 | border-radius: 4px; 259 | box-shadow: none; 260 | box-sizing: border-box; 261 | font-family: inherit; 262 | font-size: inherit; /*https://stackoverflow.com/questions/6080413/why-doesnt-input-inherit-the-font-from-body*/} 263 | /* Removes awkward default styles on some inputs for iOS */ 264 | input[type="email"], 265 | input[type="number"], 266 | input[type="search"], 267 | input[type="text"], 268 | input[type="tel"], 269 | input[type="url"], 270 | input[type="password"], 271 | textarea { 272 | -webkit-appearance: none; 273 | -moz-appearance: none; 274 | appearance: none; } 275 | textarea { 276 | min-height: 65px; 277 | padding-top: 6px; 278 | padding-bottom: 6px; } 279 | input[type="email"]:focus, 280 | input[type="number"]:focus, 281 | input[type="search"]:focus, 282 | input[type="text"]:focus, 283 | input[type="tel"]:focus, 284 | input[type="url"]:focus, 285 | input[type="password"]:focus, 286 | textarea:focus, 287 | select:focus { 288 | border: 1px solid #33C3F0; 289 | outline: 0; } 290 | label, 291 | legend { 292 | display: block; 293 | margin-bottom: 0px; } 294 | fieldset { 295 | padding: 0; 296 | border-width: 0; } 297 | input[type="checkbox"], 298 | input[type="radio"] { 299 | display: inline; } 300 | label > .label-body { 301 | display: inline-block; 302 | margin-left: .5rem; 303 | font-weight: normal; } 304 | 305 | 306 | /* Lists 307 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 308 | ul { 309 | list-style: circle inside; } 310 | ol { 311 | list-style: decimal inside; } 312 | ol, ul { 313 | padding-left: 0; 314 | margin-top: 0; } 315 | ul ul, 316 | ul ol, 317 | ol ol, 318 | ol ul { 319 | margin: 1.5rem 0 1.5rem 3rem; 320 | font-size: 90%; } 321 | li { 322 | margin-bottom: 1rem; } 323 | 324 | 325 | /* Tables 326 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 327 | table { 328 | border-collapse: collapse; 329 | } 330 | th, 331 | td { 332 | padding: 12px 15px; 333 | text-align: left; 334 | border-bottom: 1px solid #E1E1E1; } 335 | th:first-child, 336 | td:first-child { 337 | padding-left: 0; } 338 | th:last-child, 339 | td:last-child { 340 | padding-right: 0; } 341 | 342 | 343 | /* Spacing 344 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 345 | button, 346 | .button { 347 | margin-bottom: 0rem; } 348 | input, 349 | textarea, 350 | select, 351 | fieldset { 352 | margin-bottom: 0rem; } 353 | pre, 354 | dl, 355 | figure, 356 | table, 357 | form { 358 | margin-bottom: 0rem; } 359 | p, 360 | ul, 361 | ol { 362 | margin-bottom: 0.75rem; } 363 | 364 | /* Utilities 365 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 366 | .u-full-width { 367 | width: 100%; 368 | box-sizing: border-box; } 369 | .u-max-full-width { 370 | max-width: 100%; 371 | box-sizing: border-box; } 372 | .u-pull-right { 373 | float: right; } 374 | .u-pull-left { 375 | float: left; } 376 | 377 | 378 | /* Misc 379 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 380 | hr { 381 | margin-top: 3rem; 382 | margin-bottom: 3.5rem; 383 | border-width: 0; 384 | border-top: 1px solid #E1E1E1; } 385 | 386 | 387 | /* Clearing 388 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 389 | 390 | /* Self Clearing Goodness */ 391 | .container:after, 392 | .row:after, 393 | .u-cf { 394 | content: ""; 395 | display: table; 396 | clear: both; } 397 | 398 | 399 | /* Media Queries 400 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 401 | /* 402 | Note: The best way to structure the use of media queries is to create the queries 403 | near the relevant code. For example, if you wanted to change the styles for buttons 404 | on small devices, paste the mobile query code up in the buttons section and style it 405 | there. 406 | */ 407 | 408 | 409 | /* Larger than mobile */ 410 | @media (min-width: 400px) {} 411 | 412 | /* Larger than phablet (also point when grid becomes active) */ 413 | @media (min-width: 550px) {} 414 | 415 | /* Larger than tablet */ 416 | @media (min-width: 750px) {} 417 | 418 | /* Larger than desktop */ 419 | @media (min-width: 1000px) {} 420 | 421 | /* Larger than Desktop HD */ 422 | @media (min-width: 1200px) {} -------------------------------------------------------------------------------- /assets/dancer_boy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-canvas/4f4df65b43acb20bae7eb34572ca240dabfbfb72/assets/dancer_boy.jpg -------------------------------------------------------------------------------- /assets/dress.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-canvas/4f4df65b43acb20bae7eb34572ca240dabfbfb72/assets/dress.jpg -------------------------------------------------------------------------------- /assets/gallery-style.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Open+Sans'); 2 | @import url('https://fonts.googleapis.com/css?family=Dosis'); 3 | 4 | #index-waitfor { 5 | background-color: #DFE8F3; 6 | position: absolute; 7 | width: 100%; 8 | left: 0px; 9 | top: 0px; 10 | } 11 | 12 | #gallery-title { 13 | text-align: center; 14 | font-size: 36pt; 15 | font-family: 'Dosis'; 16 | } 17 | #gallery-subtitle { 18 | text-align: center; 19 | font-size: 18pt; 20 | font-family: 'Open Sans' 21 | } 22 | 23 | #gallery-apps{ 24 | position: static; 25 | margin: 0 auto; 26 | padding: 20px; 27 | text-align: center; 28 | width:calc(100% - 40px); ; 29 | height: auto; 30 | min-height: 100vh; 31 | } 32 | 33 | .gallery-app { 34 | position: relative; 35 | display: inline-block; 36 | height: 300px; 37 | width: 25%; 38 | min-width: 275px; 39 | padding: 5px; 40 | margin-right: 20px; 41 | margin-bottom:10px; 42 | vertical-align: top; 43 | text-align: left; 44 | overflow: hidden; 45 | border-radius: 15px; 46 | } 47 | .gallery-app-img { 48 | transition-duration:500ms; 49 | object-fit: cover; 50 | width: 100%; 51 | height: 100%; 52 | transform: scale(1.1); 53 | border-radius: 15px; 54 | } 55 | .gallery-app-info { 56 | height: calc(100% - 20px); 57 | border-radius: 10px; 58 | padding: 10px; 59 | position: absolute; 60 | top: 10px; 61 | left: 10px; 62 | opacity: 0; 63 | } 64 | .gallery-app-name { 65 | color: white; 66 | font-family: 'Dosis'; 67 | font-size: 24pt; 68 | line-height: 28pt; 69 | font-weight: 100 !important; 70 | } 71 | .gallery-app-desc { 72 | max-height: 160px; 73 | width: calc(100% - 30px); 74 | overflow-y: auto; 75 | color: white; 76 | font-family: 'Open Sans'; 77 | font-size: 11pt; 78 | margin: 15px; 79 | margin-top: 25px; 80 | } 81 | .gallery-app ::-webkit-scrollbar { 82 | display: none; 83 | } 84 | 85 | .gallery-app:hover .gallery-app-info, .gallery-app:hover .gallery-app-link { 86 | opacity: 1; 87 | } 88 | .gallery-app:hover .gallery-app-img { 89 | -webkit-filter: blur(5px) grayscale(0.5) brightness(30%); 90 | }} 91 | 92 | -------------------------------------------------------------------------------- /assets/general-app-page.css: -------------------------------------------------------------------------------- 1 | #app-page-content { 2 | margin: 0px; 3 | margin-top: -1px; /* hide border between header and content */ 4 | width: 100%; 5 | height: auto; 6 | min-height: calc(100vh - 100px); 7 | position: absolute; 8 | top: 100px; 9 | } 10 | 11 | #app-page-header { 12 | width: 100%; 13 | height: 100px !important; 14 | font-family: 'Dosis'; 15 | position: absolute; 16 | top: 0px; 17 | } 18 | 19 | #app-page-header > a > img { 20 | height: 70px; 21 | float: left; 22 | padding: 10px; 23 | padding-bottom: 0px; 24 | transition-duration:500ms; 25 | } 26 | 27 | #app-page-header > a > img:hover { 28 | filter:brightness(2.00); 29 | } 30 | 31 | #app-page-header h2 { 32 | font-size: 34pt; 33 | display: inline-block; 34 | } 35 | 36 | #app-page-header #gh-link > img { 37 | height: 50px; 38 | float:right; 39 | margin-top: 15px; 40 | margin-right: 10px; 41 | transition-duration:500ms; 42 | } 43 | 44 | #app-page-header #gh-link > img:hover { 45 | -webkit-filter:invert(100%); 46 | } 47 | -------------------------------------------------------------------------------- /assets/tile_0_0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-canvas/4f4df65b43acb20bae7eb34572ca240dabfbfb72/assets/tile_0_0.jpg -------------------------------------------------------------------------------- /assets/tile_0_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-canvas/4f4df65b43acb20bae7eb34572ca240dabfbfb72/assets/tile_0_1.jpg -------------------------------------------------------------------------------- /assets/tile_0_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-canvas/4f4df65b43acb20bae7eb34572ca240dabfbfb72/assets/tile_0_2.jpg -------------------------------------------------------------------------------- /assets/tile_0_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-canvas/4f4df65b43acb20bae7eb34572ca240dabfbfb72/assets/tile_0_3.jpg -------------------------------------------------------------------------------- /assets/tile_1_0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-canvas/4f4df65b43acb20bae7eb34572ca240dabfbfb72/assets/tile_1_0.jpg -------------------------------------------------------------------------------- /assets/tile_1_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-canvas/4f4df65b43acb20bae7eb34572ca240dabfbfb72/assets/tile_1_1.jpg -------------------------------------------------------------------------------- /assets/tile_1_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-canvas/4f4df65b43acb20bae7eb34572ca240dabfbfb72/assets/tile_1_2.jpg -------------------------------------------------------------------------------- /assets/tile_1_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-canvas/4f4df65b43acb20bae7eb34572ca240dabfbfb72/assets/tile_1_3.jpg -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/preset-env', 4 | '@babel/react' 5 | ], 6 | plugins: [ 7 | '@babel/plugin-syntax-dynamic-import' 8 | ] 9 | }; 10 | -------------------------------------------------------------------------------- /dash_canvas/AlternativeCanvas.py: -------------------------------------------------------------------------------- 1 | # AUTO GENERATED FILE - DO NOT EDIT 2 | 3 | from dash.development.base_component import Component, _explicitize_args 4 | 5 | 6 | class AlternativeCanvas(Component): 7 | """A AlternativeCanvas component. 8 | AlternativeCanvas is an example component. 9 | It takes a property, `label`, and 10 | displays it. 11 | It renders an input with the property `value` 12 | which is editable by the user. 13 | 14 | Keyword arguments: 15 | - id (string; optional): The ID used to identify this component in Dash callbacks 16 | - label (string; required): A label that will be printed when this component is rendered. 17 | - value (string; optional): The value displayed in the input 18 | - lineColor (string; optional): the color of the line 19 | 20 | Available events: """ 21 | @_explicitize_args 22 | def __init__(self, id=Component.UNDEFINED, label=Component.REQUIRED, value=Component.UNDEFINED, lineColor=Component.UNDEFINED, **kwargs): 23 | self._prop_names = ['id', 'label', 'value', 'lineColor'] 24 | self._type = 'AlternativeCanvas' 25 | self._namespace = 'dash_canvas' 26 | self._valid_wildcard_attributes = [] 27 | self.available_events = [] 28 | self.available_properties = ['id', 'label', 'value', 'lineColor'] 29 | self.available_wildcard_properties = [] 30 | 31 | _explicit_args = kwargs.pop('_explicit_args') 32 | _locals = locals() 33 | _locals.update(kwargs) # For wildcard attrs 34 | args = {k: _locals[k] for k in _explicit_args if k != 'children'} 35 | 36 | for k in ['label']: 37 | if k not in args: 38 | raise TypeError( 39 | 'Required argument `' + k + '` was not specified.') 40 | super(AlternativeCanvas, self).__init__(**args) 41 | 42 | def __repr__(self): 43 | if(any(getattr(self, c, None) is not None 44 | for c in self._prop_names 45 | if c is not self._prop_names[0]) 46 | or any(getattr(self, c, None) is not None 47 | for c in self.__dict__.keys() 48 | if any(c.startswith(wc_attr) 49 | for wc_attr in self._valid_wildcard_attributes))): 50 | props_string = ', '.join([c+'='+repr(getattr(self, c, None)) 51 | for c in self._prop_names 52 | if getattr(self, c, None) is not None]) 53 | wilds_string = ', '.join([c+'='+repr(getattr(self, c, None)) 54 | for c in self.__dict__.keys() 55 | if any([c.startswith(wc_attr) 56 | for wc_attr in 57 | self._valid_wildcard_attributes])]) 58 | return ('AlternativeCanvas(' + props_string + 59 | (', ' + wilds_string if wilds_string != '' else '') + ')') 60 | else: 61 | return ( 62 | 'AlternativeCanvas(' + 63 | repr(getattr(self, self._prop_names[0], None)) + ')') 64 | -------------------------------------------------------------------------------- /dash_canvas/DashCanvas.py: -------------------------------------------------------------------------------- 1 | # AUTO GENERATED FILE - DO NOT EDIT 2 | 3 | from dash.development.base_component import Component, _explicitize_args 4 | 5 | 6 | class DashCanvas(Component): 7 | """A DashCanvas component. 8 | Canvas component for drawing on a background image and selecting 9 | regions. 10 | 11 | Keyword arguments: 12 | - id (string; optional): The ID used to identify this component in Dash callbacks 13 | - image_content (string; default ''): Image data string, formatted as png or jpg data string. Can be 14 | generated by utils.io_utils.array_to_data_string. 15 | - zoom (number; default 1): Zoom factor 16 | - width (number; default 500): Width of the canvas 17 | - height (number; default 500): Height of the canvas 18 | - scale (number; default 1): Scaling ratio between canvas width and image width 19 | - tool (string; default "pencil"): Selection of drawing tool, among ["pencil", "pan", "circle", 20 | "rectangle", "select", "line"]. 21 | - lineWidth (number; default 10): Width of drawing line (in pencil mode) 22 | - lineColor (string; default 'red'): Color of drawing line (in pencil mode). Can be a text string, 23 | like 'yellow', 'red', or a color triplet like 'rgb(255, 0, 0)'. 24 | Alpha is possible with 'rgba(255, 0, 0, 0.5)'. 25 | - goButtonTitle (string; default 'Save'): Title of button 26 | - filename (string; default ''): Name of image file to load (URL string) 27 | - trigger (number; default 0): Counter of how many times the save button was pressed 28 | (to be used mostly as input) 29 | - json_data (string; default ''): Sketch content as JSON string, containing background image and 30 | annotations. Use utils.parse_json.parse_jsonstring to parse 31 | this string. 32 | - hide_buttons (list of strings; optional): Names of buttons to hide. Names are "zoom", "pan", "line", "pencil", 33 | "rectangle", "undo", "select".""" 34 | @_explicitize_args 35 | def __init__(self, id=Component.UNDEFINED, image_content=Component.UNDEFINED, zoom=Component.UNDEFINED, width=Component.UNDEFINED, height=Component.UNDEFINED, scale=Component.UNDEFINED, tool=Component.UNDEFINED, lineWidth=Component.UNDEFINED, lineColor=Component.UNDEFINED, goButtonTitle=Component.UNDEFINED, filename=Component.UNDEFINED, trigger=Component.UNDEFINED, json_data=Component.UNDEFINED, hide_buttons=Component.UNDEFINED, **kwargs): 36 | self._prop_names = ['id', 'image_content', 'zoom', 'width', 'height', 'scale', 'tool', 'lineWidth', 'lineColor', 'goButtonTitle', 'filename', 'trigger', 'json_data', 'hide_buttons'] 37 | self._type = 'DashCanvas' 38 | self._namespace = 'dash_canvas' 39 | self._valid_wildcard_attributes = [] 40 | self.available_properties = ['id', 'image_content', 'zoom', 'width', 'height', 'scale', 'tool', 'lineWidth', 'lineColor', 'goButtonTitle', 'filename', 'trigger', 'json_data', 'hide_buttons'] 41 | self.available_wildcard_properties = [] 42 | 43 | _explicit_args = kwargs.pop('_explicit_args') 44 | _locals = locals() 45 | _locals.update(kwargs) # For wildcard attrs 46 | args = {k: _locals[k] for k in _explicit_args if k != 'children'} 47 | 48 | for k in []: 49 | if k not in args: 50 | raise TypeError( 51 | 'Required argument `' + k + '` was not specified.') 52 | super(DashCanvas, self).__init__(**args) 53 | -------------------------------------------------------------------------------- /dash_canvas/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function as _ 2 | 3 | import os as _os 4 | import sys as _sys 5 | import json 6 | 7 | import dash as _dash 8 | 9 | # noinspection PyUnresolvedReferences 10 | from ._imports_ import * 11 | from ._imports_ import __all__ 12 | 13 | if not hasattr(_dash, 'development'): 14 | print('Dash was not successfully imported. ' 15 | 'Make sure you don\'t have a file ' 16 | 'named \n"dash.py" in your current directory.', file=_sys.stderr) 17 | _sys.exit(1) 18 | 19 | _basepath = _os.path.dirname(__file__) 20 | _filepath = _os.path.abspath(_os.path.join(_basepath, 'package.json')) 21 | with open(_filepath) as f: 22 | package = json.load(f) 23 | 24 | package_name = package['name'].replace(' ', '_').replace('-', '_') 25 | __version__ = package['version'] 26 | 27 | _current_path = _os.path.dirname(_os.path.abspath(__file__)) 28 | 29 | _this_module = _sys.modules[__name__] 30 | 31 | async_resources = [ 32 | 'canvas' 33 | ] 34 | 35 | _js_dist = [] 36 | 37 | _js_dist.extend([{ 38 | 'relative_package_path': 'async-{}.js'.format(async_resource), 39 | 'dev_package_path': 'async-{}.dev.js'.format(async_resource), 40 | 'external_url': ( 41 | 'https://unpkg.com/dash-canvas@{}' 42 | '/dash_canvas/async-{}.js' 43 | ).format(__version__, async_resource), 44 | 'namespace': 'dash_canvas', 45 | 'async': True 46 | } for async_resource in async_resources]) 47 | 48 | _js_dist.extend([ 49 | { 50 | 'relative_package_path': 'dash_canvas.min.js', 51 | 'dev_package_path': 'dash_canvas.dev.js', 52 | 'namespace': package_name 53 | } 54 | ]) 55 | 56 | _css_dist = [] 57 | 58 | 59 | for _component in __all__: 60 | setattr(locals()[_component], '_js_dist', _js_dist) 61 | setattr(locals()[_component], '_css_dist', _css_dist) 62 | -------------------------------------------------------------------------------- /dash_canvas/_imports_.py: -------------------------------------------------------------------------------- 1 | from .DashCanvas import DashCanvas 2 | 3 | __all__ = [ 4 | "DashCanvas" 5 | ] -------------------------------------------------------------------------------- /dash_canvas/components/__init__.py: -------------------------------------------------------------------------------- 1 | from ._components import image_upload_zone 2 | 3 | __all__ = ['image_upload_zone'] 4 | -------------------------------------------------------------------------------- /dash_canvas/components/_components.py: -------------------------------------------------------------------------------- 1 | import dash_core_components as dcc 2 | import dash_html_components as html 3 | 4 | 5 | def image_upload_zone(name, multiple=False, width=100): 6 | return dcc.Upload( 7 | id=name, 8 | children=[ 9 | 'Drag and Drop or ', 10 | html.A('Select an Image')], 11 | style={'width': str(width) + '%', 12 | 'height': '50px', 13 | 'lineHeight': '50px', 14 | 'borderWidth': '1px', 15 | 'borderStyle': 'dashed', 16 | 'borderRadius': '5px', 17 | 'textAlign': 'center' 18 | }, 19 | accept='image/*', 20 | multiple=multiple, 21 | ) 22 | 23 | -------------------------------------------------------------------------------- /dash_canvas/dash_canvas.min.js: -------------------------------------------------------------------------------- 1 | window.dash_canvas=function(e){function t(t){for(var n,o,i=t[0],a=t[1],u=0,c=[];u dash_canvas/metadata.json && copyfiles package.json dash_canvas && python -c \"import dash; dash.development.component_loader.generate_classes('dash_canvas', 'dash_canvas/metadata.json')\"", 20 | "build": "npm run build:js && npm run build:js-dev && npm run build:py && npm run validate-init", 21 | "postbuild": "es-check es5 dash_canvas/*.js" 22 | }, 23 | "author": "Emmanuelle Gouillart ", 24 | "license": "MIT", 25 | "dependencies": { 26 | "material-ui": "^0.20.0", 27 | "plotly-icons": ">=1.0", 28 | "react": "16.13.0", 29 | "react-color": "^2.18.0", 30 | "react-dom": "16.13.0", 31 | "react-sketch": ">=0.4.4" 32 | }, 33 | "devDependencies": { 34 | "@babel/cli": "^7.6.2", 35 | "@babel/core": "^7.6.2", 36 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 37 | "@babel/preset-env": "^7.6.2", 38 | "@babel/preset-react": "^7.0.0", 39 | "@plotly/webpack-dash-dynamic-import": "^1.1.4", 40 | "babel-eslint": "^8.2.3", 41 | "babel-loader": "^8.0.6", 42 | "babel-preset-env": "^1.7.0", 43 | "babel-preset-react": "^6.24.1", 44 | "copyfiles": "^2.0.0", 45 | "css-loader": "^0.28.11", 46 | "es-check": "^5.0.0", 47 | "eslint": "^4.19.1", 48 | "eslint-config-prettier": "^2.9.0", 49 | "eslint-plugin-import": "^2.12.0", 50 | "eslint-plugin-react": "^7.9.1", 51 | "flexboxgrid": "^6.3.1", 52 | "npm": "^6.1.0", 53 | "react-docgen": "^4.1.1", 54 | "react-dropzone": "4.2.7", 55 | "style-loader": "^0.21.0", 56 | "webpack": "^4.41.0", 57 | "webpack-cli": "^3.3.9", 58 | "webpack-serve": "^1.0.2" 59 | }, 60 | "peerDependencies": { 61 | "react": ">=0.14", 62 | "react-dom": ">=0.14" 63 | }, 64 | "engines": { 65 | "node": ">=8.11.0", 66 | "npm": ">=6.1.0" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /dash_canvas/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-canvas/4f4df65b43acb20bae7eb34572ca240dabfbfb72/dash_canvas/test/__init__.py -------------------------------------------------------------------------------- /dash_canvas/test/test_image_processing_utils.py: -------------------------------------------------------------------------------- 1 | from dash_canvas.utils import watershed_segmentation, modify_segmentation 2 | from skimage import data, segmentation, morphology, measure 3 | import numpy as np 4 | from scipy import ndimage 5 | 6 | 7 | def test_watershed_segmentation(): 8 | img = np.zeros((20, 20)) 9 | img[2:6, 2:6] = 1 10 | img[10:15, 10:15] = 1 11 | mask = np.zeros_like(img, dtype=np.uint8) 12 | mask[4, 4] = 1 13 | mask[12, 12] = 2 14 | res = watershed_segmentation(img, mask, sigma=0.1) 15 | assert np.all(res[2:6, 2:6] == 1) 16 | assert np.all(res[10:15, 10:15] == 2) 17 | 18 | 19 | def test_split_segmentation(): 20 | img = np.zeros((100, 100), dtype=np.uint8) 21 | img[:40, 55:] = 1 22 | img[40:, :30] = 2 23 | img[40:, 30:65] = 3 24 | img[40:, 65:] = 4 25 | img = ndimage.rotate(img, 20) 26 | img = measure.label(img) 27 | img = morphology.remove_small_objects(img, 20) 28 | img = segmentation.relabel_sequential(img)[0] 29 | 30 | mask = np.zeros_like(img) 31 | mask[2:53, 75] = 1 32 | mask[100, 17:60] = 1 33 | 34 | # Labels start at 1 35 | seg = modify_segmentation(img, measure.label(mask), mode='split') 36 | assert len(np.unique(seg)) == len(np.unique(img)) + 2 37 | 38 | # Labels start at 0 39 | seg = modify_segmentation(img + 1, measure.label(mask)) 40 | assert len(np.unique(seg)) == len(np.unique(img)) + 2 41 | 42 | 43 | def test_merge_segmentation(): 44 | img = np.zeros((20, 20), dtype=np.uint8) 45 | img[:10, :10] = 1 46 | img[10:, :10] = 2 47 | mask = np.zeros_like(img) 48 | mask[:, 5] = 1 49 | seg = modify_segmentation(img, mask, mode='merge') 50 | assert np.all(np.unique(seg) == np.array([0, 1])) 51 | -------------------------------------------------------------------------------- /dash_canvas/test/test_parse_json.py: -------------------------------------------------------------------------------- 1 | from dash_canvas.utils import parse_jsonfile 2 | 3 | 4 | def test_parse_jsonfile(): 5 | shape = (433, 640) 6 | mask = parse_jsonfile('tests/data_test.json', shape=shape) 7 | assert mask.sum() > 0 8 | assert mask.shape == shape 9 | -------------------------------------------------------------------------------- /dash_canvas/test/test_registration.py: -------------------------------------------------------------------------------- 1 | from dash_canvas.utils import register_tiles 2 | import numpy as np 3 | from skimage import data, color 4 | import matplotlib.pyplot as plt 5 | 6 | def test_stitching_one_row(): 7 | im = data.moon() 8 | l = 256 9 | 10 | n_rows = 1 11 | n_cols = im.shape[1] // l 12 | top, left = 50, 0 13 | init_i, init_j = top, left 14 | 15 | imgs = np.empty((n_rows, n_cols, l, l)) 16 | 17 | overlap_h = [30, 50] 18 | 19 | i = 0 20 | for j in range(n_cols): 21 | sub_im = im[init_i:init_i + l, init_j:init_j + l] 22 | imgs[i, j] = sub_im 23 | init_j += l - overlap_h[1] 24 | init_i += - overlap_h[0] 25 | 26 | stitch = register_tiles(imgs, n_rows, n_cols, overlap_global=0.2, pad=l//2) 27 | real_top = min(top, top - overlap_h[0]) 28 | delta = im[real_top:real_top+stitch.shape[0], :stitch.shape[1]].astype(np.float) - stitch.astype(np.float) 29 | assert np.all(delta[50:-50] == 0) 30 | stitch = register_tiles(imgs, n_rows, n_cols, overlap_global=0.2, pad=l//2, 31 | blending=False) 32 | delta = im[real_top:real_top+stitch.shape[0], :stitch.shape[1]].astype(np.float) - stitch.astype(np.float) 33 | assert np.all(delta[50:-50] == 0) 34 | 35 | # local_overlap 36 | stitch = register_tiles(imgs, n_rows, n_cols, overlap_global=0.5, pad=l//2, 37 | overlap_local={(0, 1):[20, 45]}) 38 | delta = im[real_top:stitch.shape[0]+real_top, :stitch.shape[1]].astype(np.float) - stitch.astype(np.float) 39 | assert np.all(delta[50:-50] == 0) 40 | 41 | imgs = np.empty((n_rows, n_cols, l, l)) 42 | 43 | overlap_h = [-30, 50] 44 | real_top = min(top, top - overlap_h[0]) 45 | init_i, init_j = top, left 46 | 47 | i = 0 48 | for j in range(n_cols): 49 | sub_im = im[init_i:init_i + l, init_j:init_j + l] 50 | imgs[i, j] = sub_im 51 | init_j += l - overlap_h[1] 52 | init_i += -overlap_h[0] 53 | 54 | stitch = register_tiles(imgs, n_rows, n_cols, overlap_global=0.5, pad=l//2, 55 | overlap_local={(0, 1):[-20, 45]}) 56 | delta = im[real_top:stitch.shape[0]+real_top, :stitch.shape[1]].astype(np.float) - stitch.astype(np.float) 57 | assert np.all(delta[50:-50] == 0) 58 | 59 | 60 | def test_stitching_two_rows(): 61 | im = data.moon() 62 | l = 256 63 | # two rows 64 | n_rows = 2 65 | n_cols = im.shape[1] // l 66 | init_i, init_j = 0, 0 67 | overlap_h = [5, 50] 68 | overlap_v = 40 69 | 70 | imgs = np.empty((n_rows, n_cols, l, l)) 71 | for i in range(n_rows): 72 | for j in range(n_cols): 73 | sub_im = im[init_i:init_i + l, init_j:init_j + l] 74 | imgs[i, j] = sub_im 75 | init_j += l - overlap_h[1] 76 | init_j = 0 77 | init_i += l - overlap_v 78 | 79 | stitch = register_tiles(imgs, n_rows, n_cols, overlap_global=0.2, pad=l//2) 80 | delta = im[:stitch.shape[0], :stitch.shape[1]].astype(np.float) - stitch.astype(np.float) 81 | print(delta.mean()) 82 | assert np.all(delta == 0) 83 | 84 | 85 | def test_stitching_color(): 86 | im = color.gray2rgb(data.moon()) 87 | l = 256 88 | 89 | n_rows = 1 90 | n_cols = im.shape[1] // l 91 | 92 | init_i, init_j = 0, 0 93 | 94 | imgs = np.empty((n_rows, n_cols, l, l, 3)) 95 | 96 | overlap_h = [5, 50] 97 | 98 | i = 0 99 | for j in range(n_cols): 100 | sub_im = im[init_i:init_i + l, init_j:init_j + l] 101 | imgs[i, j] = sub_im 102 | init_j += l - overlap_h[1] 103 | 104 | stitch = register_tiles(imgs, n_rows, n_cols, overlap_global=0.2, pad=l//2) 105 | delta = im[:l, :stitch.shape[1]].astype(np.float) - stitch.astype(np.float) 106 | assert np.all(delta == 0) 107 | 108 | -------------------------------------------------------------------------------- /dash_canvas/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .image_processing_utils import (watershed_segmentation, 2 | random_walker_segmentation, 3 | random_forest_segmentation, 4 | segmentation_generic, 5 | superpixel_color_segmentation, 6 | modify_segmentation) 7 | from .registration import register_tiles, autocrop 8 | from .parse_json import (parse_jsonstring, parse_jsonstring_line, 9 | parse_jsonstring_rectangle, parse_jsonfile) 10 | from .io_utils import array_to_data_url, image_string_to_PILImage 11 | from .plot_utils import image_with_contour 12 | from .exposure import brightness_adjust, contrast_adjust 13 | 14 | __all__ = ['array_to_data_url', 15 | 'autocrop', 16 | 'brightness_adjust', 17 | 'contrast_adjust', 18 | 'image_string_to_PILImage', 19 | 'image_with_contour', 20 | 'modify_segmentation', 21 | 'parse_jsonfile', 22 | 'parse_jsonstring', 23 | 'parse_jsonstring_line', 24 | 'parse_jsonstring_rectangle', 25 | 'random_forest_segmentation', 26 | 'random_walker_segmentation', 27 | 'register_tiles', 28 | 'segmentation_generic', 29 | 'superpixel_color_segmentation', 30 | 'watershed_segmentation'] 31 | 32 | -------------------------------------------------------------------------------- /dash_canvas/utils/exposure.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | def contrast_adjust(img, intensity): 4 | print("contrast adjust", intensity) 5 | contrast_range = 255 ** (2 - 2*intensity) 6 | img_f = img.astype(np.float) 7 | img_f = np.clip((img_f - 127) * 255. / contrast_range + 127, 0, 255) 8 | return img_f.astype(np.uint8) 9 | 10 | 11 | def brightness_adjust(img, intensity): 12 | offset = int((intensity - 0.5) * 255) 13 | if intensity < 0.5: 14 | img = np.clip(img, np.abs(offset), 255) 15 | return (img + offset).astype(np.uint8) 16 | elif intensity > 0.5: 17 | img = np.clip(img, None, 255 - offset) 18 | return (img + offset).astype(np.uint8) 19 | else: 20 | return img 21 | 22 | -------------------------------------------------------------------------------- /dash_canvas/utils/image_processing_utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from skimage import segmentation, morphology, measure, color, feature, filters 3 | from skimage import img_as_float 4 | from scipy import ndimage 5 | from sklearn.ensemble import RandomForestClassifier 6 | from sklearn.neighbors import KNeighborsClassifier 7 | 8 | # ------------------- Modification of segmentation ---------------------- 9 | 10 | def _split_labels(labels, mask, img, erosion_width=10, compactness=0): 11 | """ 12 | Divide already labeled array ``labels`` according to array of annotations 13 | ``mask``. 14 | 15 | Parameters 16 | ---------- 17 | 18 | labels : array of ints 19 | Array of labels. 20 | mask : array of ints 21 | Array with annotations. 22 | img : array, default None 23 | Image used for the segmentation. 24 | erosion_width : int, optional 25 | Number of pixels to erode on both sides of annotations. Decrease if 26 | boundaries should correspond to the exact annotations, increase if 27 | boundaries should rather follow strong image gradients. 28 | compactness : float, optional (default 0) 29 | Parameter of the compact waterhsed algorithm 30 | ``skimage.segmentation.watershed``, should be zero (for normal 31 | watershed) or greater than zero (for compact watershed). 32 | """ 33 | out = np.copy(labels) 34 | bounding_boxes = ndimage.find_objects(labels) 35 | max_label = labels.max() 36 | annot_indices = np.unique(mask)[1:] 37 | count = max_label + 1 38 | shift = 0 if labels.min() > 0 else 1 39 | for annot_index in annot_indices: 40 | obj_label = (np.argmax(np.bincount(labels[mask == annot_index])[shift:] 41 | ) + shift) 42 | # Get subarrays 43 | box = bounding_boxes[obj_label - 1] 44 | img_box = img[box] 45 | labels_box = labels[box] 46 | # Prepare watershed 47 | gradient_img = - ndimage.gaussian_gradient_magnitude(img_box, 2) 48 | mask_box = np.ones(img_box.shape, dtype=np.uint8) 49 | mask_box[mask[box] == annot_index] = 0 50 | mask_box = morphology.binary_erosion(mask_box, 51 | morphology.disk(erosion_width)) 52 | masked_region = labels_box == obj_label 53 | mask_box[np.logical_not(masked_region)] = 0 54 | mask_box = measure.label(mask_box) 55 | res = segmentation.watershed(gradient_img, mask_box, 56 | compactness=compactness, 57 | mask=masked_region) 58 | out[box][res == 1] = count # only modify one of the regions 59 | count += 1 60 | return out 61 | 62 | 63 | def _merge_labels(labels, mask, skip_zero=True): 64 | """ 65 | Merge objects covered by the same annotation, given an array of 66 | labels defining objects and a labeled mask of annotations. 67 | 68 | Parameters 69 | ---------- 70 | 71 | labels : array of ints 72 | Array of labels. 73 | mask : array of ints 74 | Array with annotations. 75 | 76 | """ 77 | annot_indices = np.unique(mask)[1:] 78 | label_indices = np.arange(labels.max() + 1, dtype=np.int) 79 | for index in annot_indices: 80 | object_indices = np.unique(labels[mask == index]) 81 | if skip_zero: 82 | object_indices = np.setdiff1d(object_indices, [0]) 83 | label_indices[object_indices] = object_indices[0] 84 | relabeled, _, _ = segmentation.relabel_sequential(label_indices) 85 | return relabeled[labels] 86 | 87 | 88 | def modify_segmentation(labels, mask, img=None, mode='split'): 89 | """ 90 | Modify already labeled image according to annotations. In 'split' mode, 91 | each annotation is used to divide a label in two subregions. In 'merge' 92 | mode, objects covered by the same annotation are merged together. 93 | 94 | Parameters 95 | ---------- 96 | 97 | labels : array of ints 98 | Array of labels. 99 | mask : binary of int array 100 | Array with annotations. 101 | img : array, default None 102 | Image used for the segmentation. 103 | mode: string, 'split' or 'merge' 104 | 105 | Returns 106 | ------- 107 | 108 | out : array of ints 109 | New labels. 110 | """ 111 | labels = np.asarray(labels) 112 | mask = measure.label(mask) 113 | if img is None: 114 | img = np.zeros_like(mask) 115 | 116 | if mode == 'split': 117 | return _split_labels(labels, mask, img) 118 | elif mode == 'merge': 119 | return _merge_labels(labels, mask) 120 | else: 121 | raise ValueError('mode should be either split or merge') 122 | 123 | 124 | # ------------- Segmentation from markers ---------------------------- 125 | 126 | def watershed_segmentation(img, mask, sigma=4): 127 | """ 128 | Watershed segmentation of image using annotations as label markers. 129 | 130 | The image used for the watershed is minus the gradient of the original 131 | image, convoluted with a Gaussian for more robustness. 132 | 133 | Parameters 134 | ---------- 135 | 136 | img : ndarray 137 | image to be segmented 138 | mask : ndarray of ints 139 | binary array, each connected component corresponds to a different 140 | object to be segmented 141 | sigma : float 142 | standard deviation of Gaussian convoluting the gradient. Increase 143 | for smoother boundaries. 144 | """ 145 | if img.ndim > 2: 146 | img = color.rgb2gray(img) 147 | labels = measure.label(mask) 148 | gradient_img = - ndimage.gaussian_gradient_magnitude(img, sigma) 149 | output = segmentation.watershed(gradient_img, labels) 150 | return output 151 | 152 | 153 | def random_walker_segmentation(img, mask, beta=5000): 154 | """ 155 | Random-walker segmentation of image using annotations as label markers. 156 | 157 | Parameters 158 | ---------- 159 | 160 | img : ndarray 161 | image to be segmented 162 | mask : ndarray of ints 163 | binary array, each connected component corresponds to a different 164 | object to be segmented 165 | beta : float 166 | beta parameter of random walker, smaller beta results in more compact 167 | objects, larger beta in boundaries following gradients more 168 | accurately. 169 | 170 | Returns 171 | ------- 172 | 173 | output : ndarray of ints 174 | image of labels 175 | 176 | Notes 177 | ----- 178 | 179 | see skimage.segmentation.random_walker 180 | """ 181 | if img.ndim > 2: 182 | img = color.rgb2gray(img) 183 | labels = measure.label(mask) 184 | output = segmentation.random_walker(img, labels, beta=beta, mode='cg_mg') 185 | return output 186 | 187 | 188 | def _compute_features_gabor(im): 189 | gabor_frequencies = np.logspace(-5, 0.5, num=6, base=2) 190 | thetas = [0, np.pi/4., np.pi/2] 191 | nb_fq = len(gabor_frequencies) * len(thetas) 192 | im = np.atleast_3d(im) 193 | im_gabor = np.empty((im.shape[-1], nb_fq) + im.shape[:2]) 194 | for ch in range(im.shape[-1]): 195 | img = img_as_float(im[..., ch]) 196 | for i_fq, fq in enumerate(gabor_frequencies): 197 | for i_th, theta in enumerate(thetas): 198 | tmp = filters.gabor(img, fq, theta=theta) 199 | im_gabor[ch, len(thetas) * i_fq + i_th] = \ 200 | np.abs(tmp[0] + 1j * tmp[1]) 201 | return im_gabor 202 | 203 | 204 | def random_forest_segmentation(img, mask, mode='gabor'): 205 | """ 206 | Segmentation of image using machine learning. Annotations are used 207 | as training set for a random forest classifier. Features used for the 208 | classification are Gabor or Daisy features. 209 | 210 | Parameters 211 | ---------- 212 | 213 | img : ndarray 214 | Image to be segmented 215 | mask : ndarray of ints 216 | Binary array, each connected component corresponds to a different 217 | object to be segmented 218 | mode : string 219 | Type of features used in classification, can be 'daisy' or 'gabor'. 220 | """ 221 | labels = measure.label(mask) 222 | if mode == 'daisy': 223 | if img.ndim > 2: 224 | img = color.rgb2gray(img) 225 | radius = 15 226 | features = feature.daisy(img, step=1, radius=radius, rings=2, 227 | histograms=4) 228 | crop_labels = labels[radius:-radius, radius:-radius] 229 | elif mode == 'gabor': 230 | features = _compute_features_gabor(img) 231 | nb_ch, nb_fq, sh_1, sh_2 = features.shape 232 | features = features.reshape((nb_ch * nb_fq, sh_1, sh_2)) 233 | features = np.moveaxis(features, 0, -1) 234 | crop_labels = labels 235 | X_train = features[crop_labels > 0, :] 236 | Y_train = crop_labels[crop_labels > 0] 237 | rf = RandomForestClassifier(n_estimators=100) 238 | rf.fit(X_train, Y_train) 239 | output = rf.predict(features.reshape(-1, features.shape[2])) 240 | output = output.reshape(crop_labels.shape) 241 | output[crop_labels > 0] = crop_labels[crop_labels > 0] 242 | return output 243 | 244 | 245 | def segmentation_generic(img, mask, mode='watershed'): 246 | """ 247 | Segmentation of image using annotations as markers (seeds), using 248 | different algorithms. 249 | 250 | Parameters 251 | ---------- 252 | 253 | img : ndarray 254 | Image to be segmented 255 | mask : ndarray of ints 256 | Binary array, each connected component corresponds to a different 257 | object to be segmented 258 | mode : 'string', optional 259 | Algorithm to be used, can be 'watershed', 'random_walker' or 260 | 'random_forest'. 261 | """ 262 | if mode=='watershed': 263 | return watershed_segmentation(img, mask) 264 | elif mode=='random_walker': 265 | return random_walker_segmentation(img, mask) 266 | elif mode=='random_forest': 267 | return random_forest_segmentation(img, mask) 268 | else: 269 | raise NotImplementedError 270 | 271 | 272 | # -------------------- Background removal ---------------------------- 273 | 274 | 275 | def superpixel_color_segmentation(im, mask, mode='bbox', remove_holes='all', 276 | object='single', keep_label=True): 277 | if not mask.shape == im.shape: 278 | big_mask = np.zeros(im.shape[:2], dtype=np.uint8) 279 | big_mask[:mask.shape[0], :mask.shape[1]] = mask 280 | mask = big_mask 281 | if mask.max() == 1: 282 | mask = annotation_to_background_mask(mask, mode=mode) 283 | px = segmentation.felzenszwalb(im, scale=4) 284 | colors = np.stack([ndimage.mean(im[..., i], px, 285 | index=np.arange(0, px.max() + 1)) 286 | for i in range(3)]).T 287 | nb_phases = len(np.unique(mask)) - 1 288 | indices = [np.unique(px[mask == i]) for i in range(1, nb_phases + 1)] 289 | target = np.concatenate([(i + 1) * np.ones(len(indices[i])) 290 | for i in range(len(indices))]) 291 | training_set = np.vstack([colors[indices_i] for indices_i in indices]) 292 | clf = KNeighborsClassifier() 293 | clf.fit(training_set, target) 294 | res = clf.predict(colors) 295 | if keep_label: 296 | for i in range(1, nb_phases + 1): 297 | res[indices[i - 1]] = i 298 | res_img = res[px] 299 | mask_res = res_img == 1 300 | if object == 'single': 301 | labels = measure.label(mask_res) 302 | index = np.argmax(np.bincount(labels.ravel())[1:]) + 1 303 | mask_res = labels == index 304 | 305 | if remove_holes == 'all': 306 | return ndimage.binary_fill_holes(mask_res) 307 | elif np.isscalar(remove_holes): 308 | return morphology.remove_small_holes(mask_res, remove_holes) 309 | 310 | 311 | def annotation_to_background_mask(mask_annotation, mode='bbox'): 312 | mask = np.copy(mask_annotation).astype(np.uint8) 313 | if mode == 'bbox': 314 | sls = ndimage.find_objects(mask_annotation)[0] 315 | mask[:int(0.9 * sls[0].start)] = 2 316 | mask[int(1.1 * sls[0].stop):] = 2 317 | mask[:, :int(0.9 * sls[1].start)] = 2 318 | mask[:, int(1.1 * sls[1].stop):] = 2 319 | if mode == 'cvxhull': 320 | cvx = morphology.convex_hull_image(mask_annotation) 321 | cvx = morphology.binary_dilation(cvx, np.ones((11, 11))) 322 | mask[np.logical_not(cvx)] = 2 323 | return mask 324 | -------------------------------------------------------------------------------- /dash_canvas/utils/io_utils.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | from io import BytesIO 3 | import base64 4 | 5 | 6 | def array_to_data_url(img, dtype=None): 7 | """ 8 | Converts numpy array to data string, using Pillow. 9 | 10 | The returned image string has the right format for the ``image_content`` 11 | property of DashCanvas. 12 | 13 | Parameters 14 | ========== 15 | 16 | img : numpy array 17 | 18 | Returns 19 | ======= 20 | 21 | image_string: str 22 | """ 23 | if dtype is not None: 24 | img = img.astype(dtype) 25 | pil_img = Image.fromarray(img) 26 | buff = BytesIO() 27 | pil_img.save(buff, format="png") 28 | prefix = b'data:image/png;base64,' 29 | image_string = (prefix + base64.b64encode(buff.getvalue())).decode("utf-8") 30 | return image_string 31 | 32 | 33 | def image_string_to_PILImage(image_string): 34 | """ 35 | Converts image string to PIL image object. 36 | """ 37 | return Image.open(BytesIO(base64.b64decode(image_string[22:]))) 38 | -------------------------------------------------------------------------------- /dash_canvas/utils/parse_json.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import json 3 | from skimage import draw, morphology 4 | from scipy import ndimage 5 | 6 | 7 | def _indices_of_path(path, scale=1): 8 | """ 9 | Retrieve pixel indices (integer values). 10 | 11 | Parameters 12 | ---------- 13 | 14 | path: SVG-like path formatted as JSON string 15 | The path is formatted like 16 | ['M', x0, y0], 17 | ['Q', xc1, yc1, xe1, ye1], 18 | ['Q', xc2, yc2, xe2, ye2], 19 | ... 20 | ['L', xn, yn] 21 | where (xc, yc) are for control points and (xe, ye) for end points. 22 | 23 | Notes 24 | ----- 25 | 26 | I took a weight of 1 and it seems fine from visual inspection. 27 | """ 28 | rr, cc = [], [] 29 | for (Q1, Q2) in zip(path[:-2], path[1:-1]): 30 | # int(round()) is for Python 2 compatibility 31 | inds = draw.bezier_curve(int(round(Q1[-1] / scale)), 32 | int(round(Q1[-2] / scale)), 33 | int(round(Q2[2] / scale)), 34 | int(round(Q2[1] / scale)), 35 | int(round(Q2[4] / scale)), 36 | int(round(Q2[3] / scale)), 1) 37 | rr += list(inds[0]) 38 | cc += list(inds[1]) 39 | return rr, cc 40 | 41 | 42 | def parse_jsonstring(string, shape=None, scale=1): 43 | """ 44 | Parse JSON string to draw the path saved by react-sketch. 45 | 46 | Up to now only path objects are processed (created with Pencil tool). 47 | 48 | Parameters 49 | ---------- 50 | 51 | data : str 52 | JSON string of data 53 | shape: tuple, optional 54 | shape of returned image. 55 | 56 | Returns 57 | ------- 58 | 59 | mask: ndarray of bools 60 | binary array where the painted regions are one. 61 | """ 62 | if shape is None: 63 | shape = (500, 500) 64 | mask = np.zeros(shape, dtype=np.bool) 65 | try: 66 | data = json.loads(string) 67 | except: 68 | return mask 69 | scale = 1 70 | for obj in data['objects']: 71 | if obj['type'] == 'image': 72 | scale = obj['scaleX'] 73 | elif obj['type'] == 'path': 74 | scale_obj = obj['scaleX'] 75 | inds = _indices_of_path(obj['path'], scale=scale / scale_obj) 76 | radius = round(obj['strokeWidth'] / 2. / scale) 77 | mask_tmp = np.zeros(shape, dtype=np.bool) 78 | mask_tmp[inds[0], inds[1]] = 1 79 | mask_tmp = ndimage.binary_dilation(mask_tmp, 80 | morphology.disk(radius)) 81 | mask += mask_tmp 82 | return mask 83 | 84 | 85 | def parse_jsonstring_line(string): 86 | """ 87 | Return geometry of line objects. 88 | 89 | Parameters 90 | ---------- 91 | 92 | data : str 93 | JSON string of data 94 | 95 | """ 96 | try: 97 | data = json.loads(string) 98 | except: 99 | return None 100 | scale = 1 101 | props = [] 102 | for obj in data['objects']: 103 | if obj['type'] == 'image': 104 | scale = obj['scaleX'] 105 | elif obj['type'] == 'line': 106 | length = np.sqrt(obj['width']**2 + obj['height']**2) 107 | scale_factor = obj['scaleX'] / scale 108 | props.append([scale_factor * length, 109 | scale_factor * obj['width'], 110 | scale_factor * obj['height'], 111 | scale_factor * obj['left'], 112 | scale_factor * obj['top']]) 113 | return (np.array(props)).astype(np.int) 114 | 115 | 116 | def parse_jsonstring_rectangle(string): 117 | """ 118 | Return geometry of rectangle objects. 119 | 120 | Parameters 121 | ---------- 122 | 123 | data : str 124 | JSON string of data 125 | 126 | """ 127 | try: 128 | data = json.loads(string) 129 | except: 130 | return None 131 | scale = 1 132 | props = [] 133 | for obj in data['objects']: 134 | if obj['type'] == 'image': 135 | scale = obj['scaleX'] 136 | elif obj['type'] == 'rect': 137 | scale_factor = obj['scaleX'] / scale 138 | props.append([scale_factor * obj['width'], 139 | scale_factor * obj['height'], 140 | scale_factor * obj['left'], 141 | scale_factor * obj['top']]) 142 | return (np.array(props)).astype(np.int) 143 | 144 | 145 | def parse_jsonfile(filename, shape=None): 146 | with open(filename, 'r') as fp: 147 | string = json.load(fp) 148 | return parse_jsonstring(string, shape=shape) 149 | 150 | 151 | -------------------------------------------------------------------------------- /dash_canvas/utils/plot_utils.py: -------------------------------------------------------------------------------- 1 | import plotly.graph_objs as go 2 | import PIL 3 | import numpy as np 4 | from skimage import color, img_as_ubyte 5 | from plotly import colors 6 | 7 | def image_with_contour(img, labels, mode='lines', shape=None): 8 | """ 9 | Figure with contour plot of labels superimposed on background image. 10 | 11 | Parameters 12 | ---------- 13 | 14 | img : URL, dataURI or ndarray 15 | Background image. If a numpy array, it is transformed into a PIL 16 | Image object. 17 | labels : 2D ndarray 18 | Contours are the isolines of labels. 19 | shape: tuple, optional 20 | Shape of the arrays, to be provided if ``img`` is not a numpy array. 21 | """ 22 | try: 23 | sh_y, sh_x = shape if shape is not None else img.shape 24 | except AttributeError: 25 | print('''the shape of the image must be provided with the 26 | ``shape`` parameter if ``img`` is not a numpy array''') 27 | if type(img) == np.ndarray: 28 | img = img_as_ubyte(color.gray2rgb(img)) 29 | img = PIL.Image.fromarray(img) 30 | labels = labels.astype(np.float) 31 | custom_viridis = colors.PLOTLY_SCALES['Viridis'] 32 | custom_viridis.insert(0, [0, '#FFFFFF']) 33 | custom_viridis[1][0] = 1.e-4 34 | # Contour plot of segmentation 35 | print('mode is', mode) 36 | opacity = 0.4 if mode is None else 1 37 | cont = go.Contour(z=labels[::-1], 38 | contours=dict(start=0, end=labels.max() + 1, size=1, 39 | coloring=mode), 40 | line=dict(width=1), 41 | showscale=False, 42 | colorscale=custom_viridis, 43 | opacity=opacity, 44 | ) 45 | # Layout 46 | layout= go.Layout( 47 | images = [dict( 48 | source=img, 49 | xref="x", 50 | yref="y", 51 | x=0, 52 | y=sh_y, 53 | sizex=sh_x, 54 | sizey=sh_y, 55 | sizing="contain", 56 | layer="below")], 57 | xaxis=dict( 58 | showgrid=False, 59 | zeroline=False, 60 | showline=False, 61 | ticks='', 62 | showticklabels=False, 63 | ), 64 | yaxis=dict( 65 | showgrid=False, 66 | zeroline=False, 67 | showline=False, 68 | scaleanchor="x", 69 | ticks='', 70 | showticklabels=False,), 71 | margin=dict(b=5, t=20)) 72 | fig = go.Figure(data=[cont], layout=layout) 73 | return fig 74 | 75 | 76 | if __name__ == '__main__': 77 | from skimage import data 78 | import plotly.plotly as py 79 | camera = data.camera() 80 | fig = image_with_contour(camera, camera > 150) 81 | py.iplot(fig) 82 | -------------------------------------------------------------------------------- /dash_canvas/utils/registration.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from skimage import io, measure, feature 3 | from scipy import ndimage 4 | 5 | 6 | def autocrop(img): 7 | """ 8 | Remove zero-valued rectangles at the border of the image. 9 | 10 | Parameters 11 | ---------- 12 | 13 | img: ndarray 14 | Image to be cropped 15 | """ 16 | slices = ndimage.find_objects(img > 0)[0] 17 | return img[slices] 18 | 19 | 20 | def _blending_mask(shape): 21 | mask = np.zeros(shape, dtype=np.int) 22 | mask[1:-1, 1:-1] = 1 23 | return ndimage.distance_transform_cdt(mask) + 1 24 | 25 | 26 | def register_tiles(imgs, n_rows, n_cols, overlap_global=None, 27 | overlap_local=None, pad=None, blending=True): 28 | """ 29 | Stitch together overlapping tiles of a mosaic, using Fourier-based 30 | registration to estimate the shifts between neighboring tiles. 31 | 32 | Parameters 33 | ---------- 34 | 35 | imgs: array of tiles, of shape (n_rows, n_cols, l_r, l_r) with (l_c, l_r) 36 | the shape of individual tiles. 37 | n_rows: int 38 | number of rows of the mosaic. 39 | n_cols : int 40 | number of columns of the mosaic. 41 | overlap_global : float 42 | Fraction of overlap between tiles. 43 | overlap_local : dictionary 44 | Local overlaps between pairs of tiles. overlap_local[(i, j)] is a pair 45 | of (x, y) shifts giving the 2D shift vector between tiles i and j. 46 | Indices (i, j) are the raveled indices of the tile numbers. 47 | pad : int 48 | Value of the padding used at the border of the stitched image. An 49 | autocrop is performed at the end to remove the unnecessary padding. 50 | 51 | Notes 52 | ----- 53 | 54 | Fourier-based registration is used in this function 55 | (skimage.feature.register_translation). 56 | """ 57 | if pad is None: 58 | pad = 200 59 | l_r, l_c = imgs.shape[2:4] 60 | if overlap_global is None: 61 | overlap_global = 0.15 62 | overlap_value = int(float(overlap_global) * l_r) 63 | imgs = imgs.astype(np.float) 64 | if blending: 65 | blending_mask = _blending_mask((l_r, l_c)) 66 | else: 67 | blending_mask = np.ones((l_r, l_c)) 68 | 69 | if imgs.ndim == 4: 70 | canvas = np.zeros((2 * pad + n_rows * l_r, 2 * pad + n_cols * l_c), 71 | dtype=imgs.dtype) 72 | else: 73 | canvas = np.zeros((2 * pad + n_rows * l_r, 2 * pad + n_cols * l_c, 3), 74 | dtype=imgs.dtype) 75 | blending_mask = np.dstack((blending_mask, )*3) 76 | weights = np.zeros_like(canvas) 77 | init_r, init_c = pad, pad 78 | weighted_img = imgs[0, 0] * blending_mask 79 | canvas[init_r:init_r + l_r, init_c:init_c + l_c] = weighted_img 80 | weights[init_r:init_r + l_r, init_c:init_c + l_c] = blending_mask 81 | shifts = np.empty((n_rows, n_cols, 2), dtype=np.int) 82 | shifts[0, 0] = init_r, init_c 83 | 84 | for i_rows in range(n_rows): 85 | # Shifts between rows 86 | if i_rows >= 1: 87 | index_target = np.ravel_multi_index((i_rows, 0), (n_rows, n_cols)) 88 | index_orig = index_target - n_cols 89 | try: 90 | overlap = overlap_local[(index_orig, index_target)] 91 | except (KeyError, TypeError): 92 | overlap = np.array([overlap_value, 0]) 93 | init_r, init_c = shifts[i_rows - 1, 0] 94 | init_r += l_r 95 | shift_vert = feature.register_translation( 96 | imgs[i_rows - 1, 0, -overlap[0]:, :(l_c - overlap[1])], 97 | imgs[i_rows, 0, :overlap[0], -(l_c - overlap[1]):])[0] 98 | init_r += int(shift_vert[0]) - overlap[0] 99 | init_c += int(shift_vert[1]) - overlap[1] 100 | shifts[i_rows, 0] = init_r, init_c 101 | # Fill canvas and weights 102 | weighted_img = imgs[i_rows, 0] * blending_mask 103 | canvas[init_r:init_r + l_r, init_c:init_c + l_c] += weighted_img 104 | weights[init_r:init_r + l_r, init_c:init_c + l_c] += blending_mask 105 | # Shifts between columns 106 | for j_cols in range(n_cols - 1): 107 | index_orig = np.ravel_multi_index((i_rows, j_cols), 108 | (n_rows, n_cols)) 109 | index_target = index_orig + 1 110 | try: 111 | overlap = overlap_local[(index_orig, index_target)] 112 | except (KeyError, TypeError): 113 | overlap = np.array([0, overlap_value]) 114 | init_c += l_c 115 | if overlap[0] < 0: 116 | print("up") 117 | row_start_1 = -(l_r + overlap[0]) 118 | row_end_1 = None 119 | row_start_2 = None 120 | row_end_2 = l_r + overlap[0] 121 | else: 122 | print("down") 123 | row_start_1 = None 124 | row_end_1= (l_r - overlap[0]) 125 | row_start_2 = -(l_r - overlap[0]) 126 | row_end_2 = None 127 | shift_horiz = feature.register_translation( 128 | imgs[i_rows, j_cols, row_start_1:row_end_1, -overlap[1]:], 129 | imgs[i_rows, j_cols + 1, row_start_2:row_end_2, :overlap[1]])[0] 130 | init_r += int(shift_horiz[0]) - (overlap[0]) 131 | init_c += int(shift_horiz[1]) - overlap[1] 132 | shifts[i_rows, j_cols + 1] = init_r, init_c 133 | # Fill canvas and weights 134 | weighted_img = imgs[i_rows, j_cols + 1] * blending_mask 135 | canvas[init_r:init_r + l_r, init_c:init_c + l_c] += weighted_img 136 | weights[init_r:init_r + l_r, init_c:init_c + l_c] += blending_mask 137 | 138 | canvas /= (weights + 1.e-5) 139 | return autocrop(np.rint(canvas).astype(np.uint8)) 140 | -------------------------------------------------------------------------------- /doc/segmentation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-canvas/4f4df65b43acb20bae7eb34572ca240dabfbfb72/doc/segmentation.gif -------------------------------------------------------------------------------- /extract-meta.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const reactDocs = require('react-docgen'); 6 | 7 | const componentPaths = process.argv.slice(2); 8 | if (!componentPaths.length) { 9 | help(); 10 | process.exit(1); 11 | } 12 | 13 | const metadata = Object.create(null); 14 | componentPaths.forEach(componentPath => 15 | collectMetadataRecursively(componentPath) 16 | ); 17 | writeOut(metadata); 18 | 19 | function help() { 20 | console.error('usage: '); 21 | console.error( 22 | 'extract-meta path/to/component(s) ' + 23 | ' [path/to/more/component(s), ...] > metadata.json' 24 | ); 25 | } 26 | 27 | function writeError(msg, filePath) { 28 | if (filePath) { 29 | process.stderr.write(`Error with path ${filePath}`); 30 | } 31 | 32 | process.stderr.write(msg + '\n'); 33 | if (msg instanceof Error) { 34 | process.stderr.write(msg.stack + '\n'); 35 | } 36 | } 37 | 38 | function checkWarn(name, value) { 39 | if (value.length < 1) { 40 | process.stderr.write(`\nDescription for ${name} is missing!\n`) 41 | } 42 | } 43 | 44 | function docstringWarning(doc) { 45 | checkWarn(doc.displayName, doc.description); 46 | 47 | Object.entries(doc.props).forEach( 48 | ([name, p]) => checkWarn(`${doc.displayName}.${name}`, p.description) 49 | ); 50 | } 51 | 52 | 53 | function parseFile(filepath) { 54 | const urlpath = filepath.split(path.sep).join('/'); 55 | let src; 56 | 57 | if (!['.jsx', '.js'].includes(path.extname(filepath))) { 58 | return; 59 | } 60 | 61 | try { 62 | src = fs.readFileSync(filepath); 63 | const doc = metadata[urlpath] = reactDocs.parse(src); 64 | docstringWarning(doc); 65 | } catch (error) { 66 | writeError(error, filepath); 67 | } 68 | } 69 | 70 | function collectMetadataRecursively(componentPath) { 71 | if (fs.lstatSync(componentPath).isDirectory()) { 72 | let dirs; 73 | try { 74 | dirs = fs.readdirSync(componentPath); 75 | } catch (error) { 76 | writeError(error, componentPath); 77 | } 78 | dirs.forEach(filename => { 79 | const filepath = path.join(componentPath, filename); 80 | if (fs.lstatSync(filepath).isDirectory()) { 81 | collectMetadataRecursively(filepath); 82 | } else { 83 | parseFile(filepath); 84 | } 85 | }); 86 | } else { 87 | parseFile(componentPath); 88 | } 89 | } 90 | 91 | function writeOut(result) { 92 | console.log(JSON.stringify(result, '\t', 2)); 93 | } 94 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | my-dash-component 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /index.py: -------------------------------------------------------------------------------- 1 | import dash_core_components as dcc 2 | import dash_html_components as html 3 | from dash.dependencies import Input, Output 4 | from glob import glob 5 | import base64 6 | import dash 7 | 8 | import app1_seg as app1 9 | import app2_correct_segmentation as app2 10 | import app3_background_removal as app3 11 | import app4_measure_length as app4 12 | import app5_stitching as app5 13 | 14 | app = dash.Dash(__name__) 15 | server = app.server 16 | app.config.suppress_callback_exceptions = True 17 | 18 | 19 | app.layout = html.Div([ 20 | dcc.Location(id='url', refresh=False), 21 | html.Div(id='page-content') 22 | ]) 23 | 24 | apps = {'app1': app1, 'app2': app2, 'app3': app3, 'app4': app4, 25 | 'app5': app5} 26 | 27 | for key in apps: 28 | try: 29 | apps[key].callbacks(app) 30 | except AttributeError: 31 | continue 32 | 33 | 34 | def demo_app_desc(name): 35 | """ Returns the content of the description specified in the app. """ 36 | desc = '' 37 | try: 38 | desc = apps[name].description() 39 | except AttributeError: 40 | pass 41 | return desc 42 | 43 | 44 | def demo_app_name(name): 45 | """ Returns a capitalized title for the app, with "Dash" 46 | in front.""" 47 | desc = '' 48 | try: 49 | desc = apps[name].title() 50 | except AttributeError: 51 | pass 52 | return desc 53 | 54 | 55 | def demo_app_link_id(name): 56 | """Returns the value of the id of the dcc.Link related to the demo app. """ 57 | return 'app-link-id-{}'.format(name) 58 | 59 | 60 | def demo_app_img_src(name): 61 | """ Returns the base-64 encoded image corresponding 62 | to the specified app.""" 63 | pic_fname = './app_pics/{}.png'.format( 64 | name 65 | ) 66 | try: 67 | return 'data:image/png;base64,{}'.format( 68 | base64.b64encode( 69 | open(pic_fname, 'rb').read()).decode()) 70 | except Exception: 71 | return 'data:image/png;base64,{}'.format( 72 | base64.b64encode( 73 | open('./assets/dashbio_logo.png', 'rb').read()).decode()) 74 | 75 | 76 | 77 | @app.callback(Output('page-content', 'children'), 78 | [Input('url', 'pathname')]) 79 | def display_page(pathname): 80 | if pathname is not None and len(pathname) > 1 and pathname[1:] in apps.keys(): 81 | app_name = pathname[1:] 82 | return html.Div(id="waitfor", 83 | children=apps[app_name].layout, 84 | ) 85 | else: 86 | return html.Div( 87 | id='gallery-apps', 88 | children=[ 89 | html.Div(className='gallery-app', children=[ 90 | dcc.Link( 91 | children=[ 92 | html.Img(className='gallery-app-img', 93 | src=demo_app_img_src(name)), 94 | html.Div(className='gallery-app-info', 95 | children=[ 96 | html.Div(className='gallery-app-name', 97 | children=[ 98 | demo_app_name(name) 99 | ]), 100 | html.Div(className='gallery-app-desc', 101 | children=[demo_app_desc(name) 102 | ]), 103 | 104 | ]) 105 | ], 106 | id=demo_app_link_id(name), 107 | href="/{}".format( 108 | name.replace("app_", "").replace("_", "-") 109 | ) 110 | ) 111 | ]) for name in apps 112 | ]) 113 | 114 | 115 | 116 | server = app.server 117 | 118 | if __name__ == '__main__': 119 | app.run_server(debug=True) 120 | -------------------------------------------------------------------------------- /man/dashCanvas.Rd: -------------------------------------------------------------------------------- 1 | % Auto-generated: do not edit by hand 2 | \name{dashCanvas} 3 | 4 | \alias{dashCanvas} 5 | 6 | \title{DashCanvas component} 7 | 8 | \description{ 9 | Canvas component for drawing on a background image and selecting regions. 10 | } 11 | 12 | \usage{ 13 | dashCanvas(id=NULL, image_content=NULL, zoom=NULL, width=NULL, height=NULL, scale=NULL, tool=NULL, lineWidth=NULL, lineColor=NULL, goButtonTitle=NULL, filename=NULL, trigger=NULL, json_data=NULL, hide_buttons=NULL) 14 | } 15 | 16 | \arguments{ 17 | \item{id}{The ID used to identify this component in Dash callbacks} 18 | 19 | \item{image_content}{Image data string, formatted as png or jpg data string. Can be 20 | generated by utils.io_utils.array_to_data_string.} 21 | 22 | \item{zoom}{Zoom factor} 23 | 24 | \item{width}{Width of the canvas} 25 | 26 | \item{height}{Height of the canvas} 27 | 28 | \item{scale}{Scaling ratio between canvas width and image width} 29 | 30 | \item{tool}{Selection of drawing tool, among ["pencil", "pan", "circle", 31 | "rectangle", "select", "line"].} 32 | 33 | \item{lineWidth}{Width of drawing line (in pencil mode)} 34 | 35 | \item{lineColor}{Color of drawing line (in pencil mode). Can be a text string, 36 | like 'yellow', 'red', or a color triplet like 'rgb(255, 0, 0)'. 37 | Alpha is possible with 'rgba(255, 0, 0, 0.5)'.} 38 | 39 | \item{goButtonTitle}{Title of button} 40 | 41 | \item{filename}{Name of image file to load (URL string)} 42 | 43 | \item{trigger}{Counter of how many times the save button was pressed 44 | (to be used mostly as input)} 45 | 46 | \item{json_data}{Sketch content as JSON string, containing background image and 47 | annotations. Use utils.parse_json.parse_jsonstring to parse 48 | this string.} 49 | 50 | \item{hide_buttons}{Names of buttons to hide. Names are "zoom", "pan", "line", "pencil", 51 | "rectangle", "undo", "select".} 52 | } 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dash_canvas", 3 | "version": "0.1.0", 4 | "description": "Sketching pad for dash based on react-sketch", 5 | "repository": { 6 | "type": "git", 7 | "url": "git@github.com:plotly/dash-canvas.git" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/plotly/dash-canvas/issues" 11 | }, 12 | "homepage": "https://github.com/plotly/dash-canvas", 13 | "main": "build/index.js", 14 | "scripts": { 15 | "start": "webpack-serve ./webpack.serve.config.js --open", 16 | "validate-init": "python _validate_init.py", 17 | "build:js-dev": "webpack --mode development", 18 | "build:js": "webpack --mode production", 19 | "build:py": "node ./extract-meta.js src/lib/components > dash_canvas/metadata.json && copyfiles package.json dash_canvas && python -c \"import dash; dash.development.component_loader.generate_classes('dash_canvas', 'dash_canvas/metadata.json')\"", 20 | "build": "npm run build:js && npm run build:js-dev && npm run build:py && npm run validate-init", 21 | "postbuild": "es-check es5 dash_canvas/*.js" 22 | }, 23 | "author": "Emmanuelle Gouillart ", 24 | "license": "MIT", 25 | "dependencies": { 26 | "material-ui": "^0.20.0", 27 | "plotly-icons": ">=1.0", 28 | "react": "16.13.0", 29 | "react-color": "^2.18.0", 30 | "react-dom": "16.13.0", 31 | "react-sketch": ">=0.4.4" 32 | }, 33 | "devDependencies": { 34 | "@babel/cli": "^7.6.2", 35 | "@babel/core": "^7.6.2", 36 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 37 | "@babel/preset-env": "^7.6.2", 38 | "@babel/preset-react": "^7.0.0", 39 | "@plotly/webpack-dash-dynamic-import": "^1.1.4", 40 | "babel-eslint": "^8.2.3", 41 | "babel-loader": "^8.0.6", 42 | "babel-preset-env": "^1.7.0", 43 | "babel-preset-react": "^6.24.1", 44 | "copyfiles": "^2.0.0", 45 | "css-loader": "^0.28.11", 46 | "es-check": "^5.0.0", 47 | "eslint": "^4.19.1", 48 | "eslint-config-prettier": "^2.9.0", 49 | "eslint-plugin-import": "^2.12.0", 50 | "eslint-plugin-react": "^7.9.1", 51 | "flexboxgrid": "^6.3.1", 52 | "npm": "^6.1.0", 53 | "react-docgen": "^4.1.1", 54 | "react-dropzone": "4.2.7", 55 | "style-loader": "^0.21.0", 56 | "webpack": "^4.41.0", 57 | "webpack-cli": "^3.3.9", 58 | "webpack-serve": "^1.0.2" 59 | }, 60 | "peerDependencies": { 61 | "react": ">=0.14", 62 | "react-dom": ">=0.14" 63 | }, 64 | "engines": { 65 | "node": ">=8.11.0", 66 | "npm": ">=6.1.0" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # dash is required to call `build:py` 2 | dash[testing]>=1.6.1 3 | numpy 4 | scipy 5 | scikit-image 6 | scikit-learn 7 | pandas 8 | Pillow 9 | pyamg 10 | plotly 11 | gunicorn 12 | -------------------------------------------------------------------------------- /requirements/app.txt: -------------------------------------------------------------------------------- 1 | # dash is required to call `build:py` 2 | dash>=1.6.1 3 | plotly 4 | numpy 5 | scikit-image 6 | scikit-learn 7 | pyamg 8 | gunicorn 9 | -------------------------------------------------------------------------------- /requirements/package.txt: -------------------------------------------------------------------------------- 1 | # dash is required to call `build:py` 2 | dash[dev,testing] 3 | numpy 4 | scikit-image 5 | scikit-learn 6 | Pillow 7 | pyamg 8 | -------------------------------------------------------------------------------- /review_checklist.md: -------------------------------------------------------------------------------- 1 | # Code Review Checklist 2 | 3 | ## Code quality & design 4 | 5 | - Is your code clear? If you had to go back to it in a month, would you be happy to? If someone else had to contribute to it, would they be able to? 6 | 7 | A few suggestions: 8 | 9 | - Make your variable names descriptive and use the same naming conventions throughout the code. 10 | 11 | - For more complex pieces of logic, consider putting a comment, and maybe an example. 12 | 13 | - In the comments, focus on describing _why_ the code does what it does, rather than describing _what_ it does. The reader can most likely read the code, but not necessarily understand why it was necessary. 14 | 15 | - Don't overdo it in the comments. The code should be clear enough to speak for itself. Stale comments that no longer reflect the intent of the code can hurt code comprehension. 16 | 17 | * Don't repeat yourself. Any time you see that the same piece of logic can be applied in multiple places, factor it out into a function, or variable, and reuse that code. 18 | * Scan your code for expensive operations (large computations, DOM queries, React re-renders). Have you done your possible to limit their impact? If not, it is going to slow your app down. 19 | * Can you think of cases where your current code will break? How are you handling errors? Should the user see them as notifications? Should your app try to auto-correct them for them? 20 | 21 | ## Component API 22 | 23 | - Have you tested your component on the Python side by creating an app in `usage.py` ? 24 | 25 | Do all of your component's props work when set from the back-end? 26 | 27 | Should all of them be settable from the back-end or are some only relevant to user interactions in the front-end? 28 | 29 | - Have you provided some basic documentation about your component? The Dash community uses [react docstrings](https://github.com/plotly/dash-docs/blob/master/tutorial/plugins.py#L45) to provide basic information about dash components. Take a look at this [Checklist component example](https://github.com/plotly/dash-core-components/blob/master/src/components/Checklist.react.js) and others from the dash-core-components repository. 30 | 31 | At a minimum, you should describe what your component does, and describe its props and the features they enable. 32 | 33 | Be careful to use the correct formatting for your docstrings for them to be properly recognized. 34 | 35 | ## Tests 36 | 37 | - The Dash team uses integration tests extensively, and we highly encourage you to write tests for the main functionality of your component. In the `tests` folder of the boilerplate, you can see a sample integration test. By launching it, you will run a sample Dash app in a browser. You can run the test with: 38 | ``` 39 | python -m tests.test_render 40 | ``` 41 | [Browse the Dash component code on GitHub for more examples of testing.](https://github.com/plotly/dash-core-components) 42 | 43 | ## Ready to publish? Final scan 44 | 45 | - Take a last look at the external resources that your component is using. Are all the external resources used [referenced in `MANIFEST.in`](https://github.com/plotly/dash-docs/blob/0b2fd8f892db720a7f3dc1c404b4cff464b5f8d4/tutorial/plugins.py#L55)? 46 | 47 | - [You're ready to publish!](https://github.com/plotly/dash-component-boilerplate/blob/master/%7B%7Bcookiecutter.project_shortname%7D%7D/README.md#create-a-production-build-and-publish) 48 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import setuptools 4 | from setuptools import setup 5 | 6 | 7 | with open(os.path.join('dash_canvas', 'package.json')) as f: 8 | package = json.load(f) 9 | 10 | package_name = package["name"].replace(" ", "_").replace("-", "_") 11 | 12 | 13 | setup( 14 | name=package_name, 15 | version=package["version"], 16 | author=package['author'], 17 | url='https://github.com/plotly/dash-canvas', 18 | packages=setuptools.find_packages(), 19 | include_package_data=True, 20 | license=package['license'], 21 | description=package['description'] if 'description' in package else package_name, 22 | install_requires=['dash>=1.6.1', 'scikit-image', 'Pillow', 'scikit-learn'] 23 | ) 24 | -------------------------------------------------------------------------------- /src/demo/App.js: -------------------------------------------------------------------------------- 1 | /* eslint no-magic-numbers: 0 */ 2 | import React, {Component} from 'react'; 3 | 4 | import { DashCanvas } from '../lib'; 5 | 6 | class App extends Component { 7 | 8 | constructor() { 9 | super(); 10 | this.state = { 11 | value: '' 12 | }; 13 | this.setProps = this.setProps.bind(this); 14 | } 15 | 16 | setProps(newProps) { 17 | this.setState(newProps); 18 | } 19 | 20 | render() { 21 | return ( 22 |
23 | 27 |
28 | ) 29 | } 30 | } 31 | 32 | export default App; 33 | -------------------------------------------------------------------------------- /src/demo/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /src/lib/components/DashCanvas.react.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component, lazy, Suspense } from 'react'; 3 | 4 | // eslint-disable-next-line no-inline-comments 5 | const RealDashCanvas = lazy(() => import(/* webpackChunkName: "canvas" */ '../fragments/DashCanvas.react')); 6 | 7 | /** 8 | * Canvas component for drawing on a background image and selecting 9 | * regions. 10 | */ 11 | export default class DashCanvas extends Component { 12 | render() { 13 | return ( 14 | 15 | 16 | 17 | ); 18 | } 19 | } 20 | 21 | DashCanvas.defaultProps = { 22 | filename: '', 23 | json_data: '', image_content: '', trigger: 0, 24 | width: 500, height: 500, scale: 1, lineWidth: 10, 25 | lineColor: 'red', tool: "pencil", zoom: 1, 26 | goButtonTitle: 'Save', hide_buttons: [] 27 | }; 28 | 29 | DashCanvas.propTypes = { 30 | /** 31 | * The ID used to identify this component in Dash callbacks 32 | */ 33 | id: PropTypes.string, 34 | 35 | /** 36 | * Image data string, formatted as png or jpg data string. Can be 37 | * generated by utils.io_utils.array_to_data_string. 38 | */ 39 | image_content: PropTypes.string, 40 | 41 | /** 42 | * Zoom factor 43 | */ 44 | zoom: PropTypes.number, 45 | 46 | 47 | /** 48 | * Width of the canvas 49 | */ 50 | width: PropTypes.number, 51 | 52 | /** 53 | * Height of the canvas 54 | */ 55 | height: PropTypes.number, 56 | 57 | /** 58 | * Scaling ratio between canvas width and image width 59 | */ 60 | scale: PropTypes.number, 61 | 62 | /** 63 | * Selection of drawing tool, among ["pencil", "pan", "circle", 64 | * "rectangle", "select", "line"]. 65 | */ 66 | tool: PropTypes.string, 67 | 68 | /** 69 | * Width of drawing line (in pencil mode) 70 | */ 71 | lineWidth: PropTypes.number, 72 | 73 | /** 74 | * Color of drawing line (in pencil mode). Can be a text string, 75 | * like 'yellow', 'red', or a color triplet like 'rgb(255, 0, 0)'. 76 | * Alpha is possible with 'rgba(255, 0, 0, 0.5)'. 77 | */ 78 | lineColor: PropTypes.string, 79 | 80 | /** 81 | * Title of button 82 | */ 83 | goButtonTitle: PropTypes.string, 84 | 85 | 86 | /** 87 | * Name of image file to load (URL string) 88 | */ 89 | filename: PropTypes.string, 90 | 91 | 92 | /** 93 | * Counter of how many times the save button was pressed 94 | * (to be used mostly as input) 95 | */ 96 | trigger: PropTypes.number, 97 | 98 | /** 99 | * Sketch content as JSON string, containing background image and 100 | * annotations. Use utils.parse_json.parse_jsonstring to parse 101 | * this string. 102 | */ 103 | json_data: PropTypes.string, 104 | 105 | /** 106 | * Names of buttons to hide. Names are "zoom", "pan", "line", "pencil", 107 | * "rectangle", "undo", "select". 108 | */ 109 | hide_buttons: PropTypes.arrayOf(PropTypes.string), 110 | 111 | /** 112 | * Dash-assigned callback that should be called whenever any of the 113 | * properties change 114 | */ 115 | setProps: PropTypes.func 116 | }; 117 | 118 | export const propTypes = DashCanvas.propTypes; 119 | export const defaultProps = DashCanvas.defaultProps; 120 | -------------------------------------------------------------------------------- /src/lib/fragments/DashCanvas.react.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { SketchField, Tools } from 'react-sketch'; 3 | import { 4 | ZoomMinusIcon, ZoomPlusIcon, EditIcon, PanIcon, 5 | ArrowLeftIcon, ArrowRightIcon, PlotLineIcon, SquareIcon, TagOutlineIcon 6 | } 7 | from 'plotly-icons'; 8 | 9 | import { propTypes, defaultProps } from '../components/DashCanvas.react'; 10 | 11 | const styles = { 12 | button: { 13 | margin: '3px', 14 | padding: '0px', 15 | width: '50px', 16 | height: '50px', 17 | verticalAlign: 'middle', 18 | }, 19 | 20 | textbutton: { 21 | verticalAlign: 'top', 22 | height: '50px', 23 | color: 'blue', 24 | verticalAlign: 'middle', 25 | } 26 | }; 27 | 28 | /** 29 | * Canvas component for drawing on a background image and selecting 30 | * regions. 31 | */ 32 | export default class DashCanvas extends Component { 33 | constructor(props) { 34 | super(props); 35 | this.state = { 36 | height: 200 37 | }; 38 | this._save = this._save.bind(this); 39 | this._undo = this._undo.bind(this); 40 | this._zoom = this._zoom.bind(this); 41 | this._zoom_factor = this._zoom_factor.bind(this); 42 | this._unzoom = this._unzoom.bind(this); 43 | this._pantool = this._pantool.bind(this); 44 | this._penciltool = this._penciltool.bind(this); 45 | this._linetool = this._linetool.bind(this); 46 | this._selecttool = this._selecttool.bind(this); 47 | } 48 | 49 | 50 | componentDidMount() { 51 | let sketch = this._sketch; 52 | if (this.props.filename.length > 0 || 53 | this.props.image_content.length > 0) { 54 | var content = (this.props.filename.length > 0) ? this.props.filename : 55 | this.props.image_content; 56 | var img = new Image(); 57 | img.onload = () => { 58 | var new_height = this.state.height; 59 | var new_scale = 1; 60 | var height = img.height; 61 | var width = img.width; 62 | new_height = Math.round(height * sketch.props.width / width); 63 | new_scale = new_height / height; 64 | this.setState({ height: new_height }); 65 | sketch.clear(); 66 | let opts = { 67 | left: 0, 68 | top: 0, 69 | scale: new_scale 70 | } 71 | sketch.addImg(content, opts); 72 | } 73 | img.src = content; 74 | } else { 75 | sketch._fc.setBackgroundColor(sketch.props.backgroundColor); 76 | } 77 | } 78 | 79 | 80 | componentDidUpdate(prevProps) { 81 | let sketch = this._sketch; 82 | // Typical usage (don't forget to compare props): 83 | if ( 84 | (this.props.image_content !== prevProps.image_content)) { 85 | var img = new Image(); 86 | var new_height = this.state.height; 87 | var new_scale = 1; 88 | img.onload = () => { 89 | var height = img.height; 90 | var width = img.width; 91 | new_height = Math.round(height * sketch.props.width / width); 92 | new_scale = new_height / height; 93 | this.setState({ height: new_height }); 94 | sketch.clear(); 95 | let opts = { 96 | left: 0, 97 | top: 0, 98 | scale: new_scale 99 | } 100 | sketch.addImg(this.props.image_content, opts); 101 | } 102 | img.src = this.props.image_content; 103 | if (this.props.setProps) { 104 | let JSON_string = JSON.stringify(this._sketch.toJSON()); 105 | this.props.setProps({ json_data: JSON_string }); 106 | } 107 | 108 | sketch._fc.setZoom(this.props.zoom); 109 | }; 110 | }; 111 | 112 | 113 | _save() { 114 | let JSON_string = JSON.stringify(this._sketch.toJSON()); 115 | let toggle_value = this.props.trigger + 1 116 | if (this.props.setProps) { 117 | this.props.setProps({ json_data: JSON_string, trigger: toggle_value }); 118 | } 119 | }; 120 | 121 | 122 | _undo() { 123 | this._sketch.undo(); 124 | this.setState({ 125 | canUndo: this._sketch.canUndo(), 126 | canRedo: this._sketch.canRedo() 127 | }) 128 | }; 129 | _redo() { 130 | this._sketch.redo(); 131 | console.log(this._sketch); 132 | this.setState({ 133 | canUndo: this._sketch.canUndo(), 134 | canRedo: this._sketch.canRedo() 135 | }) 136 | }; 137 | 138 | _zoom_factor(factor) { 139 | this._sketch.zoom(factor); 140 | let zoom_factor = this.props.zoom; 141 | this.props.setProps({ zoom: factor * zoom_factor }) 142 | }; 143 | 144 | 145 | _zoom() { 146 | this._sketch.zoom(1.25); 147 | let zoom_factor = this.props.zoom; 148 | this.props.setProps({ zoom: 1.25 * zoom_factor }) 149 | }; 150 | 151 | 152 | _unzoom() { 153 | this._sketch.zoom(0.8); 154 | let zoom_factor = this.props.zoom; 155 | this.props.setProps({ zoom: 0.8 * zoom_factor }); 156 | }; 157 | 158 | 159 | _pantool() { 160 | this.props.setProps({ tool: "pan" }); 161 | }; 162 | 163 | 164 | _penciltool() { 165 | this.props.setProps({ tool: "pencil" }); 166 | }; 167 | 168 | 169 | _linetool() { 170 | this.props.setProps({ tool: "line" }); 171 | }; 172 | 173 | 174 | _rectangletool() { 175 | this.props.setProps({ tool: "rectangle" }); 176 | }; 177 | 178 | 179 | 180 | _selecttool() { 181 | this.props.setProps({ tool: "select" }); 182 | }; 183 | 184 | 185 | 186 | 187 | render() { 188 | var toolsArray = {}; 189 | toolsArray["pencil"] = Tools.Pencil; 190 | toolsArray["pan"] = Tools.Pan; 191 | toolsArray["line"] = Tools.Line; 192 | toolsArray["circle"] = Tools.Circle; 193 | toolsArray["select"] = Tools.Select; 194 | toolsArray["rectangle"] = Tools.Rectangle; 195 | const hide_buttons = this.props.hide_buttons; 196 | const show_line = !(hide_buttons.includes("line")); 197 | const show_pan = !(hide_buttons.includes("pan")); 198 | const show_zoom = !(hide_buttons.includes("zoom")); 199 | const show_pencil = !(hide_buttons.includes("pencil")); 200 | const show_undo = !(hide_buttons.includes("undo")); 201 | const show_select = !(hide_buttons.includes("select")); 202 | const show_rectangle = !(hide_buttons.includes("rectangle")); 203 | var width_defined = this.props.width > 0; 204 | var width = width_defined ? this.props.width : null; 205 | return ( 206 |
207 | this._sketch = c} 209 | tool={toolsArray[this.props.tool.toLowerCase()]} 210 | lineColor={this.props.lineColor} 211 | width={width} 212 | height={this.state.height} 213 | forceValue={true} 214 | backgroundColor='#ccddff' 215 | lineWidth={this.props.lineWidth} /> 216 | {show_zoom && 217 | 222 | } 223 | {show_zoom && 224 | 229 | } 230 | {show_pencil && 231 | 236 | } 237 | {show_line && 238 | 243 | } 244 | {show_rectangle && 245 | 250 | } 251 | {show_pan && 252 | 257 | } 258 | {show_undo && 259 | 264 | } 265 | {show_undo && 266 | 271 | } 272 | {show_select && 273 | 278 | } 279 | 280 | 285 | 286 |
287 | 288 | ) 289 | } 290 | } 291 | 292 | DashCanvas.defaultProps = defaultProps; 293 | DashCanvas.propTypes = propTypes; 294 | -------------------------------------------------------------------------------- /src/lib/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | import DashCanvas from './components/DashCanvas.react'; 3 | 4 | export { 5 | DashCanvas 6 | }; 7 | -------------------------------------------------------------------------------- /standalone_app5.py: -------------------------------------------------------------------------------- 1 | from glob import glob 2 | import numpy as np 3 | import pandas as pd 4 | from skimage import io 5 | from time import sleep 6 | 7 | import dash 8 | from dash.exceptions import PreventUpdate 9 | from dash.dependencies import Input, Output, State 10 | import dash_html_components as html 11 | import dash_core_components as dcc 12 | import dash_table 13 | 14 | import dash_canvas 15 | from dash_canvas.components import image_upload_zone 16 | from dash_canvas.utils.io_utils import (image_string_to_PILImage, 17 | array_to_data_url) 18 | from dash_canvas.utils.registration import register_tiles 19 | from dash_canvas.utils.parse_json import parse_jsonstring_line 20 | from dash_canvas.utils.exposure import brightness_adjust, contrast_adjust 21 | 22 | 23 | 24 | def tile_images(list_of_images, n_rows, n_cols): 25 | dtype = list_of_images[0].dtype 26 | if len(list_of_images) < n_rows * n_cols: 27 | white = np.zeros(list_of_images[0].shape, dtype=dtype) 28 | n_missing = n_rows * n_cols - len(list_of_images) 29 | list_of_images += [white, ] * n_missing 30 | return np.vstack([np.hstack(list_of_images[i_row*n_cols: 31 | i_row*n_cols + n_cols]) 32 | for i_row in range(n_rows)]) 33 | 34 | 35 | def untile_images(image_string, n_rows, n_cols): 36 | big_im = np.asarray(image_string_to_PILImage(image_string)) 37 | tiles = [np.split(im, n_cols, axis=1) for im in np.split(big_im, n_rows)] 38 | return np.array(tiles) 39 | 40 | 41 | def _sort_props_lines(props, height, width, ncols): 42 | props = pd.DataFrame(props) 43 | index_init = ncols * ((props['top'] - props['height'] //2) // height) + \ 44 | ((props['left'] - props['width'] //2)// width) 45 | index_end = ncols * ((props['top'] + props['height'] //2) // height) + \ 46 | ((props['left'] + props['width'] //2)// width) 47 | props['index_init'] = index_init 48 | props['index_end'] = index_end 49 | overlaps = {} 50 | print(props) 51 | for line in props.iterrows(): 52 | print(line) 53 | overlaps[(line[1]['index_init'], line[1]['index_end'])] = (line[1]['height'], 54 | line[1]['width']) 55 | print(overlaps) 56 | return overlaps 57 | 58 | 59 | def instructions(): 60 | return html.Div(children=[ 61 | html.H5(children='How to use this stitching app'), 62 | dcc.Markdown(""" 63 | - Choose the number of rows and columns of the mosaic, 64 | - Upload images. 65 | - Try automatic stitching by pressing 66 | the "Run stitching" button. 67 | - If automatic stitching did not work, 68 | try adjusting the overlap parameter. 69 | 70 | If shifts between different images are very diifferent, 71 | draw lines to match points of interest in pairs of 72 | images, then press "Estimate translation" to compute an 73 | estimate of the shifts, then press "Run stitching". 74 | """) 75 | ]) 76 | 77 | app = dash.Dash(__name__) 78 | server = app.server 79 | app.config.suppress_callback_exceptions = False 80 | 81 | height, width = 200, 500 82 | canvas_width = 800 83 | canvas_height = round(height * canvas_width / width) 84 | scale = canvas_width / width 85 | 86 | list_columns = ['length', 'width', 'height', 'left', 'top'] 87 | columns = [{"name": i, "id": i} for i in list_columns] 88 | 89 | app = dash.Dash(__name__) 90 | server = app.server 91 | app.config.suppress_callback_exceptions = True 92 | 93 | 94 | app.layout = html.Div([ 95 | html.Div([ 96 | dcc.Tabs( 97 | id='stitching-tabs', 98 | value='canvas-tab', 99 | children=[ 100 | dcc.Tab( 101 | label='Image tiles', 102 | value='canvas-tab', 103 | children=[ 104 | dash_canvas.DashCanvas( 105 | id='canvas-stitch', 106 | label='my-label', 107 | width=canvas_width, 108 | height=canvas_height, 109 | scale=scale, 110 | lineWidth=2, 111 | lineColor='red', 112 | tool="line", 113 | image_content=array_to_data_url( 114 | np.zeros((width, width), dtype=np.uint8)), 115 | goButtonTitle='Estimate translation', 116 | ), 117 | html.Button('Upload demo data', id='demo'), 118 | image_upload_zone('upload-stitch', multiple=True, 119 | width=45), 120 | html.Div(id='sh_x', hidden=True), 121 | dcc.Loading(id='loading-2', children=[ 122 | html.Div(id='stitched-res', hidden=True)], 123 | type='circle'), 124 | dcc.Store(id='memory-stitch'), 125 | ] 126 | ), 127 | dcc.Tab( 128 | label='Stitched Image', 129 | value='result-tab', 130 | children=[ 131 | dcc.Loading(id='loading-1', children=[ 132 | html.Img(id='stitching-result', 133 | src=array_to_data_url( 134 | np.zeros((height, width), dtype=np.uint8)), 135 | width=canvas_width)], 136 | type='circle'), 137 | html.Div([ 138 | html.Label('Contrast'), 139 | dcc.Slider(id='contrast-stitch', 140 | min=0, 141 | max=1, 142 | step=0.02, 143 | value=0.5)], 144 | style={'width':'40%'}), 145 | html.Div([ 146 | html.Label('Brightness'), 147 | dcc.Slider(id='brightness-stitch', 148 | min=0, 149 | max=1, 150 | step=0.02, 151 | value=0.5,)], 152 | style={'width':'40%'}), 153 | 154 | 155 | ] 156 | ), 157 | dcc.Tab( 158 | label='How to use this app', 159 | value='help-tab', 160 | children=[ 161 | html.Img(id='bla', src='./assets/stitching.gif', 162 | width=canvas_width), 163 | ] 164 | ) 165 | ] 166 | ) 167 | ], className="eight columns"), 168 | html.Div([ 169 | html.Label('Number of rows'), 170 | dcc.Input( 171 | id='nrows-stitch', 172 | type='number', 173 | value=2, 174 | name='number of rows', 175 | ), 176 | html.Label('Number of columns'), 177 | dcc.Input( 178 | id='ncolumns-stitch', 179 | type='number', 180 | value=4, 181 | name='number of columns', 182 | ), 183 | html.Label('Fraction of overlap (in [0-1] range)'), 184 | dcc.Input( 185 | id='overlap-stitch', 186 | type='float', 187 | value=0.15, 188 | ), 189 | dcc.Checklist( 190 | id='do-blending-stitch', 191 | options=[{'label':'Blending images', 'value':1}], 192 | values=[1], 193 | ), 194 | html.Label('Measured shifts between images'), 195 | dash_table.DataTable( 196 | id='table-stitch', 197 | columns=columns, 198 | editable=True, 199 | ), 200 | html.Br(), 201 | html.Button('Run stitching', id='button-stitch', 202 | style={'color':'red'}), 203 | html.Br(), 204 | instructions() 205 | ], className="three columns"), 206 | ]) 207 | 208 | 209 | 210 | @app.callback(Output('table-stitch', 'data'), 211 | [Input('canvas-stitch', 'json_data')]) 212 | def estimate_translation(string): 213 | props = parse_jsonstring_line(string) 214 | df = pd.DataFrame(props, columns=list_columns) 215 | return df.to_dict("records") 216 | 217 | 218 | @app.callback(Output('sh_x', 'children'), 219 | [Input('upload-stitch', 'contents'), 220 | Input('upload-stitch', 'filename'), 221 | Input('demo', 'n_clicks')], 222 | [State('nrows-stitch', 'value'), 223 | State('ncolumns-stitch', 'value')]) 224 | def upload_content(list_image_string, list_filenames, click, 225 | n_rows, n_cols): 226 | #if list_image_string is None: 227 | # raise PreventUpdate 228 | if list_image_string is not None: 229 | print('update canvas upload') 230 | order = np.argsort(list_filenames) 231 | image_list = [np.asarray(image_string_to_PILImage( 232 | list_image_string[i])) for i in order] 233 | res = tile_images(image_list, n_rows, n_cols) 234 | return array_to_data_url(res) 235 | elif click: 236 | filelist = glob('./assets/tile*.jpg') 237 | filelist.sort() 238 | print(filelist) 239 | image_list = [io.imread(filename) for filename in filelist] 240 | res = tile_images(image_list, n_rows, n_cols) 241 | return array_to_data_url(res) 242 | else: 243 | raise PreventUpdate 244 | #return None 245 | 246 | 247 | @app.callback(Output('stitching-tabs', 'value'), 248 | [Input('button-stitch', 'n_clicks')]) 249 | def change_focus(click): 250 | print('changing focus') 251 | if click: 252 | return 'result-tab' 253 | return 'canvas-tab' 254 | 255 | 256 | @app.callback(Output('memory-stitch', 'data'), 257 | [Input('button-stitch', 'n_clicks')]) 258 | def update_store(click): 259 | sleep(1) 260 | return click 261 | 262 | 263 | 264 | @app.callback(Output('stitching-result', 'src'), 265 | [Input('contrast-stitch', 'value'), 266 | Input('brightness-stitch', 'value'), 267 | Input('stitched-res', 'children')]) 268 | def modify_result(contrast, brightness, image_string): 269 | print('in modify result') 270 | img = np.asarray(image_string_to_PILImage(image_string)) 271 | img = contrast_adjust(img, contrast) 272 | img = brightness_adjust(img, brightness) 273 | return array_to_data_url(img) 274 | 275 | 276 | @app.callback(Output('stitched-res', 'children'), 277 | [Input('button-stitch', 'n_clicks')], 278 | [State('nrows-stitch', 'value'), 279 | State('ncolumns-stitch', 'value'), 280 | State('overlap-stitch', 'value'), 281 | State('table-stitch', 'data'), 282 | State('sh_x', 'children'), 283 | State('do-blending-stitch', 'values')]) 284 | def modify_content(n_cl, 285 | n_rows, n_cols, overlap, estimate, image_string, vals): 286 | print('in modify content') 287 | blending = 1 in vals 288 | tiles = untile_images(image_string, n_rows, n_cols) 289 | if estimate is not None and len(estimate) > 0: 290 | overlap_dict = _sort_props_lines(estimate, tiles.shape[2], 291 | tiles.shape[3], n_cols) 292 | else: 293 | overlap_dict = None 294 | canvas = register_tiles(tiles, n_rows, n_cols, 295 | overlap_global=overlap, 296 | overlap_local=overlap_dict, 297 | pad=np.max(tiles.shape[2:])//2, 298 | blending=blending) 299 | return array_to_data_url(canvas) 300 | 301 | 302 | @app.callback(Output('canvas-stitch', 'image_content'), 303 | [Input('sh_x', 'children')]) 304 | def update_canvas_image(im): 305 | print('update image content') 306 | return im 307 | 308 | 309 | @app.callback(Output('canvas-stitch', 'height'), 310 | [Input('sh_x', 'children')], 311 | [State('canvas-stitch', 'width'), 312 | State('canvas-stitch', 'height')]) 313 | def update_canvas_upload_shape(image_string, w, h): 314 | if image_string is None: 315 | raise PreventUpdate 316 | if image_string is not None: 317 | im = image_string_to_PILImage(image_string) 318 | im_h, im_w = im.height, im.width 319 | return round(w / im_w * im_h) 320 | else: 321 | return canvas_height 322 | 323 | 324 | @app.callback(Output('canvas-stitch', 'scale'), 325 | [Input('sh_x', 'children')]) 326 | def update_canvas_upload_scale(image_string): 327 | if image_string is None: 328 | raise PreventUpdate 329 | if image_string is not None: 330 | # very dirty hack, this should be made more robust using regexp 331 | im = image_string_to_PILImage(image_string) 332 | im_h, im_w = im.height, im.width 333 | return canvas_width / im_w 334 | else: 335 | return scale 336 | 337 | 338 | if __name__ == '__main__': 339 | app.run_server(debug=True) 340 | 341 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-canvas/4f4df65b43acb20bae7eb34572ca240dabfbfb72/tests/__init__.py -------------------------------------------------------------------------------- /tests/data_test.json: -------------------------------------------------------------------------------- 1 | "{\"objects\":[{\"type\":\"image\",\"originX\":\"left\",\"originY\":\"top\",\"left\":0,\"top\":0,\"width\":640,\"height\":433,\"fill\":\"rgb(0,0,0)\",\"stroke\":null,\"strokeWidth\":0,\"strokeDashArray\":null,\"strokeLineCap\":\"butt\",\"strokeLineJoin\":\"miter\",\"strokeMiterLimit\":10,\"scaleX\":0.78,\"scaleY\":0.78,\"angle\":0,\"flipX\":false,\"flipY\":false,\"opacity\":1,\"shadow\":null,\"visible\":true,\"clipTo\":null,\"backgroundColor\":\"\",\"fillRule\":\"nonzero\",\"globalCompositeOperation\":\"source-over\",\"transformMatrix\":null,\"skewX\":0,\"skewY\":0,\"crossOrigin\":\"\",\"alignX\":\"none\",\"alignY\":\"none\",\"meetOrSlice\":\"meet\",\"src\":\"https://upload.wikimedia.org/wikipedia/commons/e/e4/Mitochondria%2C_mammalian_lung_-_TEM_%282%29.jpg\",\"filters\":[],\"resizeFilters\":[]},{\"type\":\"path\",\"originX\":\"left\",\"originY\":\"top\",\"left\":67,\"top\":186.97,\"width\":39.03,\"height\":39.06,\"fill\":null,\"stroke\":\"black\",\"strokeWidth\":30,\"strokeDashArray\":null,\"strokeLineCap\":\"round\",\"strokeLineJoin\":\"round\",\"strokeMiterLimit\":10,\"scaleX\":1,\"scaleY\":1,\"angle\":0,\"flipX\":false,\"flipY\":false,\"opacity\":1,\"shadow\":null,\"visible\":true,\"clipTo\":null,\"backgroundColor\":\"\",\"fillRule\":\"nonzero\",\"globalCompositeOperation\":\"source-over\",\"transformMatrix\":null,\"skewX\":0,\"skewY\":0,\"pathOffset\":{\"x\":101.515,\"y\":221.5},\"path\":[[\"M\",82,201.97],[\"Q\",82,202,82,202.5],[\"Q\",82,203,82.5,203.5],[\"Q\",83,204,83.5,205],[\"Q\",84,206,85.5,208.5],[\"Q\",87,211,95,218],[\"Q\",103,225,104.5,227],[\"Q\",106,229,106.5,229],[\"Q\",107,229,107.5,229.5],[\"Q\",108,230,109.5,231.5],[\"Q\",111,233,113,234.5],[\"Q\",115,236,117,237.5],[\"Q\",119,239,120,240],[\"L\",121.03,241.03]]},{\"type\":\"path\",\"originX\":\"left\",\"originY\":\"top\",\"left\":208.97,\"top\":117.97,\"width\":10.06,\"height\":41.06,\"fill\":null,\"stroke\":\"black\",\"strokeWidth\":30,\"strokeDashArray\":null,\"strokeLineCap\":\"round\",\"strokeLineJoin\":\"round\",\"strokeMiterLimit\":10,\"scaleX\":1,\"scaleY\":1,\"angle\":0,\"flipX\":false,\"flipY\":false,\"opacity\":1,\"shadow\":null,\"visible\":true,\"clipTo\":null,\"backgroundColor\":\"\",\"fillRule\":\"nonzero\",\"globalCompositeOperation\":\"source-over\",\"transformMatrix\":null,\"skewX\":0,\"skewY\":0,\"pathOffset\":{\"x\":229,\"y\":153.5},\"path\":[[\"M\",223.97,132.97],[\"Q\",224,133,224.5,133.5],[\"Q\",225,134,225,135],[\"Q\",225,136,225.5,138],[\"Q\",226,140,227,143.5],[\"Q\",228,147,228.5,149.5],[\"Q\",229,152,229,154.5],[\"Q\",229,157,229.5,158.5],[\"Q\",230,160,231.5,165],[\"Q\",233,170,233.5,172],[\"L\",234.03,174.03]]},{\"type\":\"path\",\"originX\":\"left\",\"originY\":\"top\",\"left\":323.97,\"top\":105,\"width\":48.06,\"height\":4,\"fill\":null,\"stroke\":\"black\",\"strokeWidth\":30,\"strokeDashArray\":null,\"strokeLineCap\":\"round\",\"strokeLineJoin\":\"round\",\"strokeMiterLimit\":10,\"scaleX\":1,\"scaleY\":1,\"angle\":0,\"flipX\":false,\"flipY\":false,\"opacity\":1,\"shadow\":null,\"visible\":true,\"clipTo\":null,\"backgroundColor\":\"\",\"fillRule\":\"nonzero\",\"globalCompositeOperation\":\"source-over\",\"transformMatrix\":null,\"skewX\":0,\"skewY\":0,\"pathOffset\":{\"x\":363,\"y\":122},\"path\":[[\"M\",338.97,124],[\"Q\",339,124,340,124],[\"Q\",341,124,342,124],[\"Q\",343,124,346,123.5],[\"Q\",349,123,353,122],[\"Q\",357,121,360.5,120.5],[\"Q\",364,120,368.5,120],[\"Q\",373,120,376,120],[\"Q\",379,120,383,120],[\"L\",387.03,120]]}]}" 2 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | # Packages needed to run the tests. 2 | # Switch into a virtual environment 3 | # pip install -r requirements.txt 4 | 5 | dash 6 | dash-daq 7 | ipdb 8 | percy 9 | flake8 10 | pylint 11 | -------------------------------------------------------------------------------- /tests/test_dash_canvas.py: -------------------------------------------------------------------------------- 1 | import json 2 | from functools import partial 3 | 4 | from skimage import img_as_ubyte 5 | import numpy as np 6 | 7 | import dash 8 | from dash.dependencies import Input, Output, State 9 | from dash.exceptions import PreventUpdate 10 | import dash_html_components as html 11 | import dash_core_components as dcc 12 | 13 | import dash_canvas 14 | from dash_canvas.utils import array_to_data_url 15 | 16 | from selenium.webdriver.support.ui import WebDriverWait 17 | 18 | def _get_button_by_title(dash_duo, title): 19 | return dash_duo.wait_for_element( 20 | 'button[title="{}"]'.format(title) 21 | ) 22 | 23 | 24 | TIMEOUT = 10 25 | 26 | def test_canvas_undo_redo(dash_duo): 27 | h, w = 10, 10 28 | overlay = np.zeros((h, w), dtype=np.uint8) 29 | overlay = img_as_ubyte(overlay) 30 | 31 | calls = 0 32 | data_saved = [] 33 | 34 | # Set up a small app. This could probably be made into a fixture. 35 | app = dash.Dash(__name__) 36 | app.layout = html.Div([ 37 | dcc.Store(id='cache', data=''), 38 | dash_canvas.DashCanvas( 39 | id="canvas", 40 | width=w, 41 | height=h, 42 | image_content=array_to_data_url(overlay), 43 | goButtonTitle="save" 44 | ) 45 | ]) 46 | 47 | @app.callback( 48 | Output('cache', 'data'), 49 | [Input("canvas", "trigger")], 50 | [State("canvas", "json_data")] 51 | ) 52 | def update_overlay(flag, data): 53 | if flag is None or data is None: 54 | raise PreventUpdate 55 | 56 | data_saved.append(data) 57 | 58 | nonlocal calls 59 | calls = calls + 1 60 | 61 | def calls_equals(count, driver): 62 | nonlocal calls 63 | return calls == count 64 | 65 | dash_duo.start_server(app) 66 | 67 | # At application startup, a black 10x10 image is shown. When we click 68 | # save, we expect a non-trivial JSON object representing this image. We 69 | # assert that we get this object, but we don't dig into it. 70 | btn = _get_button_by_title(dash_duo, "Save") 71 | btn.click() 72 | 73 | WebDriverWait(dash_duo.driver, TIMEOUT).until(partial(calls_equals, 1)) 74 | objs_1 = json.loads(data_saved[-1])['objects'] 75 | assert len(objs_1) > 0 76 | 77 | # When we click "undo", the image disappears. We check that we get an 78 | # empty JSON representation back. 79 | btn = _get_button_by_title(dash_duo, "Undo") 80 | btn.click() 81 | btn = _get_button_by_title(dash_duo, "Save") 82 | btn.click() 83 | WebDriverWait(dash_duo.driver, TIMEOUT).until(partial(calls_equals, 2)) 84 | 85 | objs_2 = json.loads(data_saved[-1])['objects'] 86 | assert objs_2 == [] 87 | 88 | # When we click "redo", the original 10x10 black image is restored. 89 | btn = _get_button_by_title(dash_duo, "Redo") 90 | btn.click() 91 | btn = _get_button_by_title(dash_duo, "Save") 92 | btn.click() 93 | WebDriverWait(dash_duo.driver, TIMEOUT).until(partial(calls_equals, 3)) 94 | 95 | objs_3 = json.loads(data_saved[-1])['objects'] 96 | assert objs_1 == objs_3 97 | -------------------------------------------------------------------------------- /usage.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import json 3 | from skimage import io 4 | 5 | import dash_canvas 6 | import dash 7 | from dash.dependencies import Input, Output 8 | import dash_html_components as html 9 | import dash_core_components as dcc 10 | import plotly.graph_objs as go 11 | 12 | from parse_json import parse_jsonstring 13 | from image_processing_utils import watershed_segmentation 14 | from plot_utils import image_with_contour 15 | 16 | # Image to segment and shape parameters 17 | filename = 'https://upload.wikimedia.org/wikipedia/commons/e/e4/Mitochondria%2C_mammalian_lung_-_TEM_%282%29.jpg' 18 | img = io.imread(filename, as_gray=True) 19 | print(img.dtype) 20 | height, width = img.shape 21 | canvas_width = 400 22 | canvas_height = int(height * canvas_width / width) 23 | scale = canvas_width / width 24 | 25 | # ------------------ App definition --------------------- 26 | 27 | app = dash.Dash(__name__) 28 | 29 | app.css.append_css({ 30 | 'external_url': 'https://codepen.io/chriddyp/pen/bWLwgP.css' 31 | }) 32 | 33 | 34 | app.scripts.config.serve_locally = True 35 | app.css.config.serve_locally = True 36 | 37 | 38 | app.layout = html.Div([ 39 | html.Div([ 40 | html.Div([ 41 | html.H2(children='Segmentation tool'), 42 | dcc.Markdown(''' 43 | Paint on each object you want to segment 44 | then press the Save button to trigger the segmentation. 45 | '''), 46 | 47 | dash_canvas.DashCanvas( 48 | id='canvas', 49 | label='my-label', 50 | width=canvas_width, 51 | height=canvas_height, 52 | scale=scale, 53 | filename=filename, 54 | ), 55 | ], className="six columns"), 56 | html.Div([ 57 | html.H2(children='Segmentation result'), 58 | dcc.Graph( 59 | id='segmentation', 60 | figure=image_with_contour(img, img>0) 61 | ) 62 | ], className="six columns")],# Div 63 | className="row") 64 | ]) 65 | 66 | # ----------------------- Callbacks ----------------------------- 67 | 68 | @app.callback(Output('segmentation', 'figure'), 69 | [Input('canvas', 'json_data')]) 70 | def update_figure(string): 71 | mask = parse_jsonstring(string, shape=(height, width)) 72 | seg = watershed_segmentation(img, mask) 73 | return image_with_contour(img, seg) 74 | 75 | 76 | if __name__ == '__main__': 77 | app.run_server(debug=True) 78 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const WebpackDashDynamicImport = require('@plotly/webpack-dash-dynamic-import'); 3 | 4 | const packagejson = require('./package.json'); 5 | 6 | const dashLibraryName = packagejson.name.replace(/-/g, '_'); 7 | 8 | module.exports = (env, argv) => { 9 | 10 | let mode; 11 | 12 | const overrides = module.exports || {}; 13 | 14 | // if user specified mode flag take that value 15 | if (argv && argv.mode) { 16 | mode = argv.mode; 17 | } 18 | 19 | // else if configuration object is already set (module.exports) use that value 20 | else if (overrides.mode) { 21 | mode = overrides.mode; 22 | } 23 | 24 | // else take webpack default (production) 25 | else { 26 | mode = 'production'; 27 | } 28 | 29 | let filename = (overrides.output || {}).filename; 30 | if (!filename) { 31 | const modeSuffix = mode === 'development' ? 'dev' : 'min'; 32 | filename = `${dashLibraryName}.${modeSuffix}.js`; 33 | } 34 | 35 | const entry = overrides.entry || { main: './src/lib/index.js' }; 36 | 37 | const devtool = overrides.devtool || ( 38 | mode === 'development' ? "eval-source-map" : 'none' 39 | ); 40 | 41 | const externals = ('externals' in overrides) ? overrides.externals : ({ 42 | react: 'React', 43 | 'react-dom': 'ReactDOM', 44 | 'plotly.js': 'Plotly', 45 | }); 46 | 47 | return { 48 | mode, 49 | entry, 50 | output: { 51 | path: path.resolve(__dirname, dashLibraryName), 52 | chunkFilename: mode === 'development' ? '[name].dev.js' : '[name].js', 53 | filename, 54 | library: dashLibraryName, 55 | libraryTarget: 'window', 56 | }, 57 | externals, 58 | module: { 59 | rules: [ 60 | { 61 | test: /\.js$/, 62 | exclude: /node_modules/, 63 | use: { 64 | loader: 'babel-loader', 65 | }, 66 | }, 67 | { 68 | test: /\.css$/, 69 | use: [ 70 | { 71 | loader: 'style-loader', 72 | }, 73 | { 74 | loader: 'css-loader', 75 | }, 76 | ], 77 | }, 78 | ], 79 | }, 80 | devtool, 81 | optimization: { 82 | splitChunks: { 83 | name: true, 84 | cacheGroups: { 85 | async: { 86 | chunks: 'async', 87 | minSize: 0, 88 | name(module, chunks, cacheGroupKey) { 89 | return `${cacheGroupKey}-${chunks[0].name}`; 90 | } 91 | } 92 | } 93 | } 94 | }, 95 | plugins: [ 96 | new WebpackDashDynamicImport() 97 | ] 98 | } 99 | }; 100 | -------------------------------------------------------------------------------- /webpack.serve.config.js: -------------------------------------------------------------------------------- 1 | const config = require('./webpack.config.js'); 2 | 3 | config.entry = {main: './src/demo/index.js'}; 4 | config.output = {filename: 'output.js'}; 5 | config.mode = 'development'; 6 | config.externals = undefined; // eslint-disable-line 7 | config.devtool = 'inline-source-map'; 8 | module.exports = config; 9 | --------------------------------------------------------------------------------